@signal9/era-ui 2.26.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,7 +29,21 @@ Every component has a passive markdown doc at `/<slug>.md`.
29
29
  Examples: `/button.md`, `/select.md`, `/table.md`.
30
30
  Full concatenated reference: `/llms-full.txt`.
31
31
 
32
- <!-- Full component documentation, concatenated. -->
32
+ ## CSS utilities
33
+
34
+ Classes, not components — they need no import beyond the stylesheet. Reach for one of these before hand-rolling the same styling or bending a component into the role.
35
+
36
+ - `era-interactive` — the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.
37
+ - `era-link` — understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text).
38
+ - `scrollbar-none` — hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.
39
+ - `era-text-trim` — vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings.
40
+ - `era-text-trim-caps` — the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
41
+ - `era-shimmer` — the in-progress text treatment — put it on a "working…" / "thinking…" label while a task streams, instead of a spinner beside the text.
42
+ - `glass-blur` — the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface="glass" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface.
43
+
44
+ Full reference (what each emits, when to use it): `/utilities.md`. Machine-readable list: `/utilities.json`.
45
+
46
+ <!-- Full component documentation, concatenated. The utilities page below carries the CSS each utility emits. -->
33
47
 
34
48
  <!-- begin: about -->
35
49
 
@@ -71,6 +85,206 @@ Copy-paste text styling — headings, prose, emphasis, and motion-aware links.
71
85
 
72
86
  <!-- end: text -->
73
87
 
88
+ <!-- begin: utilities -->
89
+
90
+ # Utilities
91
+
92
+ The CSS classes era ships alongside the components — links, ink-centred text, shimmer, hidden scrollbars.
93
+
94
+ ## Overview
95
+
96
+ era ships a handful of Tailwind v4 `@utility` classes for the patterns that are
97
+ styling, not components — an inline link, ink-centred control text, a hidden
98
+ scrollbar. They need no import: they ride along with the stylesheet.
99
+
100
+ ```ts
101
+ import "@sig-nine/era-ui/css";
102
+ ```
103
+
104
+ ```svelte
105
+ <a class="era-link" href="/spacing">the spacing ladder</a>
106
+ ```
107
+
108
+ | Utility | Use it for |
109
+ |---|---|
110
+ | `era-interactive` | the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants. |
111
+ | `era-link` | understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text). |
112
+ | `scrollbar-none` | hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea. |
113
+ | `era-text-trim` | vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings. |
114
+ | `era-text-trim-caps` | the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text. |
115
+ | `era-shimmer` | the in-progress text treatment — put it on a "working…" / "thinking…" label while a task streams, instead of a spinner beside the text. |
116
+ | `glass-blur` | the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface="glass" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface. |
117
+
118
+ ## era-interactive
119
+
120
+ **Use it for:** the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.
121
+
122
+ Shared interactive-state recipe: quiet focus, disabled affordance.
123
+ Equivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.
124
+
125
+ Declared in `index.css`.
126
+
127
+ ```css
128
+ @utility era-interactive {
129
+ &:focus {
130
+ outline: none;
131
+ }
132
+ &:disabled {
133
+ cursor: not-allowed;
134
+ opacity: 0.5;
135
+ }
136
+ }
137
+ ```
138
+
139
+ ## era-link
140
+
141
+ **Use it for:** understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text).
142
+
143
+ Inline text link. An understated, permanent underline that darkens from
144
+ muted to the text colour on hover — no layout shift, no appearing/vanishing
145
+ underline. The colour fade rides the MOTION axis (--era-duration/-ease), so
146
+ it snaps at data-motion="instant", eases at "normal", and sweeps longer at
147
+ "extra" — matching every other era transition. Font-agnostic: it styles
148
+ decoration + colour only, so it reads correctly in mono, sans, or serif, and
149
+ the 0.2em offset scales with the type size. Keyboard focus mirrors hover so
150
+ the affordance is reachable without a pointer.
151
+
152
+ Declared in `index.css`.
153
+
154
+ ```css
155
+ @utility era-link {
156
+ /* The text is bright at rest and never changes — the link reads as a link
157
+ * before you touch it. Only the UNDERLINE reacts: a faint half-strength-muted
158
+ * hint at rest that resolves to the full text colour on hover. */
159
+ color: var(--color-bright);
160
+ text-decoration-line: underline;
161
+ text-decoration-color: color-mix(in oklch, var(--color-muted) 50%, transparent);
162
+ text-underline-offset: 0.2em;
163
+ transition: text-decoration-color var(--era-duration) var(--era-ease);
164
+ &:hover,
165
+ &:focus-visible {
166
+ text-decoration-color: var(--color-fg);
167
+ }
168
+ }
169
+ ```
170
+
171
+ ## scrollbar-none
172
+
173
+ **Use it for:** hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.
174
+
175
+ Scrollable but chromeless — hides the scrollbar while keeping wheel / touch /
176
+ drag scrolling (e.g. an overflowing tab strip in a small pane handle).
177
+
178
+ Declared in `index.css`.
179
+
180
+ ```css
181
+ @utility scrollbar-none {
182
+ scrollbar-width: none;
183
+ &::-webkit-scrollbar {
184
+ display: none;
185
+ }
186
+ }
187
+ ```
188
+
189
+ ## era-text-trim
190
+
191
+ **Use it for:** vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings.
192
+
193
+ Font-agnostic vertical ink-centering for single-line control text.
194
+ Measured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):
195
+ the offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in
196
+ a 24px row places ink on a half pixel, ~1px error) and font ascent/descent
197
+ asymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and
198
+ is the Firefox fallback; text-box ex/alphabetic fixes the second by
199
+ centering the x-height band — the visual mass of MIXED-CASE text (caps and
200
+ descenders cancel above/below). cap alphabetic is the spec-example recipe
201
+ but is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.
202
+ Where text-box is supported it fully determines the box, so the two
203
+ declarations never fight. Worst-case residual: 0.44px.
204
+
205
+ The one hard rule concerns overflow clipping, and it is HEIGHT-conditional.
206
+ On an AUTO-height element that also clips — a `truncate` child, a
207
+ <textarea>, an auto-height <input> — trim-both collapses the box to the
208
+ x-height band (~7.5px at 14px) and the clip slices off every ascender, cap,
209
+ and descender (text renders as a chopped middle stripe): NEVER do that.
210
+ A FIXED-height control is the intended home, and is safe: the explicit
211
+ `h-(--era-*)` overrides the collapse, so a single-line <input> (Input,
212
+ command-input) and clipping display tokens (KV) correctly carry it. For
213
+ truncating text, put the trim on the fixed-height ROW and let the child
214
+ center via the row's items-center.
215
+
216
+ Declared in `index.css`.
217
+
218
+ ```css
219
+ @utility era-text-trim {
220
+ line-height: 1;
221
+ text-box: trim-both ex alphabetic;
222
+ }
223
+ ```
224
+
225
+ ## era-text-trim-caps
226
+
227
+ **Use it for:** the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
228
+
229
+ ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
230
+ misjudges caps-only strings — cap/alphabetic edges are the right box.
231
+
232
+ Declared in `index.css`.
233
+
234
+ ```css
235
+ @utility era-text-trim-caps {
236
+ line-height: 1;
237
+ text-box: trim-both cap alphabetic;
238
+ }
239
+ ```
240
+
241
+ ## era-shimmer
242
+
243
+ **Use it for:** the in-progress text treatment — put it on a "working…" / "thinking…" label while a task streams, instead of a spinner beside the text.
244
+
245
+ In-progress text shimmer — a bright sweep through muted text (the standard
246
+ "working…" label treatment in streaming UIs). Timing derives from the motion
247
+ axis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the
248
+ duration is 0s and the sweep freezes on its base frame — plain muted text —
249
+ so reduced-motion and data-motion="instant" both kill it for free.
250
+
251
+ Declared in `index.css`.
252
+
253
+ ```css
254
+ @utility era-shimmer {
255
+ background: linear-gradient(
256
+ 110deg,
257
+ var(--color-muted) 0% 43%,
258
+ var(--color-bright) 50%,
259
+ var(--color-muted) 57% 100%
260
+ );
261
+ background-size: 200% 100%;
262
+ background-clip: text;
263
+ -webkit-text-fill-color: transparent;
264
+ animation: era-shimmer calc(var(--era-duration) * 10) cubic-bezier(0.7, 0, 1, 0.4) infinite;
265
+ }
266
+ ```
267
+
268
+ ## glass-blur
269
+
270
+ **Use it for:** the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface="glass" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface.
271
+
272
+ Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter
273
+ is a registered utility, gated to glass subtrees.
274
+
275
+ Declared in `surfaces/glass.css`.
276
+
277
+ ```css
278
+ @utility glass-blur {
279
+ [data-surface='glass'] & {
280
+ backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
281
+ -webkit-backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
282
+ }
283
+ }
284
+ ```
285
+
286
+ <!-- end: utilities -->
287
+
74
288
  <!-- begin: draggable -->
75
289
 
76
290
  # Draggable
@@ -275,6 +489,7 @@ and screen-reader accessible names don't change shape mid-flight. |
275
489
 
276
490
  - Heights, radii, paddings, and gaps come from density tokens — never hard-code px.
277
491
  - `link` variant intentionally does not change color on hover (underline only) so it reads as static prose until confirmed interactive.
492
+ - `variant="link"` is for a _control_ that should look like a link (a button that reveals a panel, a destructive action in a row). For a link in body copy or prose, use the `era-link` CSS utility on an `<a>` instead — see [Utilities](/utilities.md).
278
493
  - For a segmented row of buttons, use `ButtonGroup`.
279
494
 
280
495
  <!-- end: button -->
@@ -2137,6 +2352,14 @@ Inherits all props from `ScrollArea.CornerProps`.
2137
2352
  |------|------|---------|-------|
2138
2353
  | `ref?` | `(forwarded)` | `null` | bindable |
2139
2354
 
2355
+ ## Notes
2356
+
2357
+ - For a region that should scroll but show no scrollbar at all — an overflowing
2358
+ tab strip, a chip rail, a small pane handle — you don't need ScrollArea: put
2359
+ the `scrollbar-none` CSS utility on the overflowing element. See
2360
+ [Utilities](/utilities.md). ScrollArea is for the case where the bar itself is
2361
+ part of the design.
2362
+
2140
2363
  <!-- end: scroll-area -->
2141
2364
 
2142
2365
  <!-- begin: input -->
@@ -31,4 +31,18 @@ Full concatenated reference: `{{ORIGIN}}/llms-full.txt`.
31
31
 
32
32
  ## Components
33
33
 
34
- about, spacing, surfaces, measurements, text, draggable, llm-shell, os, aspect-ratio, badge, avatar, bar, button, button-group, pane, card, chip, code-block, copy-button, cycle, kv, separator, sheet, skeleton, switch, toggle, accordion, alert-dialog, calendar, checkbox, collapsible, combobox, command, command-bar, step, context-menu, date-field, date-picker, date-range-field, date-range-picker, dialog, dropdown-menu, file-upload, scroll-area, input, link-preview, label, menu, menubar, meter, navigation-menu, mode, pagination, pin-input, popover, progress, range-calendar, radio-group, rating-group, select, slider, table, tabs, time-field, time-range-field, toggle-group, toolbar, video-player, tooltip
34
+ about, spacing, surfaces, measurements, text, utilities, draggable, llm-shell, os, aspect-ratio, badge, avatar, bar, button, button-group, pane, card, chip, code-block, copy-button, cycle, kv, separator, sheet, skeleton, switch, toggle, accordion, alert-dialog, calendar, checkbox, collapsible, combobox, command, command-bar, step, context-menu, date-field, date-picker, date-range-field, date-range-picker, dialog, dropdown-menu, file-upload, scroll-area, input, link-preview, label, menu, menubar, meter, navigation-menu, mode, pagination, pin-input, popover, progress, range-calendar, radio-group, rating-group, select, slider, table, tabs, time-field, time-range-field, toggle-group, toolbar, video-player, tooltip
35
+
36
+ ## CSS utilities
37
+
38
+ Classes, not components — they need no import beyond the stylesheet. Reach for one of these before hand-rolling the same styling or bending a component into the role.
39
+
40
+ - `era-interactive` — the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.
41
+ - `era-link` — understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text).
42
+ - `scrollbar-none` — hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.
43
+ - `era-text-trim` — vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings.
44
+ - `era-text-trim-caps` — the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
45
+ - `era-shimmer` — the in-progress text treatment — put it on a "working…" / "thinking…" label while a task streams, instead of a spinner beside the text.
46
+ - `glass-blur` — the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface="glass" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface.
47
+
48
+ Full reference (what each emits, when to use it): `{{ORIGIN}}/utilities.md`. Machine-readable list: `{{ORIGIN}}/utilities.json`.
@@ -40,6 +40,23 @@
40
40
  "sections": [],
41
41
  "file": "text.md"
42
42
  },
43
+ {
44
+ "slug": "utilities",
45
+ "title": "Utilities",
46
+ "summary": "The CSS classes era ships alongside the components — links, ink-centred text, shimmer, hidden scrollbars.",
47
+ "tokenEstimate": 1989,
48
+ "sections": [
49
+ "Overview",
50
+ "era-interactive",
51
+ "era-link",
52
+ "scrollbar-none",
53
+ "era-text-trim",
54
+ "era-text-trim-caps",
55
+ "era-shimmer",
56
+ "glass-blur"
57
+ ],
58
+ "file": "utilities.md"
59
+ },
43
60
  {
44
61
  "slug": "draggable",
45
62
  "title": "Draggable",
@@ -116,7 +133,7 @@
116
133
  "slug": "button",
117
134
  "title": "Button",
118
135
  "summary": "A flexible button or link element.",
119
- "tokenEstimate": 390,
136
+ "tokenEstimate": 453,
120
137
  "sections": [
121
138
  "Import",
122
139
  "Props",
@@ -564,14 +581,15 @@
564
581
  "slug": "scroll-area",
565
582
  "title": "Scroll Area",
566
583
  "summary": "A cross-platform scroll container.",
567
- "tokenEstimate": 314,
584
+ "tokenEstimate": 399,
568
585
  "sections": [
569
586
  "Import",
570
587
  "ScrollArea.Root",
571
588
  "ScrollArea.Viewport",
572
589
  "ScrollArea.Scrollbar",
573
590
  "ScrollArea.Thumb",
574
- "ScrollArea.Corner"
591
+ "ScrollArea.Corner",
592
+ "Notes"
575
593
  ],
576
594
  "file": "scroll-area.md"
577
595
  },
@@ -892,5 +910,56 @@
892
910
  ],
893
911
  "file": "tooltip.md"
894
912
  }
913
+ ],
914
+ "utilities": [
915
+ {
916
+ "name": "era-interactive",
917
+ "file": "index.css",
918
+ "useCase": "the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.",
919
+ "description": "Shared interactive-state recipe: quiet focus, disabled affordance.\nEquivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.",
920
+ "css": "@utility era-interactive {\n\t&:focus {\n\t\toutline: none;\n\t}\n\t&:disabled {\n\t\tcursor: not-allowed;\n\t\topacity: 0.5;\n\t}\n}"
921
+ },
922
+ {
923
+ "name": "era-link",
924
+ "file": "index.css",
925
+ "useCase": "understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant=\"link\"` (which is a control, not running text).",
926
+ "description": "Inline text link. An understated, permanent underline that darkens from\nmuted to the text colour on hover — no layout shift, no appearing/vanishing\nunderline. The colour fade rides the MOTION axis (--era-duration/-ease), so\nit snaps at data-motion=\"instant\", eases at \"normal\", and sweeps longer at\n\"extra\" — matching every other era transition. Font-agnostic: it styles\ndecoration + colour only, so it reads correctly in mono, sans, or serif, and\nthe 0.2em offset scales with the type size. Keyboard focus mirrors hover so\nthe affordance is reachable without a pointer.",
927
+ "css": "@utility era-link {\n\t/* The text is bright at rest and never changes — the link reads as a link\n\t * before you touch it. Only the UNDERLINE reacts: a faint half-strength-muted\n\t * hint at rest that resolves to the full text colour on hover. */\n\tcolor: var(--color-bright);\n\ttext-decoration-line: underline;\n\ttext-decoration-color: color-mix(in oklch, var(--color-muted) 50%, transparent);\n\ttext-underline-offset: 0.2em;\n\ttransition: text-decoration-color var(--era-duration) var(--era-ease);\n\t&:hover,\n\t&:focus-visible {\n\t\ttext-decoration-color: var(--color-fg);\n\t}\n}"
928
+ },
929
+ {
930
+ "name": "scrollbar-none",
931
+ "file": "index.css",
932
+ "useCase": "hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.",
933
+ "description": "Scrollable but chromeless — hides the scrollbar while keeping wheel / touch /\ndrag scrolling (e.g. an overflowing tab strip in a small pane handle).",
934
+ "css": "@utility scrollbar-none {\n\tscrollbar-width: none;\n\t&::-webkit-scrollbar {\n\t\tdisplay: none;\n\t}\n}"
935
+ },
936
+ {
937
+ "name": "era-text-trim",
938
+ "file": "index.css",
939
+ "useCase": "vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings.",
940
+ "description": "Font-agnostic vertical ink-centering for single-line control text.\nMeasured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):\nthe offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in\na 24px row places ink on a half pixel, ~1px error) and font ascent/descent\nasymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and\nis the Firefox fallback; text-box ex/alphabetic fixes the second by\ncentering the x-height band — the visual mass of MIXED-CASE text (caps and\ndescenders cancel above/below). cap alphabetic is the spec-example recipe\nbut is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.\nWhere text-box is supported it fully determines the box, so the two\ndeclarations never fight. Worst-case residual: 0.44px.\n\nThe one hard rule concerns overflow clipping, and it is HEIGHT-conditional.\nOn an AUTO-height element that also clips — a `truncate` child, a\n<textarea>, an auto-height <input> — trim-both collapses the box to the\nx-height band (~7.5px at 14px) and the clip slices off every ascender, cap,\nand descender (text renders as a chopped middle stripe): NEVER do that.\nA FIXED-height control is the intended home, and is safe: the explicit\n`h-(--era-*)` overrides the collapse, so a single-line <input> (Input,\ncommand-input) and clipping display tokens (KV) correctly carry it. For\ntruncating text, put the trim on the fixed-height ROW and let the child\ncenter via the row's items-center.",
941
+ "css": "@utility era-text-trim {\n\tline-height: 1;\n\ttext-box: trim-both ex alphabetic;\n}"
942
+ },
943
+ {
944
+ "name": "era-text-trim-caps",
945
+ "file": "index.css",
946
+ "useCase": "the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.",
947
+ "description": "ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band\nmisjudges caps-only strings — cap/alphabetic edges are the right box.",
948
+ "css": "@utility era-text-trim-caps {\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}"
949
+ },
950
+ {
951
+ "name": "era-shimmer",
952
+ "file": "index.css",
953
+ "useCase": "the in-progress text treatment — put it on a \"working…\" / \"thinking…\" label while a task streams, instead of a spinner beside the text.",
954
+ "description": "In-progress text shimmer — a bright sweep through muted text (the standard\n\"working…\" label treatment in streaming UIs). Timing derives from the motion\naxis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the\nduration is 0s and the sweep freezes on its base frame — plain muted text —\nso reduced-motion and data-motion=\"instant\" both kill it for free.",
955
+ "css": "@utility era-shimmer {\n\tbackground: linear-gradient(\n\t\t110deg,\n\t\tvar(--color-muted) 0% 43%,\n\t\tvar(--color-bright) 50%,\n\t\tvar(--color-muted) 57% 100%\n\t);\n\tbackground-size: 200% 100%;\n\tbackground-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tanimation: era-shimmer calc(var(--era-duration) * 10) cubic-bezier(0.7, 0, 1, 0.4) infinite;\n}"
956
+ },
957
+ {
958
+ "name": "glass-blur",
959
+ "file": "surfaces/glass.css",
960
+ "useCase": "the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface=\"glass\" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface.",
961
+ "description": "Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter\nis a registered utility, gated to glass subtrees.",
962
+ "css": "@utility glass-blur {\n\t[data-surface='glass'] & {\n\t\tbackdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));\n\t\t-webkit-backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));\n\t}\n}"
963
+ }
895
964
  ]
896
965
  }
@@ -55,3 +55,11 @@ Inherits all props from `ScrollArea.CornerProps`.
55
55
  | Prop | Type | Default | Notes |
56
56
  |------|------|---------|-------|
57
57
  | `ref?` | `(forwarded)` | `null` | bindable |
58
+
59
+ ## Notes
60
+
61
+ - For a region that should scroll but show no scrollbar at all — an overflowing
62
+ tab strip, a chip rail, a small pane handle — you don't need ScrollArea: put
63
+ the `scrollbar-none` CSS utility on the overflowing element. See
64
+ [Utilities](/utilities.md). ScrollArea is for the case where the bar itself is
65
+ part of the design.
@@ -0,0 +1,53 @@
1
+ {
2
+ "utilities": [
3
+ {
4
+ "name": "era-interactive",
5
+ "file": "index.css",
6
+ "useCase": "the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.",
7
+ "description": "Shared interactive-state recipe: quiet focus, disabled affordance.\nEquivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.",
8
+ "css": "@utility era-interactive {\n\t&:focus {\n\t\toutline: none;\n\t}\n\t&:disabled {\n\t\tcursor: not-allowed;\n\t\topacity: 0.5;\n\t}\n}"
9
+ },
10
+ {
11
+ "name": "era-link",
12
+ "file": "index.css",
13
+ "useCase": "understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant=\"link\"` (which is a control, not running text).",
14
+ "description": "Inline text link. An understated, permanent underline that darkens from\nmuted to the text colour on hover — no layout shift, no appearing/vanishing\nunderline. The colour fade rides the MOTION axis (--era-duration/-ease), so\nit snaps at data-motion=\"instant\", eases at \"normal\", and sweeps longer at\n\"extra\" — matching every other era transition. Font-agnostic: it styles\ndecoration + colour only, so it reads correctly in mono, sans, or serif, and\nthe 0.2em offset scales with the type size. Keyboard focus mirrors hover so\nthe affordance is reachable without a pointer.",
15
+ "css": "@utility era-link {\n\t/* The text is bright at rest and never changes — the link reads as a link\n\t * before you touch it. Only the UNDERLINE reacts: a faint half-strength-muted\n\t * hint at rest that resolves to the full text colour on hover. */\n\tcolor: var(--color-bright);\n\ttext-decoration-line: underline;\n\ttext-decoration-color: color-mix(in oklch, var(--color-muted) 50%, transparent);\n\ttext-underline-offset: 0.2em;\n\ttransition: text-decoration-color var(--era-duration) var(--era-ease);\n\t&:hover,\n\t&:focus-visible {\n\t\ttext-decoration-color: var(--color-fg);\n\t}\n}"
16
+ },
17
+ {
18
+ "name": "scrollbar-none",
19
+ "file": "index.css",
20
+ "useCase": "hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.",
21
+ "description": "Scrollable but chromeless — hides the scrollbar while keeping wheel / touch /\ndrag scrolling (e.g. an overflowing tab strip in a small pane handle).",
22
+ "css": "@utility scrollbar-none {\n\tscrollbar-width: none;\n\t&::-webkit-scrollbar {\n\t\tdisplay: none;\n\t}\n}"
23
+ },
24
+ {
25
+ "name": "era-text-trim",
26
+ "file": "index.css",
27
+ "useCase": "vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings.",
28
+ "description": "Font-agnostic vertical ink-centering for single-line control text.\nMeasured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):\nthe offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in\na 24px row places ink on a half pixel, ~1px error) and font ascent/descent\nasymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and\nis the Firefox fallback; text-box ex/alphabetic fixes the second by\ncentering the x-height band — the visual mass of MIXED-CASE text (caps and\ndescenders cancel above/below). cap alphabetic is the spec-example recipe\nbut is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.\nWhere text-box is supported it fully determines the box, so the two\ndeclarations never fight. Worst-case residual: 0.44px.\n\nThe one hard rule concerns overflow clipping, and it is HEIGHT-conditional.\nOn an AUTO-height element that also clips — a `truncate` child, a\n<textarea>, an auto-height <input> — trim-both collapses the box to the\nx-height band (~7.5px at 14px) and the clip slices off every ascender, cap,\nand descender (text renders as a chopped middle stripe): NEVER do that.\nA FIXED-height control is the intended home, and is safe: the explicit\n`h-(--era-*)` overrides the collapse, so a single-line <input> (Input,\ncommand-input) and clipping display tokens (KV) correctly carry it. For\ntruncating text, put the trim on the fixed-height ROW and let the child\ncenter via the row's items-center.",
29
+ "css": "@utility era-text-trim {\n\tline-height: 1;\n\ttext-box: trim-both ex alphabetic;\n}"
30
+ },
31
+ {
32
+ "name": "era-text-trim-caps",
33
+ "file": "index.css",
34
+ "useCase": "the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.",
35
+ "description": "ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band\nmisjudges caps-only strings — cap/alphabetic edges are the right box.",
36
+ "css": "@utility era-text-trim-caps {\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}"
37
+ },
38
+ {
39
+ "name": "era-shimmer",
40
+ "file": "index.css",
41
+ "useCase": "the in-progress text treatment — put it on a \"working…\" / \"thinking…\" label while a task streams, instead of a spinner beside the text.",
42
+ "description": "In-progress text shimmer — a bright sweep through muted text (the standard\n\"working…\" label treatment in streaming UIs). Timing derives from the motion\naxis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the\nduration is 0s and the sweep freezes on its base frame — plain muted text —\nso reduced-motion and data-motion=\"instant\" both kill it for free.",
43
+ "css": "@utility era-shimmer {\n\tbackground: linear-gradient(\n\t\t110deg,\n\t\tvar(--color-muted) 0% 43%,\n\t\tvar(--color-bright) 50%,\n\t\tvar(--color-muted) 57% 100%\n\t);\n\tbackground-size: 200% 100%;\n\tbackground-clip: text;\n\t-webkit-text-fill-color: transparent;\n\tanimation: era-shimmer calc(var(--era-duration) * 10) cubic-bezier(0.7, 0, 1, 0.4) infinite;\n}"
44
+ },
45
+ {
46
+ "name": "glass-blur",
47
+ "file": "surfaces/glass.css",
48
+ "useCase": "the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface=\"glass\" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface.",
49
+ "description": "Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter\nis a registered utility, gated to glass subtrees.",
50
+ "css": "@utility glass-blur {\n\t[data-surface='glass'] & {\n\t\tbackdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));\n\t\t-webkit-backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));\n\t}\n}"
51
+ }
52
+ ]
53
+ }
@@ -0,0 +1,195 @@
1
+ # Utilities
2
+
3
+ The CSS classes era ships alongside the components — links, ink-centred text, shimmer, hidden scrollbars.
4
+
5
+ ## Overview
6
+
7
+ era ships a handful of Tailwind v4 `@utility` classes for the patterns that are
8
+ styling, not components — an inline link, ink-centred control text, a hidden
9
+ scrollbar. They need no import: they ride along with the stylesheet.
10
+
11
+ ```ts
12
+ import "@sig-nine/era-ui/css";
13
+ ```
14
+
15
+ ```svelte
16
+ <a class="era-link" href="/spacing">the spacing ladder</a>
17
+ ```
18
+
19
+ | Utility | Use it for |
20
+ |---|---|
21
+ | `era-interactive` | the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants. |
22
+ | `era-link` | understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text). |
23
+ | `scrollbar-none` | hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea. |
24
+ | `era-text-trim` | vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings. |
25
+ | `era-text-trim-caps` | the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text. |
26
+ | `era-shimmer` | the in-progress text treatment — put it on a "working…" / "thinking…" label while a task streams, instead of a spinner beside the text. |
27
+ | `glass-blur` | the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface="glass" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface. |
28
+
29
+ ## era-interactive
30
+
31
+ **Use it for:** the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.
32
+
33
+ Shared interactive-state recipe: quiet focus, disabled affordance.
34
+ Equivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.
35
+
36
+ Declared in `index.css`.
37
+
38
+ ```css
39
+ @utility era-interactive {
40
+ &:focus {
41
+ outline: none;
42
+ }
43
+ &:disabled {
44
+ cursor: not-allowed;
45
+ opacity: 0.5;
46
+ }
47
+ }
48
+ ```
49
+
50
+ ## era-link
51
+
52
+ **Use it for:** understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text).
53
+
54
+ Inline text link. An understated, permanent underline that darkens from
55
+ muted to the text colour on hover — no layout shift, no appearing/vanishing
56
+ underline. The colour fade rides the MOTION axis (--era-duration/-ease), so
57
+ it snaps at data-motion="instant", eases at "normal", and sweeps longer at
58
+ "extra" — matching every other era transition. Font-agnostic: it styles
59
+ decoration + colour only, so it reads correctly in mono, sans, or serif, and
60
+ the 0.2em offset scales with the type size. Keyboard focus mirrors hover so
61
+ the affordance is reachable without a pointer.
62
+
63
+ Declared in `index.css`.
64
+
65
+ ```css
66
+ @utility era-link {
67
+ /* The text is bright at rest and never changes — the link reads as a link
68
+ * before you touch it. Only the UNDERLINE reacts: a faint half-strength-muted
69
+ * hint at rest that resolves to the full text colour on hover. */
70
+ color: var(--color-bright);
71
+ text-decoration-line: underline;
72
+ text-decoration-color: color-mix(in oklch, var(--color-muted) 50%, transparent);
73
+ text-underline-offset: 0.2em;
74
+ transition: text-decoration-color var(--era-duration) var(--era-ease);
75
+ &:hover,
76
+ &:focus-visible {
77
+ text-decoration-color: var(--color-fg);
78
+ }
79
+ }
80
+ ```
81
+
82
+ ## scrollbar-none
83
+
84
+ **Use it for:** hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.
85
+
86
+ Scrollable but chromeless — hides the scrollbar while keeping wheel / touch /
87
+ drag scrolling (e.g. an overflowing tab strip in a small pane handle).
88
+
89
+ Declared in `index.css`.
90
+
91
+ ```css
92
+ @utility scrollbar-none {
93
+ scrollbar-width: none;
94
+ &::-webkit-scrollbar {
95
+ display: none;
96
+ }
97
+ }
98
+ ```
99
+
100
+ ## era-text-trim
101
+
102
+ **Use it for:** vertically center single-line control text (a label beside an icon, an input's own text) without a translateY nudge — fixed-height elements only; see era-text-trim-caps for ALL-CAPS strings.
103
+
104
+ Font-agnostic vertical ink-centering for single-line control text.
105
+ Measured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):
106
+ the offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in
107
+ a 24px row places ink on a half pixel, ~1px error) and font ascent/descent
108
+ asymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and
109
+ is the Firefox fallback; text-box ex/alphabetic fixes the second by
110
+ centering the x-height band — the visual mass of MIXED-CASE text (caps and
111
+ descenders cancel above/below). cap alphabetic is the spec-example recipe
112
+ but is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.
113
+ Where text-box is supported it fully determines the box, so the two
114
+ declarations never fight. Worst-case residual: 0.44px.
115
+
116
+ The one hard rule concerns overflow clipping, and it is HEIGHT-conditional.
117
+ On an AUTO-height element that also clips — a `truncate` child, a
118
+ <textarea>, an auto-height <input> — trim-both collapses the box to the
119
+ x-height band (~7.5px at 14px) and the clip slices off every ascender, cap,
120
+ and descender (text renders as a chopped middle stripe): NEVER do that.
121
+ A FIXED-height control is the intended home, and is safe: the explicit
122
+ `h-(--era-*)` overrides the collapse, so a single-line <input> (Input,
123
+ command-input) and clipping display tokens (KV) correctly carry it. For
124
+ truncating text, put the trim on the fixed-height ROW and let the child
125
+ center via the row's items-center.
126
+
127
+ Declared in `index.css`.
128
+
129
+ ```css
130
+ @utility era-text-trim {
131
+ line-height: 1;
132
+ text-box: trim-both ex alphabetic;
133
+ }
134
+ ```
135
+
136
+ ## era-text-trim-caps
137
+
138
+ **Use it for:** the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
139
+
140
+ ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
141
+ misjudges caps-only strings — cap/alphabetic edges are the right box.
142
+
143
+ Declared in `index.css`.
144
+
145
+ ```css
146
+ @utility era-text-trim-caps {
147
+ line-height: 1;
148
+ text-box: trim-both cap alphabetic;
149
+ }
150
+ ```
151
+
152
+ ## era-shimmer
153
+
154
+ **Use it for:** the in-progress text treatment — put it on a "working…" / "thinking…" label while a task streams, instead of a spinner beside the text.
155
+
156
+ In-progress text shimmer — a bright sweep through muted text (the standard
157
+ "working…" label treatment in streaming UIs). Timing derives from the motion
158
+ axis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the
159
+ duration is 0s and the sweep freezes on its base frame — plain muted text —
160
+ so reduced-motion and data-motion="instant" both kill it for free.
161
+
162
+ Declared in `index.css`.
163
+
164
+ ```css
165
+ @utility era-shimmer {
166
+ background: linear-gradient(
167
+ 110deg,
168
+ var(--color-muted) 0% 43%,
169
+ var(--color-bright) 50%,
170
+ var(--color-muted) 57% 100%
171
+ );
172
+ background-size: 200% 100%;
173
+ background-clip: text;
174
+ -webkit-text-fill-color: transparent;
175
+ animation: era-shimmer calc(var(--era-duration) * 10) cubic-bezier(0.7, 0, 1, 0.4) infinite;
176
+ }
177
+ ```
178
+
179
+ ## glass-blur
180
+
181
+ **Use it for:** the backdrop blur for a floating panel (popover, dialog, taskbar) — it applies only inside a data-surface="glass" subtree and is inert elsewhere, so it is safe to leave on a panel that renders under every surface.
182
+
183
+ Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter
184
+ is a registered utility, gated to glass subtrees.
185
+
186
+ Declared in `surfaces/glass.css`.
187
+
188
+ ```css
189
+ @utility glass-blur {
190
+ [data-surface='glass'] & {
191
+ backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
192
+ -webkit-backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
193
+ }
194
+ }
195
+ ```