@ -1,137 +1,42 @@
/ * ─ ─ spin-field — shared stepper-field visual ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
The single source of the "field with increment/decrement spin controls"
visual . NumberField and CssField are the same component visually ( a bordered
field + input + increment / decrement triggers + scrubber , split / stacked layouts ,
sizes / variants / colors , themeable glyphs ) ; only their value model differs ( soma :
number vs CSS value ) . Rather than clone this CSS per component — which drifted —
both declare structural identity in their morfo ( ` data-spin-field * ` , like the
toggle-group → toggle pattern ) and reuse these rules + the ` spin-field ` recipe
tokens . A theme tints per component by scoping ` [ data-number-field ] { - - s p i n -
field- … } ` ( inherited by the steppers ) ; the default is shared .
Loaded via the foundation ` @ import ` in ` index . css ` ( no ` spin-field . svelte ` to
self-import it ) . ` data-size ` / ` data-variant ` / ` data-color ` / ` data-steppers `
are set by each eidos wrapper on the same root element that carries
` data-spin-field ` . * /
[ data-spin-field ] {
--_spin-field-height : var ( --spin-field-height-md ) ;
--_spin-field-padding-inline : var ( --spin-field-padding-inline-md ) ;
--_spin-field-gap : var ( --spin-field-gap-md ) ;
--_spin-field-font-size : var ( --spin-field-font-size-md ) ;
display : inline-flex ;
align-items : center ;
inline-size : 100 % ;
min-inline-size : 0 ;
block-size : var ( --_spin-field-height ) ;
padding-inline : var ( --_spin-field-padding-inline ) ;
border : var ( --spin-field-border-width ) solid var ( --spin-field-border ) ;
border-radius : var ( --spin-field-radius ) ;
background : var ( --spin-field-bg ) ;
/ * The edge steppers break OUT of this inline padding ( negative margin ) so they
sit FLUSH against the field border ; ` overflow ` then clips them to the
rounded corners . The padding still spaces the input from the field ends . * /
/ * ─ ─ spin-field — shared stepper affordances over a Field control ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
NumberField and CssField ARE Field controls ( their box collapses ` data-field ` +
` data-field-control ` , so field . css owns the whole box CHROME — border / radius /
bg / height / padding / font / hover / focus / invalid / disabled / variant — and
the bare-input reset ) . This shared layer paints ONLY what a stepper field ADDS :
the increment / decrement triggers , the pointer scrubber , and the split / stacked
layouts . They size off field ' s OWN resolved control tokens
( ` --_field-control-height ` / ` --_field-control-padding-inline ` / ` --_field-control-gap ` ,
declared per-size on the collapsed box ) so they always match the input row —
never a parallel ` --spin-field- * ` chrome that would alias the Field base .
Loaded via the foundation ` @ import ` in ` index . css ` ( no ` spin-field . svelte ` ) ;
each field ' s eidos wrapper imports ` field . css ` so the chrome resolves . The box
overrides use the compound ` [ data-spin-field ] [ data-field-control ] ` ( 0 , 2 , 0 ) so
they beat field . css ' s ` [ data-field-control ] ` ( 0 , 1 , 0 ) regardless of load order . * /
/ * Two spin-only box overrides : ` overflow : hidden ` ( the edge steppers break OUT of
the control ' s inline padding via negative margin , then clip to the rounded
corners ) and ` gap : 0 ` ( steppers butt flush against the input via their dividers ;
the input carries the text spacing instead of a flex gap ) . * /
[ data-spin-field ] [ data-field-control ] {
overflow : hidden ;
color : var ( --spin-field-color ) ;
font-family : var ( --spin-field-font-family ) ;
font-size : var ( --_spin-field-font-size ) ;
line-height : var ( --spin-field-line-height ) ;
transition :
background var ( --spin-field-transition-duration ) var ( --spin-field-transition-ease ) ,
border-color var ( --spin-field-transition-duration ) var ( --spin-field-transition-ease ) ,
box-shadow var ( --spin-field-transition-duration ) var ( --spin-field-transition-ease ) ;
}
[ data-spin-field ] [ data-size = 'xs' ] {
--_spin-field-height : var ( --spin-field-height-xs ) ;
--_spin-field-padding-inline : var ( --spin-field-padding-inline-xs ) ;
--_spin-field-gap : var ( --spin-field-gap-xs ) ;
--_spin-field-font-size : var ( --spin-field-font-size-xs ) ;
}
[ data-spin-field ] [ data-size = 'sm' ] {
--_spin-field-height : var ( --spin-field-height-sm ) ;
--_spin-field-padding-inline : var ( --spin-field-padding-inline-sm ) ;
--_spin-field-gap : var ( --spin-field-gap-sm ) ;
--_spin-field-font-size : var ( --spin-field-font-size-sm ) ;
}
[ data-spin-field ] [ data-size = 'lg' ] {
--_spin-field-height : var ( --spin-field-height-lg ) ;
--_spin-field-padding-inline : var ( --spin-field-padding-inline-lg ) ;
--_spin-field-gap : var ( --spin-field-gap-lg ) ;
--_spin-field-font-size : var ( --spin-field-font-size-lg ) ;
}
[ data-spin-field ] [ data-size = 'xl' ] {
--_spin-field-height : var ( --spin-field-height-xl ) ;
--_spin-field-padding-inline : var ( --spin-field-padding-inline-xl ) ;
--_spin-field-gap : var ( --spin-field-gap-xl ) ;
--_spin-field-font-size : var ( --spin-field-font-size-xl ) ;
}
[ data-spin-field ] [ data-variant = 'outline' ] {
background : transparent ;
}
[ data-spin-field ] [ data-variant = 'ghost' ] {
border-color : transparent ;
background : transparent ;
padding-inline : 0 ;
}
[ data-spin-field ] [ data-focused ] ,
[ data-spin-field ] [ data-scrubbing ] {
border-color : var ( --spin-field-border-focus ) ;
/* Focus ring as a flush outline (no offset → no gap) — survives forced-colors. */
outline : var ( --focus-ring-width ) solid var ( --focus-ring-color ) ;
outline-offset : 0 ;
}
[ data-spin-field ] [ data-invalid ] {
border-color : var ( --spin-field-border-invalid ) ;
}
/ * Read-only — value visible , field reads non-editable ( muted surface , no
text cursor ) , mirroring the date-field control . Steppers / scrubber keep
rendering ; soma already ignores their input . * /
[ data-spin-field ] [ data-readonly ] {
background : var ( --color-surface-muted ) ;
}
[ data-spin-field ] [ data-readonly ] [ data-spin-field-input ] {
cursor : default ;
}
[ data-spin-field ] [ data-disabled ] {
opacity : var ( --spin-field-disabled-opacity ) ;
gap : 0 ;
}
[ data-spin-field-input ] {
flex : 1 1 auto ;
order : 1 ;
min-inline-size : 0 ;
/ * No gap on the field — the steppers butt right up against the input via their
divider . This padding ( the former gap value ) keeps the text off the divider . * /
padding-inline : var ( --_spin-field-gap ) ;
border : 0 ;
background : transparent ;
color : inherit ;
font : inherit ;
font-weight : var ( --font-weight-medium ) ;
outline : none ;
transition : box-shadow var ( --spin-field-transition-duration )
var ( --spin-field-transition-ease ) ;
/* Keep the text off the stepper dividers (the box gap is 0). */
padding-inline : var ( --_field-control-gap ) ;
}
/ * Split layout ( default ) — decrement flush at the inline-start edge , increment
flush at the inline-end edge , scrubber tucked just inside the increment .
` order ` makes the arrangement independent of composition order and flips under
RTL ; the negative margin on the two end triggers cancels the field ' s inline
padding so they reach the border ; the divider sits on each trigger ' s INNER
side ( decrement → end , increment → start ) , flipping under RTL . * /
flush at the inline-end edge , scrubber tucked just inside the increment . ` order `
makes the arrangement independent of composition order and flips under RTL ; the
negative margin cancels the field ' s inline padding so they reach the border ; the
divider sits on each trigger ' s INNER side ( decrement → end , increment → start ) . * /
[ data-spin-field ] : not ( [ data-steppers = 'stacked' ] ) [ data-spin-field-decrement-trigger ] {
order : 0 ;
margin-inline-start : calc ( -1 * var ( --_ spin- field-padding-inline) ) ;
margin-inline-start : calc ( -1 * var ( --_field-control-padding-inline ) ) ;
border-inline-end : var ( --spin-field-control-border-width ) solid
var ( --spin-field-control-divider ) ;
}
@ -140,19 +45,11 @@
}
[ data-spin-field ] : not ( [ data-steppers = 'stacked' ] ) [ data-spin-field-increment-trigger ] {
order : 3 ;
margin-inline-end : calc ( -1 * var ( --_ spin- field-padding-inline) ) ;
margin-inline-end : calc ( -1 * var ( --_ field-control -padding-inline) ) ;
border-inline-start : var ( --spin-field-control-border-width ) solid
var ( --spin-field-control-divider ) ;
}
[ data-spin-field-input ] [ data-disabled ] {
cursor : not-allowed ;
}
/ * Focus is shown once , by the root ' s canonical two-ring focus-shadow
( ` [ data-spin-field ] [ data-focused ] ` ) . No separate ring on the input — that
stacked a second ( inner ) frame inside the root ring . * /
[ data-spin-field-increment-trigger ] ,
[ data-spin-field-decrement-trigger ] ,
[ data-spin-field-scrubber ] {
@ -160,11 +57,11 @@
flex : 0 0 auto ;
align-items : center ;
justify-content : center ;
/ * Stepper buttons and scrubber inherit the Provider ' s resolved height so
they always match the input row , regardless of size token . * /
inline-size : var ( --_ spin- field-height) ;
block-size : var ( --_ spin- field-height) ;
min-inline-size : var ( --_ spin- field-height) ;
/ * Stepper buttons + scrubber take the field ' s resolved control height so they
always match the input row , at every size . * /
inline-size : var ( --_ field-control -height) ;
block-size : var ( --_ field-control -height) ;
min-inline-size : var ( --_ field-control -height) ;
border : var ( --spin-field-control-border-width ) solid var ( --spin-field-control-border ) ;
border-radius : var ( --spin-field-control-radius ) ;
background : var ( --spin-field-control-bg ) ;
@ -242,50 +139,44 @@
}
/ * ─ ─ Stacked steppers — compact column on the inline-end side ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─
A 2 × 2 grid : the input fills the inline-start , the increment ( top ) and
decrement ( bottom ) share one narrow column on the inline-end . The grid mirrors
under RTL , so the column moves to the inline-start ( left ) automatically . * /
[ data-spin-field ] [ data-steppers = 'stacked' ] {
A 2 × 2 grid : the input fills the inline-start , the increment ( top ) and decrement
( bottom ) share one narrow column on the inline-end . The grid mirrors under RTL . * /
[ data-spin-field ] [ data-steppers = 'stacked' ] [ data-field-control ] {
display : grid ;
grid-template-columns : 1fr auto ;
/ * ` minmax ( 0 , … ) ` lets each row be EXACTLY half the field height . Plain ` 1fr `
has an implicit ` min-content ` floor , so a glyph taller than half the field
( small sizes ) would grow its row and push the other past the clipped edge —
the bug where decrement looked cut off and the two arrows were unequal . * /
/ * ` minmax ( 0 , … ) ` lets each row be EXACTLY half the field height — plain ` 1fr ` has
an implicit min-content floor , so a glyph taller than half the field would grow
its row and push the other past the clipped edge . * /
grid-template-rows : minmax ( 0 , 1fr ) minmax ( 0 , 1fr ) ;
grid-template-areas :
'input increment'
'input decrement' ;
/ * The base field rule sets ` align-items : center ` for its inline-flex layout ;
once we switch to grid that would center each arrow at its content height
instead of filling its row . Restore the grid default so each arrow stretches
to half the field height — making the stepper height a function of ` size ` . * /
/ * Grid default ( field . css control centers for its flex row ) ; stretch so each
arrow fills its half-height row , making the stepper height a function of size . * /
align-items : stretch ;
/ * The stepper column reaches the edge itself , so the field drops its inline
padding; the input carries it instead . * /
/ * The stepper column reaches the edge itself , so the box drops its inline padding ;
the input carries it instead . * /
padding-inline : 0 ;
}
[ data-spin-field ] [ data-steppers = 'stacked' ] [ data-spin-field-input ] {
grid-area : input ;
padding-inline : var ( --_ spin- field-padding-inline) ;
padding-inline : var ( --_ field-control -padding-inline) ;
}
[ data-spin-field ] [ data-steppers = 'stacked' ] [ data-spin-field-increment-trigger ] ,
[ data-spin-field ] [ data-steppers = 'stacked' ] [ data-spin-field-decrement-trigger ] {
inline-size : calc ( var ( --_ spin- field-height) * 0 . 62 ) ;
min-inline-size : calc ( var ( --_ spin- field-height) * 0 . 62 ) ;
inline-size : calc ( var ( --_ field-control -height) * 0 . 62 ) ;
min-inline-size : calc ( var ( --_ field-control -height) * 0 . 62 ) ;
/* each arrow takes half the field height (the grid row), not the full height */
block-size : auto ;
/* the split layout's negative edge margins don't apply here */
margin : 0 ;
/* vertical divider toward the input */
border-inline-start : var ( --spin-field-control-border-width ) solid
var ( --spin-field-border ) ;
/ * Glyph scales with the field height ( ≈ a third of it ) , so it stays inside the
half-height cell at every size — including ` xs ` , where a fixed ` 0 . 9em ` was too
tall and overflowed . Scales up cleanly through ` xl ` . * /
font-size : calc ( var ( --_spin-field-height ) * 0 . 34 ) ;
border-inline-start : var ( --spin-field-control-border-width ) solid var ( --spin-field-border ) ;
/ * Glyph scales with the field height ( ≈ a third ) , so it stays inside the
half-height cell at every size . * /
font-size : calc ( var ( --_field-control-height ) * 0 . 34 ) ;
line-height : 1 ;
}
@ -296,8 +187,7 @@
[ data-spin-field ] [ data-steppers = 'stacked' ] [ data-spin-field-decrement-trigger ] {
grid-area : decrement ;
/* horizontal divider between the two arrows */
border-block-start : var ( --spin-field-control-border-width ) solid
var ( --spin-field-border ) ;
border-block-start : var ( --spin-field-control-border-width ) solid var ( --spin-field-border ) ;
}
/* No room for the pointer scrubber in the stacked layout. */
@ -306,7 +196,6 @@
}
@ media ( prefers-reduced-motion : reduce ) {
[ data-spin-field ] ,
[ data-spin-field-increment-trigger ] ,
[ data-spin-field-decrement-trigger ] ,
[ data-spin-field-scrubber ] {