|
|
|
|
@ -0,0 +1,301 @@
|
|
|
|
|
/* ── 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] { --spin-
|
|
|
|
|
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-px: var(--spin-field-px-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-px);
|
|
|
|
|
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. */
|
|
|
|
|
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-px: var(--spin-field-px-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-px: var(--spin-field-px-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-px: var(--spin-field-px-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-px: var(--spin-field-px-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);
|
|
|
|
|
box-shadow: var(--spin-field-focus-shadow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field][data-invalid] {
|
|
|
|
|
border-color: var(--spin-field-border-invalid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field][data-disabled] {
|
|
|
|
|
opacity: var(--spin-field-disabled-opacity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
outline: none;
|
|
|
|
|
transition: box-shadow var(--spin-field-transition-duration)
|
|
|
|
|
var(--spin-field-transition-ease);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 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. */
|
|
|
|
|
[data-spin-field]:not([data-steppers='stacked']) [data-spin-field-decrement-trigger] {
|
|
|
|
|
order: 0;
|
|
|
|
|
margin-inline-start: calc(-1 * var(--_spin-field-px));
|
|
|
|
|
border-inline-end: var(--spin-field-control-border-width) solid
|
|
|
|
|
var(--spin-field-control-divider);
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field]:not([data-steppers='stacked']) [data-spin-field-scrubber] {
|
|
|
|
|
order: 2;
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field]:not([data-steppers='stacked']) [data-spin-field-increment-trigger] {
|
|
|
|
|
order: 3;
|
|
|
|
|
margin-inline-end: calc(-1 * var(--_spin-field-px));
|
|
|
|
|
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] {
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
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);
|
|
|
|
|
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);
|
|
|
|
|
color: var(--spin-field-control-color);
|
|
|
|
|
font: inherit;
|
|
|
|
|
outline: none;
|
|
|
|
|
transition:
|
|
|
|
|
background var(--spin-field-transition-duration) var(--spin-field-transition-ease),
|
|
|
|
|
border-color var(--spin-field-transition-duration) var(--spin-field-transition-ease),
|
|
|
|
|
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-increment-trigger],
|
|
|
|
|
[data-spin-field-decrement-trigger] {
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field-scrubber] {
|
|
|
|
|
cursor: ew-resize;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field-increment-trigger]:hover:not([data-disabled]),
|
|
|
|
|
[data-spin-field-decrement-trigger]:hover:not([data-disabled]),
|
|
|
|
|
[data-spin-field-scrubber]:hover:not([data-disabled]) {
|
|
|
|
|
border-color: var(--spin-field-control-border-hover);
|
|
|
|
|
background: var(--spin-field-control-bg-hover);
|
|
|
|
|
color: var(--spin-field-control-color-hover);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field-increment-trigger]:focus-visible,
|
|
|
|
|
[data-spin-field-decrement-trigger]:focus-visible,
|
|
|
|
|
[data-spin-field-scrubber]:focus-visible {
|
|
|
|
|
background: var(--spin-field-control-bg-focus);
|
|
|
|
|
color: var(--spin-field-control-color-hover);
|
|
|
|
|
box-shadow: var(--spin-field-control-focus-shadow);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field-increment-trigger][data-disabled],
|
|
|
|
|
[data-spin-field-decrement-trigger][data-disabled],
|
|
|
|
|
[data-spin-field-scrubber][data-disabled] {
|
|
|
|
|
cursor: default;
|
|
|
|
|
opacity: var(--spin-field-control-disabled-opacity);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field-scrubber][data-scrubbing] {
|
|
|
|
|
border-color: var(--spin-field-control-border-active);
|
|
|
|
|
background: var(--spin-field-control-bg-active);
|
|
|
|
|
color: var(--spin-field-control-color-active);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Default stepper glyphs (themeable) ───────────────────────────────────────
|
|
|
|
|
A trigger with NO children renders the glyph from a token via `:empty::before`;
|
|
|
|
|
passing children overrides it. The glyph is decorative — the button is labeled
|
|
|
|
|
by its aria-label. The internal var picks the split (+/−) or stacked (▲/▼) token
|
|
|
|
|
so a single `content` rule serves both layouts; a theme overrides any token. */
|
|
|
|
|
[data-spin-field-increment-trigger] {
|
|
|
|
|
--_spin-field-increment-glyph: var(--spin-field-control-increment-glyph);
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field-decrement-trigger] {
|
|
|
|
|
--_spin-field-decrement-glyph: var(--spin-field-control-decrement-glyph);
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field][data-steppers='stacked'] [data-spin-field-increment-trigger] {
|
|
|
|
|
--_spin-field-increment-glyph: var(--spin-field-control-increment-glyph-stacked);
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field][data-steppers='stacked'] [data-spin-field-decrement-trigger] {
|
|
|
|
|
--_spin-field-decrement-glyph: var(--spin-field-control-decrement-glyph-stacked);
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field-increment-trigger]:empty::before {
|
|
|
|
|
content: var(--_spin-field-increment-glyph);
|
|
|
|
|
}
|
|
|
|
|
[data-spin-field-decrement-trigger]:empty::before {
|
|
|
|
|
content: var(--_spin-field-decrement-glyph);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── 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'] {
|
|
|
|
|
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. */
|
|
|
|
|
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`. */
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
/* The stepper column reaches the edge itself, so the field 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-px);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
/* 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);
|
|
|
|
|
line-height: 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[data-spin-field][data-steppers='stacked'] [data-spin-field-increment-trigger] {
|
|
|
|
|
grid-area: increment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* No room for the pointer scrubber in the stacked layout. */
|
|
|
|
|
[data-spin-field][data-steppers='stacked'] [data-spin-field-scrubber] {
|
|
|
|
|
display: none;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
[data-spin-field],
|
|
|
|
|
[data-spin-field-increment-trigger],
|
|
|
|
|
[data-spin-field-decrement-trigger],
|
|
|
|
|
[data-spin-field-scrubber] {
|
|
|
|
|
transition: none;
|
|
|
|
|
}
|
|
|
|
|
}
|