@signal9/era-ui 3.0.1 → 3.2.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.
@@ -2,6 +2,9 @@
2
2
  "utilities": [
3
3
  {
4
4
  "name": "era-interactive",
5
+ "names": [
6
+ "era-interactive"
7
+ ],
5
8
  "file": "index.css",
6
9
  "useCase": "the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.",
7
10
  "description": "Shared interactive-state recipe: quiet focus, disabled affordance.\nEquivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.",
@@ -9,6 +12,9 @@
9
12
  },
10
13
  {
11
14
  "name": "era-link",
15
+ "names": [
16
+ "era-link"
17
+ ],
12
18
  "file": "index.css",
13
19
  "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
20
  "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.",
@@ -16,6 +22,9 @@
16
22
  },
17
23
  {
18
24
  "name": "scrollbar-none",
25
+ "names": [
26
+ "scrollbar-none"
27
+ ],
19
28
  "file": "index.css",
20
29
  "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
30
  "description": "Scrollable but chromeless — hides the scrollbar while keeping wheel / touch /\ndrag scrolling (e.g. an overflowing tab strip in a small pane handle).",
@@ -23,6 +32,9 @@
23
32
  },
24
33
  {
25
34
  "name": "era-text-trim",
35
+ "names": [
36
+ "era-text-trim"
37
+ ],
26
38
  "file": "index.css",
27
39
  "useCase": "vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings.",
28
40
  "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 hard rules concern overflow clipping, and there are TWO of them.\n\n1. NEVER on a NATIVE EDITABLE — <input>, <textarea>, <select>. These clip to\n a UA-owned inner editor box that IS the trimmed line box, so a fixed\n height does NOT hold it open: whatever you type gets its ascenders and\n descenders sliced off (a \"d\" renders as an \"o\"). This rule used to read\n the other way round — it claimed a fixed-height <input> was the trim's\n intended home — and the Input component shipped chopped text as a result.\n A tier-height input centres its own text; it needs no trim.\n\n2. NEVER on an AUTO-height element that clips — a `truncate` child, an\n auto-height box with overflow-hidden. With no height to hold it open the\n box collapses to the x-height band (~7.5px at 14px) and the clip chops the\n glyphs into a middle stripe. Put the trim on the fixed-height ROW instead\n and let the child centre via the row's items-center.\n\nA fixed-height NON-editable element is the safe home: the ink overflows the\ntrimmed LINE box but stays inside the padding box, which is what actually\nclips. Both rules are enforced at runtime by typography/collapsed-text-trim.",
@@ -30,6 +42,9 @@
30
42
  },
31
43
  {
32
44
  "name": "era-text-trim-caps",
45
+ "names": [
46
+ "era-text-trim-caps"
47
+ ],
33
48
  "file": "index.css",
34
49
  "useCase": "the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.",
35
50
  "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.",
@@ -37,13 +52,157 @@
37
52
  },
38
53
  {
39
54
  "name": "era-shimmer",
55
+ "names": [
56
+ "era-shimmer"
57
+ ],
40
58
  "file": "index.css",
41
59
  "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
60
  "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
61
  "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
62
  },
63
+ {
64
+ "name": "h-xs",
65
+ "names": [
66
+ "h-xs",
67
+ "h-xxs",
68
+ "h-sm",
69
+ "h-md",
70
+ "h-lg"
71
+ ],
72
+ "file": "index.css",
73
+ "useCase": "the tier-height ladder — h-xs (icon) · h-xxs (nested pill) · h-sm (standalone pill) · h-md (control) · h-lg (bar); the named form of h-(--era-h-*).",
74
+ "description": "",
75
+ "css": "@utility h-xs {\n\theight: var(--era-h-xs);\n}\n@utility h-xxs {\n\theight: var(--era-h-xxs);\n}\n@utility h-sm {\n\theight: var(--era-h-sm);\n}\n@utility h-md {\n\theight: var(--era-h-md);\n}\n@utility h-lg {\n\theight: var(--era-h-lg);\n}"
76
+ },
77
+ {
78
+ "name": "min-h-xs",
79
+ "names": [
80
+ "min-h-xs",
81
+ "min-h-xxs",
82
+ "min-h-sm",
83
+ "min-h-md",
84
+ "min-h-lg"
85
+ ],
86
+ "file": "index.css",
87
+ "useCase": "minimum tier height for containers that may grow — a bar that is at least lg-tier but can wrap (min-h-lg).",
88
+ "description": "",
89
+ "css": "@utility min-h-xs {\n\tmin-height: var(--era-h-xs);\n}\n@utility min-h-xxs {\n\tmin-height: var(--era-h-xxs);\n}\n@utility min-h-sm {\n\tmin-height: var(--era-h-sm);\n}\n@utility min-h-md {\n\tmin-height: var(--era-h-md);\n}\n@utility min-h-lg {\n\tmin-height: var(--era-h-lg);\n}"
90
+ },
91
+ {
92
+ "name": "size-xs",
93
+ "names": [
94
+ "size-xs",
95
+ "size-xxs",
96
+ "size-sm",
97
+ "size-md",
98
+ "size-lg"
99
+ ],
100
+ "file": "index.css",
101
+ "useCase": "a tier-sized square — size-xs is THE icon size (a Lucide glyph beside body text), size-md a square control face.",
102
+ "description": "",
103
+ "css": "@utility size-xs {\n\twidth: var(--era-h-xs);\n\theight: var(--era-h-xs);\n}\n@utility size-xxs {\n\twidth: var(--era-h-xxs);\n\theight: var(--era-h-xxs);\n}\n@utility size-sm {\n\twidth: var(--era-h-sm);\n\theight: var(--era-h-sm);\n}\n@utility size-md {\n\twidth: var(--era-h-md);\n\theight: var(--era-h-md);\n}\n@utility size-lg {\n\twidth: var(--era-h-lg);\n\theight: var(--era-h-lg);\n}"
104
+ },
105
+ {
106
+ "name": "px-sm",
107
+ "names": [
108
+ "px-sm",
109
+ "px-md",
110
+ "px-lg"
111
+ ],
112
+ "file": "index.css",
113
+ "useCase": "tier-to-tier nesting padding (sp/2) — the horizontal padding of a bar or row that holds tiered controls; the named form of px-(--era-px-*).",
114
+ "description": "",
115
+ "css": "@utility px-sm {\n\tpadding-inline: var(--era-px-sm);\n}\n@utility px-md {\n\tpadding-inline: var(--era-px-md);\n}\n@utility px-lg {\n\tpadding-inline: var(--era-px-lg);\n}"
116
+ },
117
+ {
118
+ "name": "gap-gutter",
119
+ "names": [
120
+ "gap-gutter",
121
+ "p-gutter",
122
+ "px-gutter",
123
+ "py-gutter"
124
+ ],
125
+ "file": "index.css",
126
+ "useCase": "the inter-element gap (sp/2) — flex/grid gap between sibling controls, and the matching gutter padding a scrollable list or rail owes its rows on all four sides (p-gutter on the rail = gap-gutter between its rows).",
127
+ "description": "",
128
+ "css": "@utility gap-gutter {\n\tgap: var(--era-gap);\n}\n@utility p-gutter {\n\tpadding: var(--era-gap);\n}\n@utility px-gutter {\n\tpadding-inline: var(--era-gap);\n}\n@utility py-gutter {\n\tpadding-block: var(--era-gap);\n}"
129
+ },
130
+ {
131
+ "name": "p-inset-xxs",
132
+ "names": [
133
+ "p-inset-xxs",
134
+ "p-inset-sm",
135
+ "p-inset-md",
136
+ "px-inset-xxs",
137
+ "px-inset-sm",
138
+ "px-inset-md",
139
+ "py-inset-xxs",
140
+ "py-inset-sm",
141
+ "py-inset-md"
142
+ ],
143
+ "file": "index.css",
144
+ "useCase": "text-symmetric content padding — (tier − text)/2, the inset that vertically centers body text in its tier; px-inset-md is a text button's side padding, p-inset-md a code block's.",
145
+ "description": "",
146
+ "css": "@utility p-inset-xxs {\n\tpadding: var(--era-inset-xxs);\n}\n@utility p-inset-sm {\n\tpadding: var(--era-inset-sm);\n}\n@utility p-inset-md {\n\tpadding: var(--era-inset-md);\n}\n@utility px-inset-xxs {\n\tpadding-inline: var(--era-inset-xxs);\n}\n@utility px-inset-sm {\n\tpadding-inline: var(--era-inset-sm);\n}\n@utility px-inset-md {\n\tpadding-inline: var(--era-inset-md);\n}\n@utility py-inset-xxs {\n\tpadding-block: var(--era-inset-xxs);\n}\n@utility py-inset-sm {\n\tpadding-block: var(--era-inset-sm);\n}\n@utility py-inset-md {\n\tpadding-block: var(--era-inset-md);\n}"
147
+ },
148
+ {
149
+ "name": "px-pill-xxs",
150
+ "names": [
151
+ "px-pill-xxs",
152
+ "px-pill-sm"
153
+ ],
154
+ "file": "index.css",
155
+ "useCase": "pill side padding — the wider-than-vertical horizontal breathing room badge/chip text wants (glyph-advance derived, ~2× the vertical inset).",
156
+ "description": "",
157
+ "css": "@utility px-pill-xxs {\n\tpadding-inline: var(--era-pill-xxs);\n}\n@utility px-pill-sm {\n\tpadding-inline: var(--era-pill-sm);\n}"
158
+ },
159
+ {
160
+ "name": "px-field",
161
+ "names": [
162
+ "px-field"
163
+ ],
164
+ "file": "index.css",
165
+ "useCase": "text-field side padding — where typed text starts in an md-tier input; the named form of px-(--era-field-px).",
166
+ "description": "",
167
+ "css": "@utility px-field {\n\tpadding-inline: var(--era-field-px);\n}"
168
+ },
169
+ {
170
+ "name": "p-panel",
171
+ "names": [
172
+ "p-panel",
173
+ "p-card",
174
+ "p-content"
175
+ ],
176
+ "file": "index.css",
177
+ "useCase": "optical padding for unbounded containers — p-panel for compact shells and menu-adjacent chrome, p-card for cards/dialogs/popovers, p-content for full content panels. Grows super-linearly with density so whitespace holds its perceived share as controls loosen.",
178
+ "description": "",
179
+ "css": "@utility p-panel {\n\tpadding: var(--era-pad-sm);\n}\n@utility p-card {\n\tpadding: var(--era-pad-md);\n}\n@utility p-content {\n\tpadding: var(--era-content-p);\n}"
180
+ },
181
+ {
182
+ "name": "p-menu",
183
+ "names": [
184
+ "p-menu"
185
+ ],
186
+ "file": "index.css",
187
+ "useCase": "menu-panel padding — the surface-owned inset (0 on flat, 2px on glass/bevel) that keeps row highlights inside a floating panel's frame; pairs with rounded-item on the rows.",
188
+ "description": "",
189
+ "css": "@utility p-menu {\n\tpadding: var(--era-panel-inset);\n}"
190
+ },
191
+ {
192
+ "name": "duration-base",
193
+ "names": [
194
+ "duration-base"
195
+ ],
196
+ "file": "index.css",
197
+ "useCase": "the motion axis as an explicit transition-duration — for the rare case a utility (not the transition-* default) needs to name it; 0s at data-motion=\"instant\", longer at \"extra\".",
198
+ "description": "",
199
+ "css": "@utility duration-base {\n\ttransition-duration: var(--era-duration);\n}"
200
+ },
45
201
  {
46
202
  "name": "glass-blur",
203
+ "names": [
204
+ "glass-blur"
205
+ ],
47
206
  "file": "surfaces/glass.css",
48
207
  "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
208
  "description": "Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter\nis a registered utility, gated to glass subtrees.",
@@ -4,18 +4,38 @@ The CSS classes era ships alongside the components — links, ink-centred text,
4
4
 
5
5
  ## Overview
6
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.
7
+ era's styling API is pure class names write `h-md rounded-md px-md shadow`,
8
+ never the `h-(--era-h-md)` var-class spelling. Everything below rides along
9
+ with the stylesheet and needs no import:
10
10
 
11
11
  ```ts
12
12
  import "@sig-nine/era-ui/css";
13
13
  ```
14
14
 
15
15
  ```svelte
16
- <a class="era-link" href="/spacing">the spacing ladder</a>
16
+ <button class="h-md rounded-md px-inset-md shadow text-body">era styled</button>
17
17
  ```
18
18
 
19
+ Alongside the `@utility` classes referenced below, era registers named theme
20
+ scales that OVERLOAD the stock Tailwind names, so ordinary-looking markup
21
+ renders on-design and responds to every axis (density, surface, corners,
22
+ motion):
23
+
24
+ | Stock-looking class | What it resolves to |
25
+ |---|---|
26
+ | `rounded-xs/xxs/sm/md/lg` | the concentric radius ladder (collapses on `data-corners="square"`) |
27
+ | `rounded-item` | menu-row radius, concentric inside a `rounded-md` panel |
28
+ | `shadow` / `shadow-lg` | resting control edge / floating panel chrome, per surface |
29
+ | `shadow-well` / `shadow-pressed` / `shadow-highlight` | recessed, pressed/latched, hover-material chrome |
30
+ | `bg-well` / `bg-elevated` / `bg-highlight` / `bg-overlay` | surface fills: resting, raised, transient feedback, modal scrim |
31
+ | `transition-*` (bare) | duration + easing default to the motion axis |
32
+ | `ease-base` / `duration-base` | the motion axis, named explicitly |
33
+ | `text-body` | density-derived body/control text size |
34
+
35
+ Deliberate gaps: `w-*`/`min-w-*` tier names would shadow Tailwind's container
36
+ scale (`min-w-md` stays 28rem), and the concentric-inset tokens are advanced
37
+ enough to stay var-form — `min-w-(--era-h-md)` etc. remain the escape hatch.
38
+
19
39
  | Utility | Use it for |
20
40
  |---|---|
21
41
  | `era-interactive` | the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants. |
@@ -24,6 +44,17 @@ import "@sig-nine/era-ui/css";
24
44
  | `era-text-trim` | vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings. |
25
45
  | `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
46
  | `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. |
47
+ | `h-xs` `h-xxs` `h-sm` `h-md` `h-lg` | the tier-height ladder — h-xs (icon) · h-xxs (nested pill) · h-sm (standalone pill) · h-md (control) · h-lg (bar); the named form of h-(--era-h-*). |
48
+ | `min-h-xs` `min-h-xxs` `min-h-sm` `min-h-md` `min-h-lg` | minimum tier height for containers that may grow — a bar that is at least lg-tier but can wrap (min-h-lg). |
49
+ | `size-xs` `size-xxs` `size-sm` `size-md` `size-lg` | a tier-sized square — size-xs is THE icon size (a Lucide glyph beside body text), size-md a square control face. |
50
+ | `px-sm` `px-md` `px-lg` | tier-to-tier nesting padding (sp/2) — the horizontal padding of a bar or row that holds tiered controls; the named form of px-(--era-px-*). |
51
+ | `gap-gutter` `p-gutter` `px-gutter` `py-gutter` | the inter-element gap (sp/2) — flex/grid gap between sibling controls, and the matching gutter padding a scrollable list or rail owes its rows on all four sides (p-gutter on the rail = gap-gutter between its rows). |
52
+ | `p-inset-xxs` `p-inset-sm` `p-inset-md` `px-inset-xxs` `px-inset-sm` `px-inset-md` `py-inset-xxs` `py-inset-sm` `py-inset-md` | text-symmetric content padding — (tier − text)/2, the inset that vertically centers body text in its tier; px-inset-md is a text button's side padding, p-inset-md a code block's. |
53
+ | `px-pill-xxs` `px-pill-sm` | pill side padding — the wider-than-vertical horizontal breathing room badge/chip text wants (glyph-advance derived, ~2× the vertical inset). |
54
+ | `px-field` | text-field side padding — where typed text starts in an md-tier input; the named form of px-(--era-field-px). |
55
+ | `p-panel` `p-card` `p-content` | optical padding for unbounded containers — p-panel for compact shells and menu-adjacent chrome, p-card for cards/dialogs/popovers, p-content for full content panels. Grows super-linearly with density so whitespace holds its perceived share as controls loosen. |
56
+ | `p-menu` | menu-panel padding — the surface-owned inset (0 on flat, 2px on glass/bevel) that keeps row highlights inside a floating panel's frame; pairs with rounded-item on the rows. |
57
+ | `duration-base` | the motion axis as an explicit transition-duration — for the rare case a utility (not the transition-* default) needs to name it; 0s at data-motion="instant", longer at "extra". |
27
58
  | `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
59
 
29
60
  ## era-interactive
@@ -186,6 +217,227 @@ Declared in `index.css`.
186
217
  }
187
218
  ```
188
219
 
220
+ ## h-xs · h-xxs · h-sm · h-md · h-lg
221
+
222
+ **Use it for:** the tier-height ladder — h-xs (icon) · h-xxs (nested pill) · h-sm (standalone pill) · h-md (control) · h-lg (bar); the named form of h-(--era-h-*).
223
+
224
+ Declared in `index.css`.
225
+
226
+ ```css
227
+ @utility h-xs {
228
+ height: var(--era-h-xs);
229
+ }
230
+ @utility h-xxs {
231
+ height: var(--era-h-xxs);
232
+ }
233
+ @utility h-sm {
234
+ height: var(--era-h-sm);
235
+ }
236
+ @utility h-md {
237
+ height: var(--era-h-md);
238
+ }
239
+ @utility h-lg {
240
+ height: var(--era-h-lg);
241
+ }
242
+ ```
243
+
244
+ ## min-h-xs · min-h-xxs · min-h-sm · min-h-md · min-h-lg
245
+
246
+ **Use it for:** minimum tier height for containers that may grow — a bar that is at least lg-tier but can wrap (min-h-lg).
247
+
248
+ Declared in `index.css`.
249
+
250
+ ```css
251
+ @utility min-h-xs {
252
+ min-height: var(--era-h-xs);
253
+ }
254
+ @utility min-h-xxs {
255
+ min-height: var(--era-h-xxs);
256
+ }
257
+ @utility min-h-sm {
258
+ min-height: var(--era-h-sm);
259
+ }
260
+ @utility min-h-md {
261
+ min-height: var(--era-h-md);
262
+ }
263
+ @utility min-h-lg {
264
+ min-height: var(--era-h-lg);
265
+ }
266
+ ```
267
+
268
+ ## size-xs · size-xxs · size-sm · size-md · size-lg
269
+
270
+ **Use it for:** a tier-sized square — size-xs is THE icon size (a Lucide glyph beside body text), size-md a square control face.
271
+
272
+ Declared in `index.css`.
273
+
274
+ ```css
275
+ @utility size-xs {
276
+ width: var(--era-h-xs);
277
+ height: var(--era-h-xs);
278
+ }
279
+ @utility size-xxs {
280
+ width: var(--era-h-xxs);
281
+ height: var(--era-h-xxs);
282
+ }
283
+ @utility size-sm {
284
+ width: var(--era-h-sm);
285
+ height: var(--era-h-sm);
286
+ }
287
+ @utility size-md {
288
+ width: var(--era-h-md);
289
+ height: var(--era-h-md);
290
+ }
291
+ @utility size-lg {
292
+ width: var(--era-h-lg);
293
+ height: var(--era-h-lg);
294
+ }
295
+ ```
296
+
297
+ ## px-sm · px-md · px-lg
298
+
299
+ **Use it for:** tier-to-tier nesting padding (sp/2) — the horizontal padding of a bar or row that holds tiered controls; the named form of px-(--era-px-*).
300
+
301
+ Declared in `index.css`.
302
+
303
+ ```css
304
+ @utility px-sm {
305
+ padding-inline: var(--era-px-sm);
306
+ }
307
+ @utility px-md {
308
+ padding-inline: var(--era-px-md);
309
+ }
310
+ @utility px-lg {
311
+ padding-inline: var(--era-px-lg);
312
+ }
313
+ ```
314
+
315
+ ## gap-gutter · p-gutter · px-gutter · py-gutter
316
+
317
+ **Use it for:** the inter-element gap (sp/2) — flex/grid gap between sibling controls, and the matching gutter padding a scrollable list or rail owes its rows on all four sides (p-gutter on the rail = gap-gutter between its rows).
318
+
319
+ Declared in `index.css`.
320
+
321
+ ```css
322
+ @utility gap-gutter {
323
+ gap: var(--era-gap);
324
+ }
325
+ @utility p-gutter {
326
+ padding: var(--era-gap);
327
+ }
328
+ @utility px-gutter {
329
+ padding-inline: var(--era-gap);
330
+ }
331
+ @utility py-gutter {
332
+ padding-block: var(--era-gap);
333
+ }
334
+ ```
335
+
336
+ ## p-inset-xxs · p-inset-sm · p-inset-md · px-inset-xxs · px-inset-sm · px-inset-md · py-inset-xxs · py-inset-sm · py-inset-md
337
+
338
+ **Use it for:** text-symmetric content padding — (tier − text)/2, the inset that vertically centers body text in its tier; px-inset-md is a text button's side padding, p-inset-md a code block's.
339
+
340
+ Declared in `index.css`.
341
+
342
+ ```css
343
+ @utility p-inset-xxs {
344
+ padding: var(--era-inset-xxs);
345
+ }
346
+ @utility p-inset-sm {
347
+ padding: var(--era-inset-sm);
348
+ }
349
+ @utility p-inset-md {
350
+ padding: var(--era-inset-md);
351
+ }
352
+ @utility px-inset-xxs {
353
+ padding-inline: var(--era-inset-xxs);
354
+ }
355
+ @utility px-inset-sm {
356
+ padding-inline: var(--era-inset-sm);
357
+ }
358
+ @utility px-inset-md {
359
+ padding-inline: var(--era-inset-md);
360
+ }
361
+ @utility py-inset-xxs {
362
+ padding-block: var(--era-inset-xxs);
363
+ }
364
+ @utility py-inset-sm {
365
+ padding-block: var(--era-inset-sm);
366
+ }
367
+ @utility py-inset-md {
368
+ padding-block: var(--era-inset-md);
369
+ }
370
+ ```
371
+
372
+ ## px-pill-xxs · px-pill-sm
373
+
374
+ **Use it for:** pill side padding — the wider-than-vertical horizontal breathing room badge/chip text wants (glyph-advance derived, ~2× the vertical inset).
375
+
376
+ Declared in `index.css`.
377
+
378
+ ```css
379
+ @utility px-pill-xxs {
380
+ padding-inline: var(--era-pill-xxs);
381
+ }
382
+ @utility px-pill-sm {
383
+ padding-inline: var(--era-pill-sm);
384
+ }
385
+ ```
386
+
387
+ ## px-field
388
+
389
+ **Use it for:** text-field side padding — where typed text starts in an md-tier input; the named form of px-(--era-field-px).
390
+
391
+ Declared in `index.css`.
392
+
393
+ ```css
394
+ @utility px-field {
395
+ padding-inline: var(--era-field-px);
396
+ }
397
+ ```
398
+
399
+ ## p-panel · p-card · p-content
400
+
401
+ **Use it for:** optical padding for unbounded containers — p-panel for compact shells and menu-adjacent chrome, p-card for cards/dialogs/popovers, p-content for full content panels. Grows super-linearly with density so whitespace holds its perceived share as controls loosen.
402
+
403
+ Declared in `index.css`.
404
+
405
+ ```css
406
+ @utility p-panel {
407
+ padding: var(--era-pad-sm);
408
+ }
409
+ @utility p-card {
410
+ padding: var(--era-pad-md);
411
+ }
412
+ @utility p-content {
413
+ padding: var(--era-content-p);
414
+ }
415
+ ```
416
+
417
+ ## p-menu
418
+
419
+ **Use it for:** menu-panel padding — the surface-owned inset (0 on flat, 2px on glass/bevel) that keeps row highlights inside a floating panel's frame; pairs with rounded-item on the rows.
420
+
421
+ Declared in `index.css`.
422
+
423
+ ```css
424
+ @utility p-menu {
425
+ padding: var(--era-panel-inset);
426
+ }
427
+ ```
428
+
429
+ ## duration-base
430
+
431
+ **Use it for:** the motion axis as an explicit transition-duration — for the rare case a utility (not the transition-* default) needs to name it; 0s at data-motion="instant", longer at "extra".
432
+
433
+ Declared in `index.css`.
434
+
435
+ ```css
436
+ @utility duration-base {
437
+ transition-duration: var(--era-duration);
438
+ }
439
+ ```
440
+
189
441
  ## glass-blur
190
442
 
191
443
  **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.
@@ -18,6 +18,7 @@
18
18
  floating = false,
19
19
  pinned = [],
20
20
  showWindows = true,
21
+ showWorkspaces = true,
21
22
  commands = [],
22
23
  hotkey = ['`', 'Escape'],
23
24
  placeholder = 'run a command…',
@@ -42,6 +43,11 @@
42
43
  /** Show the open/minimized window tabs (default true). Hidden, windows
43
44
  * stay reachable through the command bar's `windows` view. */
44
45
  showWindows?: boolean;
46
+ /** Show the multi-workspace UI (default true). Hidden, both the numbered
47
+ * switcher and the command bar's `workspace` view go away — a
48
+ * single-workspace shell. `wm.addWorkspace()` still works for consumers
49
+ * who drive workspaces themselves. */
50
+ showWorkspaces?: boolean;
45
51
  /** Extra root commands appended to the built-in ones. */
46
52
  commands?: CommandBarItem[];
47
53
  /** Focus binding(s) for the command bar (default backtick + Escape). */
@@ -121,6 +127,7 @@
121
127
  onSelect: () => wm.focus(w.id)
122
128
  }))
123
129
  };
130
+ if (!showWorkspaces) return [...apps, windows];
124
131
  const workspaces: CommandBarItem = {
125
132
  value: 'workspace',
126
133
  label: 'workspace',
@@ -228,8 +235,9 @@
228
235
  {/if}
229
236
 
230
237
  <!-- Numbered workspace switcher, term-style on the right — appears only
231
- once there's something to switch between. -->
232
- {#if wm.workspaces.length > 1}
238
+ once there's something to switch between (and only when the bar exposes
239
+ workspaces at all). -->
240
+ {#if showWorkspaces && wm.workspaces.length > 1}
233
241
  <div class="flex shrink-0 items-center gap-(--era-gap)">
234
242
  {#each wm.workspaces as ws, i (ws.id)}
235
243
  <Button
@@ -20,6 +20,11 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
20
20
  /** Show the open/minimized window tabs (default true). Hidden, windows
21
21
  * stay reachable through the command bar's `windows` view. */
22
22
  showWindows?: boolean;
23
+ /** Show the multi-workspace UI (default true). Hidden, both the numbered
24
+ * switcher and the command bar's `workspace` view go away — a
25
+ * single-workspace shell. `wm.addWorkspace()` still works for consumers
26
+ * who drive workspaces themselves. */
27
+ showWorkspaces?: boolean;
23
28
  /** Extra root commands appended to the built-in ones. */
24
29
  commands?: CommandBarItem[];
25
30
  /** Focus binding(s) for the command bar (default backtick + Escape). */