@signal9/era-ui 5.0.0 → 7.0.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.
@@ -4,13 +4,32 @@
4
4
 
5
5
  ## Using the library
6
6
 
7
+ **One entry point per component.** There is no barrel that re-exports everything — importing a component can never drag in another one, or another one's dependencies.
8
+
7
9
  ```ts
8
- import { Button, Table, Select } from "@sig-nine/era-ui";
9
- import "@sig-nine/era-ui/css";
10
+ import { Button } from "@sig-nine/era-ui/button";
11
+ import * as Select from "@sig-nine/era-ui/select";
12
+ import * as Table from "@sig-nine/era-ui/table";
13
+
14
+ // The root is the SYSTEM layer only: cn/tv, keybindings, the axis
15
+ // registry, and Mode. It imports no component.
16
+ import { cn, ModeProvider } from "@sig-nine/era-ui";
17
+
18
+ import "@sig-nine/era-ui/styles"; // source CSS, scanned by your Tailwind
10
19
  ```
11
20
 
21
+ The path is always the component's own kebab-case name — `/button`, `/dropdown-menu`, `/date-range-picker` — and the other layers are namespaced: `/ai` and `/ai/<part>`, `/apps/<app>`, `/os`, `/audit`.
22
+
12
23
  Named exports wrap a single primitive (`Button`, `Badge`, `Chip`, `Input`, …). Namespace exports compose primitives via sub-components (`Table.Root`, `Table.Row`, `Select.Trigger`, `Dialog.Content`, …). Every wrapper forwards unknown props to its underlying element or bits-ui primitive, so standard HTML attributes (`class`, `style`, event handlers, `aria-*`, `data-*`) work without ceremony.
13
24
 
25
+ ### Stylesheets
26
+
27
+ `@sig-nine/era-ui/styles` is the source CSS — import it once and your own Tailwind build scans era's components and emits only the utilities they use. `@sig-nine/era-ui/css` is the prebuilt alternative for a project without Tailwind. Two stylesheets are opt-in because they are heavy and single-purpose: `/styles/video-player` (vidstack's vendor CSS, needed only with `VideoPlayer`) and `/styles/themes` (the raw theme tokens).
28
+
29
+ ### Optional peers
30
+
31
+ era depends on five packages. Anything needed by ONE component is an optional peer you install only if you import that component: `vidstack` (VideoPlayer), `@neodrag/svelte` (Pane, and the `/os` shell), `marked` + `dompurify` (`/ai/response`), and the tiptap set (`/apps/notes`).
32
+
14
33
  ## Scoped overrides
15
34
 
16
35
  Five orthogonal axes are exposed as HTML attributes on any ancestor. The entire subtree re-tiers through CSS inheritance — no JS, no context provider.
@@ -39,7 +58,7 @@ Classes, not components — they need no import beyond the stylesheet. Reach for
39
58
  - `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: never on an <input>/<textarea>, and never on the same element as `truncate` or any overflow clip — put it on the fixed-height ROW and let the truncating child centre inside it. See era-text-trim-caps for ALL-CAPS.
40
59
  - `truncate` — single-line horizontal truncation with an ellipsis — put it on the truncating CHILD inside a trimmed or leading-none row, never on the same element as era-text-trim; it will not clip ascenders or descenders.
41
60
  - `era-text-trim-caps` — the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never for mixed-case text (a label like "System" reads high; use era-text-trim), and never on the same element as `truncate`.
42
- - `era-ink-center` — on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim.
61
+ - `era-token-ink · era-ink-center` — the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies).
43
62
  - `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.
44
63
  - `era-collapse` — the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden).
45
64
  - `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-*).
@@ -148,7 +167,7 @@ enough to stay var-form — `min-w-(--era-h-md)` etc. remain the escape hatch.
148
167
  | `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: never on an <input>/<textarea>, and never on the same element as `truncate` or any overflow clip — put it on the fixed-height ROW and let the truncating child centre inside it. See era-text-trim-caps for ALL-CAPS. |
149
168
  | `truncate` | single-line horizontal truncation with an ellipsis — put it on the truncating CHILD inside a trimmed or leading-none row, never on the same element as era-text-trim; it will not clip ascenders or descenders. |
150
169
  | `era-text-trim-caps` | the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never for mixed-case text (a label like "System" reads high; use era-text-trim), and never on the same element as `truncate`. |
151
- | `era-ink-center` | on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim. |
170
+ | `era-token-ink` `era-ink-center` | the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies). |
152
171
  | `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. |
153
172
  | `era-collapse` | the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden). |
154
173
  | `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-*). |
@@ -340,39 +359,48 @@ Declared in `index.css`.
340
359
  }
341
360
  ```
342
361
 
343
- ## era-ink-center
362
+ ## era-token-ink · era-ink-center
344
363
 
345
- **Use it for:** on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim.
364
+ **Use it for:** the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies).
346
365
 
347
- The EDITABLE counterpart of era-text-trim for native <input>s, which the
348
- trim is forbidden on (it collapses the UA's inner editor box and slices the
349
- glyphs; see typography/collapsed-text-trim).
366
+ TOKEN INK the ink centring for a pill whose content is a TOKEN: an id, a
367
+ count, a status, a tag. Font-independent, unlike everything it replaces.
350
368
 
351
- Without help, a tier-height input centres its text by FONT BOX, and the ui
352
- stacks' font box is deeply asymmetric: ascent ≈1.07em against descent
353
- ≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em
354
- LOW so a field's text sat visibly below the trimmed label beside it.
369
+ THE BUG IT FIXES, which had been shipping library-wide and invisibly:
370
+ `text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all
371
+ flex, and a flex container's bare text becomes an ANONYMOUS flex item — a box
372
+ no selector can reach and no non-inherited property can touch. So
373
+ `era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one
374
+ thing: `line-height: 1`. The ink was then centred by `align-items: center`
375
+ on the LINE BOX, which is precisely the font-metric-dependent centring the
376
+ trim exists to eliminate. Measured on the shipped mono stack: a badge reading
377
+ "AC-2.1" sat 2px high in an 18px pill, and no amount of tuning the trim's
378
+ BAND changed it, because the trim was never running.
355
379
 
356
- The value is OPTICAL, not the derivation's: pure x-band centring computes
357
- to ~0.23em of padding, but pixel-measured against real Title-Case text it
358
- over-lifts the eye weighs the cap-to-baseline mass, which x-band
359
- centring parks ~2px high (caps 2.75px off the top against 5.7px of air
360
- under the baseline, measured at dense). 0.08em splits the difference
361
- between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
362
- 18px field with ~4.7px under the baseline — the reading where neither
363
- band's partisans win and neither complains.
380
+ The mechanism here restores it: a BLOCK box (where text-box-trim does apply)
381
+ whose trimmed line box is then centred by `align-content` block-container
382
+ alignment, so no flex formatting context and no anonymous item. The browser
383
+ computes the band from the font's own metrics, which is what makes it hold
384
+ across ui-monospace, system-ui and ui-serif instead of needing a constant
385
+ per stack.
364
386
 
365
- The lift is padding, not a translateY: an icon-less Input paints the field
366
- chrome itself, and a transform would drag the field along with the ink.
367
- padding-bottom shrinks the content box from below, and a single-line input
368
- re-centres its line box in what remains the ink rises by exactly half the
369
- padding, the chrome doesn't move, and the caret and selection follow the
370
- text. In em on purpose: the correction is a fraction of the type, so it
371
- holds across every density and text size with one value.
387
+ CAP band, not the x-height band the general trim uses, because the content
388
+ class is different: a token is caps and digits (both cap-height), and
389
+ centring the x-band parks them high by (cap x)/2. Measured on the shipped
390
+ stacks, cap-band centring takes an id/count/status from −2.0px to 0.0.
391
+ Prose and sentence-case labels keep era-text-trim; this is for tokens.
372
392
 
373
393
  Declared in `index.css`.
374
394
 
375
395
  ```css
396
+ @utility era-token-ink {
397
+ display: block;
398
+ align-content: center;
399
+ text-align: center;
400
+ line-height: 1;
401
+ text-box: trim-both cap alphabetic;
402
+ }
403
+
376
404
  @utility era-ink-center {
377
405
  padding-bottom: 0.08em;
378
406
  }
@@ -38,13 +38,32 @@ Full derivations: `{{ORIGIN}}/spacing.md` (the ladder and every token), `{{ORIGI
38
38
 
39
39
  ## Using the library
40
40
 
41
+ **One entry point per component.** There is no barrel that re-exports everything — importing a component can never drag in another one, or another one's dependencies.
42
+
41
43
  ```ts
42
- import { Button, Table, Select } from "@sig-nine/era-ui";
43
- import "@sig-nine/era-ui/css";
44
+ import { Button } from "@sig-nine/era-ui/button";
45
+ import * as Select from "@sig-nine/era-ui/select";
46
+ import * as Table from "@sig-nine/era-ui/table";
47
+
48
+ // The root is the SYSTEM layer only: cn/tv, keybindings, the axis
49
+ // registry, and Mode. It imports no component.
50
+ import { cn, ModeProvider } from "@sig-nine/era-ui";
51
+
52
+ import "@sig-nine/era-ui/styles"; // source CSS, scanned by your Tailwind
44
53
  ```
45
54
 
55
+ The path is always the component's own kebab-case name — `/button`, `/dropdown-menu`, `/date-range-picker` — and the other layers are namespaced: `/ai` and `/ai/<part>`, `/apps/<app>`, `/os`, `/audit`.
56
+
46
57
  Named exports wrap a single primitive (`Button`, `Badge`, `Chip`, `Input`, …). Namespace exports compose primitives via sub-components (`Table.Root`, `Table.Row`, `Select.Trigger`, `Dialog.Content`, …). Every wrapper forwards unknown props to its underlying element or bits-ui primitive, so standard HTML attributes (`class`, `style`, event handlers, `aria-*`, `data-*`) work without ceremony.
47
58
 
59
+ ### Stylesheets
60
+
61
+ `@sig-nine/era-ui/styles` is the source CSS — import it once and your own Tailwind build scans era's components and emits only the utilities they use. `@sig-nine/era-ui/css` is the prebuilt alternative for a project without Tailwind. Two stylesheets are opt-in because they are heavy and single-purpose: `/styles/video-player` (vidstack's vendor CSS, needed only with `VideoPlayer`) and `/styles/themes` (the raw theme tokens).
62
+
63
+ ### Optional peers
64
+
65
+ era depends on five packages. Anything needed by ONE component is an optional peer you install only if you import that component: `vidstack` (VideoPlayer), `@neodrag/svelte` (Pane, and the `/os` shell), `marked` + `dompurify` (`/ai/response`), and the tiptap set (`/apps/notes`).
66
+
48
67
  ## Scoped overrides
49
68
 
50
69
  Five orthogonal axes are exposed as HTML attributes on any ancestor. The entire subtree re-tiers through CSS inheritance — no JS, no context provider.
@@ -77,7 +96,7 @@ Classes, not components — they need no import beyond the stylesheet. Reach for
77
96
  - `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: never on an <input>/<textarea>, and never on the same element as `truncate` or any overflow clip — put it on the fixed-height ROW and let the truncating child centre inside it. See era-text-trim-caps for ALL-CAPS.
78
97
  - `truncate` — single-line horizontal truncation with an ellipsis — put it on the truncating CHILD inside a trimmed or leading-none row, never on the same element as era-text-trim; it will not clip ascenders or descenders.
79
98
  - `era-text-trim-caps` — the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never for mixed-case text (a label like "System" reads high; use era-text-trim), and never on the same element as `truncate`.
80
- - `era-ink-center` — on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim.
99
+ - `era-token-ink · era-ink-center` — the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies).
81
100
  - `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.
82
101
  - `era-collapse` — the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden).
83
102
  - `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-*).
@@ -44,7 +44,7 @@
44
44
  "slug": "utilities",
45
45
  "title": "Utilities",
46
46
  "summary": "The CSS classes era ships alongside the components — links, ink-centred text, shimmer, hidden scrollbars.",
47
- "tokenEstimate": 6097,
47
+ "tokenEstimate": 6197,
48
48
  "sections": [
49
49
  "Overview",
50
50
  "era-interactive",
@@ -53,7 +53,7 @@
53
53
  "era-text-trim",
54
54
  "truncate",
55
55
  "era-text-trim-caps",
56
- "era-ink-center",
56
+ "era-token-ink · era-ink-center",
57
57
  "era-shimmer",
58
58
  "era-collapse",
59
59
  "h-xs · h-xxs · h-sm · h-md · h-lg",
@@ -1023,14 +1023,15 @@
1023
1023
  "css": "@utility era-text-trim-caps {\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}"
1024
1024
  },
1025
1025
  {
1026
- "name": "era-ink-center",
1026
+ "name": "era-token-ink",
1027
1027
  "names": [
1028
+ "era-token-ink",
1028
1029
  "era-ink-center"
1029
1030
  ],
1030
1031
  "file": "index.css",
1031
- "useCase": "on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim.",
1032
- "description": "The EDITABLE counterpart of era-text-trim for native <input>s, which the\ntrim is forbidden on (it collapses the UA's inner editor box and slices the\nglyphs; see typography/collapsed-text-trim).\n\nWithout help, a tier-height input centres its text by FONT BOX, and the ui\nstacks' font box is deeply asymmetric: ascent ≈1.07em against descent\n≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em\nLOW so a field's text sat visibly below the trimmed label beside it.\n\nThe value is OPTICAL, not the derivation's: pure x-band centring computes\nto ~0.23em of padding, but pixel-measured against real Title-Case text it\nover-lifts the eye weighs the cap-to-baseline mass, which x-band\ncentring parks ~2px high (caps 2.75px off the top against 5.7px of air\nunder the baseline, measured at dense). 0.08em splits the difference\nbetween the x-band ideal and the cap-band ideal, landing caps ~3.7px in an\n18px field with ~4.7px under the baselinethe reading where neither\nband's partisans win and neither complains.\n\nThe lift is padding, not a translateY: an icon-less Input paints the field\nchrome itself, and a transform would drag the field along with the ink.\npadding-bottom shrinks the content box from below, and a single-line input\nre-centres its line box in what remains the ink rises by exactly half the\npadding, the chrome doesn't move, and the caret and selection follow the\ntext. In em on purpose: the correction is a fraction of the type, so it\nholds across every density and text size with one value.",
1033
- "css": "@utility era-ink-center {\n\tpadding-bottom: 0.08em;\n}"
1032
+ "useCase": "the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies).",
1033
+ "description": "TOKEN INK the ink centring for a pill whose content is a TOKEN: an id, a\ncount, a status, a tag. Font-independent, unlike everything it replaces.\n\nTHE BUG IT FIXES, which had been shipping library-wide and invisibly:\n`text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all\nflex, and a flex container's bare text becomes an ANONYMOUS flex item a box\nno selector can reach and no non-inherited property can touch. So\n`era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one\nthing: `line-height: 1`. The ink was then centred by `align-items: center`\non the LINE BOX, which is precisely the font-metric-dependent centring the\ntrim exists to eliminate. Measured on the shipped mono stack: a badge reading\n\"AC-2.1\" sat 2px high in an 18px pill, and no amount of tuning the trim's\nBAND changed it, because the trim was never running.\n\nThe mechanism here restores it: a BLOCK box (where text-box-trim does apply)\nwhose trimmed line box is then centred by `align-content` block-container\nalignment, so no flex formatting context and no anonymous item. The browser\ncomputes the band from the font's own metrics, which is what makes it hold\nacross ui-monospace, system-ui and ui-serif instead of needing a constant\nper stack.\n\nCAP band, not the x-height band the general trim uses, because the content\nclass is different: a token is caps and digits (both cap-height), and\ncentring the x-band parks them high by (cap x)/2. Measured on the shipped\nstacks, cap-band centring takes an id/count/status from −2.0px to 0.0.\nProse and sentence-case labels keep era-text-trim; this is for tokens.",
1034
+ "css": "@utility era-token-ink {\n\tdisplay: block;\n\talign-content: center;\n\ttext-align: center;\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}\n\n@utility era-ink-center {\n\tpadding-bottom: 0.08em;\n}"
1034
1035
  },
1035
1036
  {
1036
1037
  "name": "era-shimmer",
@@ -61,14 +61,15 @@
61
61
  "css": "@utility era-text-trim-caps {\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}"
62
62
  },
63
63
  {
64
- "name": "era-ink-center",
64
+ "name": "era-token-ink",
65
65
  "names": [
66
+ "era-token-ink",
66
67
  "era-ink-center"
67
68
  ],
68
69
  "file": "index.css",
69
- "useCase": "on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim.",
70
- "description": "The EDITABLE counterpart of era-text-trim for native <input>s, which the\ntrim is forbidden on (it collapses the UA's inner editor box and slices the\nglyphs; see typography/collapsed-text-trim).\n\nWithout help, a tier-height input centres its text by FONT BOX, and the ui\nstacks' font box is deeply asymmetric: ascent ≈1.07em against descent\n≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em\nLOW so a field's text sat visibly below the trimmed label beside it.\n\nThe value is OPTICAL, not the derivation's: pure x-band centring computes\nto ~0.23em of padding, but pixel-measured against real Title-Case text it\nover-lifts the eye weighs the cap-to-baseline mass, which x-band\ncentring parks ~2px high (caps 2.75px off the top against 5.7px of air\nunder the baseline, measured at dense). 0.08em splits the difference\nbetween the x-band ideal and the cap-band ideal, landing caps ~3.7px in an\n18px field with ~4.7px under the baselinethe reading where neither\nband's partisans win and neither complains.\n\nThe lift is padding, not a translateY: an icon-less Input paints the field\nchrome itself, and a transform would drag the field along with the ink.\npadding-bottom shrinks the content box from below, and a single-line input\nre-centres its line box in what remains the ink rises by exactly half the\npadding, the chrome doesn't move, and the caret and selection follow the\ntext. In em on purpose: the correction is a fraction of the type, so it\nholds across every density and text size with one value.",
71
- "css": "@utility era-ink-center {\n\tpadding-bottom: 0.08em;\n}"
70
+ "useCase": "the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies).",
71
+ "description": "TOKEN INK the ink centring for a pill whose content is a TOKEN: an id, a\ncount, a status, a tag. Font-independent, unlike everything it replaces.\n\nTHE BUG IT FIXES, which had been shipping library-wide and invisibly:\n`text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all\nflex, and a flex container's bare text becomes an ANONYMOUS flex item a box\nno selector can reach and no non-inherited property can touch. So\n`era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one\nthing: `line-height: 1`. The ink was then centred by `align-items: center`\non the LINE BOX, which is precisely the font-metric-dependent centring the\ntrim exists to eliminate. Measured on the shipped mono stack: a badge reading\n\"AC-2.1\" sat 2px high in an 18px pill, and no amount of tuning the trim's\nBAND changed it, because the trim was never running.\n\nThe mechanism here restores it: a BLOCK box (where text-box-trim does apply)\nwhose trimmed line box is then centred by `align-content` block-container\nalignment, so no flex formatting context and no anonymous item. The browser\ncomputes the band from the font's own metrics, which is what makes it hold\nacross ui-monospace, system-ui and ui-serif instead of needing a constant\nper stack.\n\nCAP band, not the x-height band the general trim uses, because the content\nclass is different: a token is caps and digits (both cap-height), and\ncentring the x-band parks them high by (cap x)/2. Measured on the shipped\nstacks, cap-band centring takes an id/count/status from −2.0px to 0.0.\nProse and sentence-case labels keep era-text-trim; this is for tokens.",
72
+ "css": "@utility era-token-ink {\n\tdisplay: block;\n\talign-content: center;\n\ttext-align: center;\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}\n\n@utility era-ink-center {\n\tpadding-bottom: 0.08em;\n}"
72
73
  },
73
74
  {
74
75
  "name": "era-shimmer",
@@ -44,7 +44,7 @@ enough to stay var-form — `min-w-(--era-h-md)` etc. remain the escape hatch.
44
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: never on an <input>/<textarea>, and never on the same element as `truncate` or any overflow clip — put it on the fixed-height ROW and let the truncating child centre inside it. See era-text-trim-caps for ALL-CAPS. |
45
45
  | `truncate` | single-line horizontal truncation with an ellipsis — put it on the truncating CHILD inside a trimmed or leading-none row, never on the same element as era-text-trim; it will not clip ascenders or descenders. |
46
46
  | `era-text-trim-caps` | the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never for mixed-case text (a label like "System" reads high; use era-text-trim), and never on the same element as `truncate`. |
47
- | `era-ink-center` | on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim. |
47
+ | `era-token-ink` `era-ink-center` | the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies). |
48
48
  | `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. |
49
49
  | `era-collapse` | the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden). |
50
50
  | `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-*). |
@@ -236,39 +236,48 @@ Declared in `index.css`.
236
236
  }
237
237
  ```
238
238
 
239
- ## era-ink-center
240
-
241
- **Use it for:** on a native single-line <input> (or the text layer that must mirror one) inside a fixed-height field never alongside era-text-trim.
242
-
243
- The EDITABLE counterpart of era-text-trim for native <input>s, which the
244
- trim is forbidden on (it collapses the UA's inner editor box and slices the
245
- glyphs; see typography/collapsed-text-trim).
246
-
247
- Without help, a tier-height input centres its text by FONT BOX, and the ui
248
- stacks' font box is deeply asymmetric: ascent ≈1.07em against descent
249
- ≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em
250
- LOW so a field's text sat visibly below the trimmed label beside it.
251
-
252
- The value is OPTICAL, not the derivation's: pure x-band centring computes
253
- to ~0.23em of padding, but pixel-measured against real Title-Case text it
254
- over-lifts the eye weighs the cap-to-baseline mass, which x-band
255
- centring parks ~2px high (caps 2.75px off the top against 5.7px of air
256
- under the baseline, measured at dense). 0.08em splits the difference
257
- between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
258
- 18px field with ~4.7px under the baseline the reading where neither
259
- band's partisans win and neither complains.
260
-
261
- The lift is padding, not a translateY: an icon-less Input paints the field
262
- chrome itself, and a transform would drag the field along with the ink.
263
- padding-bottom shrinks the content box from below, and a single-line input
264
- re-centres its line box in what remains — the ink rises by exactly half the
265
- padding, the chrome doesn't move, and the caret and selection follow the
266
- text. In em on purpose: the correction is a fraction of the type, so it
267
- holds across every density and text size with one value.
239
+ ## era-token-ink · era-ink-center
240
+
241
+ **Use it for:** the ink centring for a token pill a badge, a KV value, any short caps-or-digits label in a fixed-height box; it needs no icon sibling (with one the box must stay flex, and the general trim applies).
242
+
243
+ TOKEN INK the ink centring for a pill whose content is a TOKEN: an id, a
244
+ count, a status, a tag. Font-independent, unlike everything it replaces.
245
+
246
+ THE BUG IT FIXES, which had been shipping library-wide and invisibly:
247
+ `text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all
248
+ flex, and a flex container's bare text becomes an ANONYMOUS flex item a box
249
+ no selector can reach and no non-inherited property can touch. So
250
+ `era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one
251
+ thing: `line-height: 1`. The ink was then centred by `align-items: center`
252
+ on the LINE BOX, which is precisely the font-metric-dependent centring the
253
+ trim exists to eliminate. Measured on the shipped mono stack: a badge reading
254
+ "AC-2.1" sat 2px high in an 18px pill, and no amount of tuning the trim's
255
+ BAND changed it, because the trim was never running.
256
+
257
+ The mechanism here restores it: a BLOCK box (where text-box-trim does apply)
258
+ whose trimmed line box is then centred by `align-content` block-container
259
+ alignment, so no flex formatting context and no anonymous item. The browser
260
+ computes the band from the font's own metrics, which is what makes it hold
261
+ across ui-monospace, system-ui and ui-serif instead of needing a constant
262
+ per stack.
263
+
264
+ CAP band, not the x-height band the general trim uses, because the content
265
+ class is different: a token is caps and digits (both cap-height), and
266
+ centring the x-band parks them high by (cap x)/2. Measured on the shipped
267
+ stacks, cap-band centring takes an id/count/status from −2.0px to 0.0.
268
+ Prose and sentence-case labels keep era-text-trim; this is for tokens.
268
269
 
269
270
  Declared in `index.css`.
270
271
 
271
272
  ```css
273
+ @utility era-token-ink {
274
+ display: block;
275
+ align-content: center;
276
+ text-align: center;
277
+ line-height: 1;
278
+ text-box: trim-both cap alphabetic;
279
+ }
280
+
272
281
  @utility era-ink-center {
273
282
  padding-bottom: 0.08em;
274
283
  }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,35 @@
1
- export * from './ui/index.js';
2
- export * as OS from './os/index.js';
3
- export * as AI from './ai/index.js';
4
- export type { Point, Size, Rect, Insets, Frame, Workspace, PaneState, JsonValue, PaneSnapshot, WorkspaceSnapshot, SnapZone, AppPane, AppDefinition, AppComponent, AppProps } from './os/index.js';
5
- export { PaneManager, SNAPSHOT_VERSION, readSnapshot, persistWorkspace, loadWorkspace, saveWorkspace, clearWorkspace, WORKSPACE_STORAGE_KEY } from './os/index.js';
1
+ /**
2
+ * The SYSTEM layer — and deliberately nothing else.
3
+ *
4
+ * This used to be `export * from './ui/index.js'` plus the OS and AI
5
+ * namespaces: one import that could reach every component in the library. A
6
+ * barrel like that is a guarantee that the whole graph is reachable, and
7
+ * reachable is what matters — `import { Button } from '@signal9/era-ui'` put
8
+ * vidstack (4MB, needed by exactly one component nobody was rendering) into the
9
+ * module graph, where a production bundler MIGHT prove it away and a dev server
10
+ * pre-bundles it regardless. Selectivity that depends on a bundler proving
11
+ * purity is not selectivity; it is luck with good tooling.
12
+ *
13
+ * So the root now carries only what is genuinely cross-cutting and weightless:
14
+ * the class utilities every consumer composes with, the keybinding helpers, and
15
+ * the AXIS REGISTRY plus `Mode` — the design system's own contract, the thing
16
+ * you reach for to set up density/surface/corners/font/motion for a subtree.
17
+ * None of it imports a component; none of it imports a third-party runtime
18
+ * beyond clsx / tailwind-merge / tailwind-variants.
19
+ *
20
+ * Everything else is one import away, at its own entry point:
21
+ *
22
+ * import { Button } from '@signal9/era-ui/button';
23
+ * import * as Select from '@signal9/era-ui/select';
24
+ * import { Swarm } from '@signal9/era-ui/ai/swarm';
25
+ * import { Desktop } from '@signal9/era-ui/os';
26
+ * import { Notes } from '@signal9/era-ui/apps/notes';
27
+ * import { runAudit } from '@signal9/era-ui/audit';
28
+ *
29
+ * The `./*` wildcard in package.json means a new component is importable the
30
+ * moment it exists, with no export map to maintain — and it means no component
31
+ * can ever be pulled in by accident, because nothing re-exports them together.
32
+ */
6
33
  export { cn, tv, hotkeys, keys, parseKeybinding, matchKeyBindingPress, createKeybindingsHandler, formatKeybinding, isApplePlatform, modKey, type KeyBindingMap, type KeyBindingPress, type KeyBindingOptions, type PartProps, type VariantProps } from './utils/index.js';
34
+ export { modes, defaultMode, surfaces, defaultSurface, surfaceRegistry, corners, defaultCorners, fonts, defaultFont, motions, defaultMotion, axisRegistry, axisKeys, type Mode, type Surface, type SurfaceMeta, type Corners, type Font, type Motion, type AxisKey, type AxisMeta, type AxisLevel } from './ui/provider/index.js';
35
+ export { Mode as ModeProvider } from './ui/mode/index.js';
package/dist/index.js CHANGED
@@ -1,9 +1,43 @@
1
- export * from './ui/index.js';
2
- export * as OS from './os/index.js';
3
- export * as AI from './ai/index.js';
4
- // The snapshot + persistence API, flat for the same reason: saving and restoring
5
- // a desktop is a first-class thing consumers do, not an OS-namespace detail.
6
- // PaneManager comes with them because it is what every one of these functions
7
- // takes without it the flat surface cannot type its own arguments.
8
- export { PaneManager, SNAPSHOT_VERSION, readSnapshot, persistWorkspace, loadWorkspace, saveWorkspace, clearWorkspace, WORKSPACE_STORAGE_KEY } from './os/index.js';
1
+ /**
2
+ * The SYSTEM layer — and deliberately nothing else.
3
+ *
4
+ * This used to be `export * from './ui/index.js'` plus the OS and AI
5
+ * namespaces: one import that could reach every component in the library. A
6
+ * barrel like that is a guarantee that the whole graph is reachable, and
7
+ * reachable is what matters `import { Button } from '@signal9/era-ui'` put
8
+ * vidstack (4MB, needed by exactly one component nobody was rendering) into the
9
+ * module graph, where a production bundler MIGHT prove it away and a dev server
10
+ * pre-bundles it regardless. Selectivity that depends on a bundler proving
11
+ * purity is not selectivity; it is luck with good tooling.
12
+ *
13
+ * So the root now carries only what is genuinely cross-cutting and weightless:
14
+ * the class utilities every consumer composes with, the keybinding helpers, and
15
+ * the AXIS REGISTRY plus `Mode` — the design system's own contract, the thing
16
+ * you reach for to set up density/surface/corners/font/motion for a subtree.
17
+ * None of it imports a component; none of it imports a third-party runtime
18
+ * beyond clsx / tailwind-merge / tailwind-variants.
19
+ *
20
+ * Everything else is one import away, at its own entry point:
21
+ *
22
+ * import { Button } from '@signal9/era-ui/button';
23
+ * import * as Select from '@signal9/era-ui/select';
24
+ * import { Swarm } from '@signal9/era-ui/ai/swarm';
25
+ * import { Desktop } from '@signal9/era-ui/os';
26
+ * import { Notes } from '@signal9/era-ui/apps/notes';
27
+ * import { runAudit } from '@signal9/era-ui/audit';
28
+ *
29
+ * The `./*` wildcard in package.json means a new component is importable the
30
+ * moment it exists, with no export map to maintain — and it means no component
31
+ * can ever be pulled in by accident, because nothing re-exports them together.
32
+ */
33
+ /* The composition utilities: `cn` for class merging (era-aware — it knows the
34
+ * tier/radius/shadow scales), `tv` for variants, and the keybinding layer. */
9
35
  export { cn, tv, hotkeys, keys, parseKeybinding, matchKeyBindingPress, createKeybindingsHandler, formatKeybinding, isApplePlatform, modKey } from './utils/index.js';
36
+ /* The axis contract — the five orthogonal display axes, their levels, their
37
+ * defaults, and the registry that drives every picker built on top of them.
38
+ * Pure data: no component, no DOM, no dependency. */
39
+ export { modes, defaultMode, surfaces, defaultSurface, surfaceRegistry, corners, defaultCorners, fonts, defaultFont, motions, defaultMotion, axisRegistry, axisKeys } from './ui/provider/index.js';
40
+ /* `Mode` applies those axes (plus optional accent overrides) to a subtree. It
41
+ * is the one component that belongs here: it configures the system rather than
42
+ * being a piece of UI, and it imports nothing but types. */
43
+ export { Mode as ModeProvider } from './ui/mode/index.js';
@@ -81,7 +81,7 @@ function stores() {
81
81
  */
82
82
  export function loadWorkspace(pm, key = WORKSPACE_STORAGE_KEY) {
83
83
  for (const store of stores()) {
84
- let raw = null;
84
+ let raw;
85
85
  try {
86
86
  raw = store.getItem(key);
87
87
  }
@@ -11,21 +11,31 @@
11
11
  @import './surfaces/bevel.css';
12
12
  @import './typography.css';
13
13
 
14
- /* vidstack base layout + slider/button primitives for the VideoPlayer component.
15
- * Scoped internally to `:where(media-player)` so these rules don't affect
16
- * anything outside of a media-player subtree. Design-token theming is applied
17
- * on the component itself via CSS variables. */
18
- @import 'vidstack/styles/base.css';
19
- @import 'vidstack/styles/ui/buttons.css';
20
- @import 'vidstack/styles/ui/sliders.css';
21
- @import 'vidstack/styles/ui/buffering.css';
22
- @import 'vidstack/styles/ui/captions.css';
23
- @import 'vidstack/styles/ui/live.css';
24
- @import 'vidstack/styles/ui/menus.css';
25
- @import 'vidstack/styles/ui/tooltips.css';
14
+ /* WHAT IS NOT HERE: vidstack's stylesheets, which the VideoPlayer needs and
15
+ * nothing else does. They were imported right here, so every consumer of this
16
+ * file paid 46KB — 37% of the built stylesheet for a component almost none of
17
+ * them render. They now live in ./video-player.css, published as
18
+ * `@signal9/era-ui/styles/video-player`, and only a consumer using the player
19
+ * imports them.
20
+ *
21
+ * The split is by DEPENDENCY WEIGHT, not by layer, and that is deliberate.
22
+ * Tailwind utilities DEDUPE — 60-odd components share one small vocabulary of
23
+ * flex/h-md/items-center, so scanning every layer below costs almost nothing
24
+ * and cannot be forgotten. Third-party CSS is the opposite: it is unshared,
25
+ * heavy, and belongs to exactly one component. Split what is unshared; keep
26
+ * what is shared whole. */
26
27
 
27
- /* Tell Tailwind to scan this package's components for utility classes */
28
+ /* Scan every SHIPPED layer for utility classes, not just ui.
29
+ *
30
+ * This read `@source '../ui'` alone, so a consumer on the source-CSS path got
31
+ * no utilities generated for any ai/, os/ or apps/ component — those rendered
32
+ * structurally correct and completely unstyled, silently. The scan follows the
33
+ * package's layers so that adding a layer means adding a line here, and
34
+ * nothing else. */
28
35
  @source '../ui';
36
+ @source '../ai';
37
+ @source '../os';
38
+ @source '../apps';
29
39
 
30
40
  /* Lucide icons use an SVG presentation attribute for stroke width by default.
31
41
  * Set it from an Era token globally so every imported Lucide component tracks
@@ -261,6 +271,44 @@
261
271
  *
262
272
  * @use on a native single-line <input> (or the text layer that must mirror
263
273
  * one) inside a fixed-height field — never alongside era-text-trim. */
274
+ /* TOKEN INK — the ink centring for a pill whose content is a TOKEN: an id, a
275
+ * count, a status, a tag. Font-independent, unlike everything it replaces.
276
+ *
277
+ * THE BUG IT FIXES, which had been shipping library-wide and invisibly:
278
+ * `text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all
279
+ * flex, and a flex container's bare text becomes an ANONYMOUS flex item — a box
280
+ * no selector can reach and no non-inherited property can touch. So
281
+ * `era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one
282
+ * thing: `line-height: 1`. The ink was then centred by `align-items: center`
283
+ * on the LINE BOX, which is precisely the font-metric-dependent centring the
284
+ * trim exists to eliminate. Measured on the shipped mono stack: a badge reading
285
+ * "AC-2.1" sat 2px high in an 18px pill, and no amount of tuning the trim's
286
+ * BAND changed it, because the trim was never running.
287
+ *
288
+ * The mechanism here restores it: a BLOCK box (where text-box-trim does apply)
289
+ * whose trimmed line box is then centred by `align-content` — block-container
290
+ * alignment, so no flex formatting context and no anonymous item. The browser
291
+ * computes the band from the font's own metrics, which is what makes it hold
292
+ * across ui-monospace, system-ui and ui-serif instead of needing a constant
293
+ * per stack.
294
+ *
295
+ * CAP band, not the x-height band the general trim uses, because the content
296
+ * class is different: a token is caps and digits (both cap-height), and
297
+ * centring the x-band parks them high by (cap − x)/2. Measured on the shipped
298
+ * stacks, cap-band centring takes an id/count/status from −2.0px to 0.0.
299
+ * Prose and sentence-case labels keep era-text-trim; this is for tokens.
300
+ *
301
+ * @use the ink centring for a token pill — a badge, a KV value, any short
302
+ * caps-or-digits label in a fixed-height box; it needs no icon sibling (with
303
+ * one the box must stay flex, and the general trim applies). */
304
+ @utility era-token-ink {
305
+ display: block;
306
+ align-content: center;
307
+ text-align: center;
308
+ line-height: 1;
309
+ text-box: trim-both cap alphabetic;
310
+ }
311
+
264
312
  @utility era-ink-center {
265
313
  padding-bottom: 0.08em;
266
314
  }