@signal9/era-ui 32.0.0 → 33.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/dev/audit-overlay.svelte +2 -2
  3. package/dist/era-ui.css +1 -1
  4. package/dist/generated-docs/utilities.json +99 -99
  5. package/dist/os/pane.svelte +3 -13
  6. package/dist/styles/index.css +2 -956
  7. package/dist/styles/typography.css +1 -1
  8. package/dist/styles/utilities/geometry.css +188 -0
  9. package/dist/styles/utilities/interaction.css +334 -0
  10. package/dist/styles/utilities/materials.css +39 -0
  11. package/dist/styles/utilities/public.css +391 -0
  12. package/dist/styles/utilities.css +5 -0
  13. package/dist/ui/card/card.svelte +2 -52
  14. package/dist/ui/card/card.svelte.d.ts +0 -43
  15. package/dist/ui/card/variants.d.ts +44 -0
  16. package/dist/ui/card/variants.js +20 -0
  17. package/dist/ui/context-menu/context-menu-checkbox-item.svelte +2 -4
  18. package/dist/ui/context-menu/context-menu-group-heading.svelte +2 -5
  19. package/dist/ui/context-menu/context-menu-item.svelte +2 -8
  20. package/dist/ui/context-menu/context-menu-radio-item.svelte +2 -8
  21. package/dist/ui/context-menu/context-menu-separator.svelte +2 -5
  22. package/dist/ui/context-menu/context-menu-sub-trigger.svelte +2 -8
  23. package/dist/ui/dialog/dialog-header.svelte +1 -1
  24. package/dist/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +2 -4
  25. package/dist/ui/dropdown-menu/dropdown-menu-group-heading.svelte +2 -5
  26. package/dist/ui/dropdown-menu/dropdown-menu-item.svelte +2 -8
  27. package/dist/ui/dropdown-menu/dropdown-menu-radio-item.svelte +2 -8
  28. package/dist/ui/dropdown-menu/dropdown-menu-separator.svelte +2 -5
  29. package/dist/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +2 -8
  30. package/dist/ui/menu-recipes.d.ts +6 -0
  31. package/dist/ui/menu-recipes.js +6 -0
  32. package/dist/ui/menubar/menubar-checkbox-item.svelte +2 -9
  33. package/dist/ui/menubar/menubar-group-heading.svelte +2 -5
  34. package/dist/ui/menubar/menubar-item.svelte +2 -8
  35. package/dist/ui/menubar/menubar-radio-item.svelte +2 -8
  36. package/dist/ui/menubar/menubar-separator.svelte +2 -5
  37. package/dist/ui/menubar/menubar-sub-trigger.svelte +2 -8
  38. package/dist/ui/pane/pane-close.svelte +7 -26
  39. package/dist/ui/video-player/video-player.svelte +8 -8
  40. package/package.json +48 -12
  41. package/dist/docs/badge.md +0 -18
  42. package/dist/docs/bits-ui-audit.md +0 -58
  43. package/dist/docs/button.md +0 -19
  44. package/dist/docs/chip.md +0 -20
  45. package/dist/docs/component-docs-core.d.ts +0 -8
  46. package/dist/docs/component-docs-core.js +0 -73
  47. package/dist/docs/component-docs.d.ts +0 -2
  48. package/dist/docs/component-docs.js +0 -25
  49. package/dist/docs/component-source-scanners.d.ts +0 -18
  50. package/dist/docs/component-source-scanners.js +0 -46
  51. package/dist/docs/consumer-import.d.ts +0 -65
  52. package/dist/docs/consumer-import.js +0 -70
  53. package/dist/docs/css-utilities.d.ts +0 -58
  54. package/dist/docs/css-utilities.js +0 -317
  55. package/dist/docs/extract-props.d.ts +0 -13
  56. package/dist/docs/extract-props.js +0 -324
  57. package/dist/docs/generated-docs.d.ts +0 -6
  58. package/dist/docs/generated-docs.js +0 -30
  59. package/dist/docs/index.d.ts +0 -5
  60. package/dist/docs/index.js +0 -66
  61. package/dist/docs/notes.md +0 -83
  62. package/dist/docs/scroll-area.md +0 -7
  63. package/dist/docs/separator.md +0 -20
  64. package/dist/docs/table.md +0 -35
  65. package/dist/docs/utilities.d.ts +0 -4
  66. package/dist/docs/utilities.js +0 -15
  67. package/dist/generated-docs/accordion.md +0 -57
  68. package/dist/generated-docs/alert-dialog.md +0 -91
  69. package/dist/generated-docs/aspect-ratio.md +0 -17
  70. package/dist/generated-docs/avatar.md +0 -39
  71. package/dist/generated-docs/badge.md +0 -41
  72. package/dist/generated-docs/bar.md +0 -41
  73. package/dist/generated-docs/button-group.md +0 -19
  74. package/dist/generated-docs/button.md +0 -117
  75. package/dist/generated-docs/calendar.md +0 -126
  76. package/dist/generated-docs/card.md +0 -58
  77. package/dist/generated-docs/checkbox.md +0 -42
  78. package/dist/generated-docs/chip.md +0 -61
  79. package/dist/generated-docs/code-block.md +0 -20
  80. package/dist/generated-docs/collapsible.md +0 -30
  81. package/dist/generated-docs/combobox.md +0 -74
  82. package/dist/generated-docs/command-bar.md +0 -3
  83. package/dist/generated-docs/command.md +0 -126
  84. package/dist/generated-docs/context-menu.md +0 -104
  85. package/dist/generated-docs/copy-button.md +0 -29
  86. package/dist/generated-docs/cycle.md +0 -29
  87. package/dist/generated-docs/date-field.md +0 -49
  88. package/dist/generated-docs/date-picker.md +0 -180
  89. package/dist/generated-docs/date-range-field.md +0 -50
  90. package/dist/generated-docs/date-range-picker.md +0 -180
  91. package/dist/generated-docs/dialog.md +0 -94
  92. package/dist/generated-docs/draggable.md +0 -3
  93. package/dist/generated-docs/dropdown-menu.md +0 -104
  94. package/dist/generated-docs/file-upload.md +0 -18
  95. package/dist/generated-docs/getting-started.md +0 -3
  96. package/dist/generated-docs/input.md +0 -23
  97. package/dist/generated-docs/kv.md +0 -20
  98. package/dist/generated-docs/label.md +0 -17
  99. package/dist/generated-docs/link-preview.md +0 -40
  100. package/dist/generated-docs/llm-shell.md +0 -3
  101. package/dist/generated-docs/llms-full.txt +0 -5201
  102. package/dist/generated-docs/llms.txt +0 -138
  103. package/dist/generated-docs/logo.md +0 -24
  104. package/dist/generated-docs/manifest.json +0 -1685
  105. package/dist/generated-docs/measurements.md +0 -3
  106. package/dist/generated-docs/menu.md +0 -11
  107. package/dist/generated-docs/menubar.md +0 -104
  108. package/dist/generated-docs/meter.md +0 -101
  109. package/dist/generated-docs/mode.md +0 -24
  110. package/dist/generated-docs/nav.md +0 -73
  111. package/dist/generated-docs/navigation-menu.md +0 -11
  112. package/dist/generated-docs/notes.md +0 -87
  113. package/dist/generated-docs/os.md +0 -3
  114. package/dist/generated-docs/pagination.md +0 -55
  115. package/dist/generated-docs/pane.md +0 -34
  116. package/dist/generated-docs/pin-input.md +0 -31
  117. package/dist/generated-docs/popover.md +0 -43
  118. package/dist/generated-docs/progress.md +0 -17
  119. package/dist/generated-docs/radio-group.md +0 -31
  120. package/dist/generated-docs/range-calendar.md +0 -11
  121. package/dist/generated-docs/rating-group.md +0 -31
  122. package/dist/generated-docs/scroll-area.md +0 -65
  123. package/dist/generated-docs/select.md +0 -67
  124. package/dist/generated-docs/separator.md +0 -57
  125. package/dist/generated-docs/sheet.md +0 -11
  126. package/dist/generated-docs/skeleton.md +0 -18
  127. package/dist/generated-docs/slider.md +0 -71
  128. package/dist/generated-docs/spacing.md +0 -3
  129. package/dist/generated-docs/step.md +0 -51
  130. package/dist/generated-docs/surfaces.md +0 -3
  131. package/dist/generated-docs/switch.md +0 -18
  132. package/dist/generated-docs/table.md +0 -113
  133. package/dist/generated-docs/tabs.md +0 -48
  134. package/dist/generated-docs/text.md +0 -3
  135. package/dist/generated-docs/time-field.md +0 -49
  136. package/dist/generated-docs/time-range-field.md +0 -50
  137. package/dist/generated-docs/timeline.md +0 -32
  138. package/dist/generated-docs/todo.md +0 -3
  139. package/dist/generated-docs/toggle-group.md +0 -31
  140. package/dist/generated-docs/toggle.md +0 -20
  141. package/dist/generated-docs/toolbar.md +0 -11
  142. package/dist/generated-docs/tooltip.md +0 -40
  143. package/dist/generated-docs/tree.md +0 -36
  144. package/dist/generated-docs/utilities.md +0 -1236
  145. package/dist/generated-docs/video-player.md +0 -73
@@ -1,1236 +0,0 @@
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's styling API is pure class names — write `tier-control px-gutter shadow`,
8
- never the `h-(--era-h-control)` var-class spelling. Everything below rides along
9
- with the stylesheet and needs no import:
10
-
11
- ```ts
12
- import "@signal9/era-ui/css";
13
- ```
14
-
15
- ```svelte
16
- <button class="tier-control px-inset-control shadow text-body">era styled</button>
17
- ```
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
- | `tier-icon/pill/chip/control/bar` | indivisible bounded height + concentric radius |
27
- | `rounded-icon/pill/chip/control/bar` | the concentric radius ladder (collapses on `data-corners="square"`) |
28
- | `rounded-menu-item` | menu-row radius, concentric inside a `rounded-control` panel |
29
- | `shadow` / `shadow-lg` | resting control edge / floating panel chrome, per surface |
30
- | `shadow-well` / `shadow-pressed` / `shadow-highlight` | recessed, pressed/latched, hover-material chrome |
31
- | `bg-well` / `bg-elevated` / `bg-highlight` / `bg-overlay` | surface fills: resting, raised, transient feedback, modal scrim |
32
- | `transition-*` (bare) | duration + easing default to the motion axis |
33
- | `ease-base` / `duration-base` | the motion axis, named explicitly |
34
- | `text-body` | density-derived body/control text size |
35
- | `p-gutter` / `gap-gutter` / `mt-gutter` / … | the spacing scale — EVERY spacing prefix works |
36
-
37
- THE SPACING NAMES IN SIZE ORDER, smallest first, because the names do not
38
- imply it and guessing gets it backwards:
39
-
40
- gutter < panel < field < card < content
41
-
42
- Measured at `data-mode="balanced"`: gutter 5px, field ~9px, card ~11px,
43
- content ~22px. `gutter` is the TIGHT one — the gap between sibling controls
44
- and a rail's own padding. `field` is a text input's side padding, not the
45
- label-to-input gap. Reading them the other way round produces a layout whose
46
- hierarchy is inverted and therefore reads flat.
47
-
48
- The spacing names (`gutter`, `card`, `panel`, `content`, `menu`, `field`, the
49
- `inset-*` and concentric `*-inset-*` insets, `glyph-*`) are registered as a
50
- Tailwind spacing scale, so every prefix derives automatically: `p-`, `px-`,
51
- `pt-`, `mt-`, `gap-`, `gap-x-`, `inset-`, `translate-`, and the rest. The TIER
52
- names (`icon`, `pill`, `chip`, `control`, `bar`) are dimensions, not spacing, so
53
- they are registered per prefix instead — `h-`, `w-`, `min-h-`, `min-w-` and
54
- `size-` — and the two vocabularies deliberately share no word. Putting a tier
55
- in the spacing scale would silently make `px-control` mean 24px of padding.
56
- For chrome, use `tier-T`, which binds `h-T` and `rounded-T`; use the separate
57
- dimension/radius names only for width, minimums, unbounded surfaces, or
58
- asymmetric/docked corners.
59
-
60
- The var form (`h-(--era-h-control)`) still works and is the escape hatch for a
61
- token with no registered name — `--era-sp`, the press-sink offsets, anything
62
- a component reaches for once. `scripts/check-canonical.ts` fails the build if
63
- source uses it where a name exists.
64
-
65
- | Utility | Use it for |
66
- |---|---|
67
- | `era-interactive` | the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants. |
68
- | `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). |
69
- | `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. |
70
- | `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. |
71
- | `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. |
72
- | `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`. |
73
- | `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. |
74
- | `era-token-ink` | the ink centring for a token pill — a badge, a KV value, any short caps-or-digits label in a fixed-height box, CENTRED horizontally and with no icon sibling. For a left-aligned sentence-case label use era-label-ink; when an icon really must sit beside the text, see the note there. |
75
- | `era-label-ink` | the ink centring for a fixed-height control holding ONE line of sentence-case text and no icon — a menu item, a nav trigger, a button label. Never alongside `truncate` (the trimmed box becomes the clip). |
76
- | `surface-backdrop` | the surface-aware backdrop treatment without choosing a fill or relief. Prefer material-popover/material-panel when the element is a complete surface. |
77
- | `material-popover` | a floating transient surface: menus, popovers, tooltips, pickers, toasts. Binds its raised fill, floating relief, and backdrop treatment atomically. |
78
- | `material-panel` | a floating persistent surface: dialogs, sheets, panes, inspectors. Binds its resting fill, floating relief, and backdrop treatment atomically. |
79
- | `pressable` | the physical press contract on a control whose direct content plane is marked data-era-press-content. The surface owns relief/travel; the component may scale travel with --era-press-scale. |
80
- | `tier-icon` `tier-pill` `tier-chip` `tier-control` `tier-bar` | — |
81
- | `h-line` | the height of one line of the element's own text — a skeleton standing in for a line, a spacer that must match a line box. |
82
- | `era-track-well` | instead of `shadow-well` on a track whose value is painted by a child that covers the box — a progress/meter/slider track, a custom gauge. For a well holding ordinary content, `shadow-well` is still right. |
83
- | `era-skeleton-pulse` | — |
84
- | `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. |
85
- | `era-collapse` | the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden). |
86
- | `h-icon` `h-pill` `h-chip` `h-control` `h-bar` | the tier-height ladder — h-icon (icon) · h-pill (nested pill) · h-chip (standalone pill) · h-control (control) · h-bar (bar); the named form of h-(--era-h-*). |
87
- | `min-h-icon` `min-h-pill` `min-h-chip` `min-h-control` `min-h-bar` | minimum tier height for containers that may grow — a bar that is at least lg-tier but can wrap (min-h-bar). |
88
- | `w-icon` `w-pill` `w-chip` `w-control` `w-bar` | a tier-sized WIDTH — the horizontal twin of the h-* ladder, for a rail or a column measured in tiers rather than in content. |
89
- | `min-w-icon` `min-w-pill` `min-w-chip` `min-w-control` `min-w-bar` | a minimum tier WIDTH — the floor that makes a single-glyph control render as a square rather than a skinny pill (min-w-control on an icon button, min-w-pill on a badge). These could not exist before the ladder was renamed: the old rungs were sm/md/lg, and Tailwind's container scale already owns min-w-md and min-w-lg, so the names collided and every call site had to write min-w-(--era-h-control) by hand. icon/pill/chip/control/bar collide with nothing. |
90
- | `size-icon` `size-pill` `size-chip` `size-control` `size-bar` | a tier-sized square — size-icon is THE icon size (a Lucide glyph beside body text), size-control a square control face. |
91
- | `px-pill` `px-chip` `pl-pill` `pr-pill` `pl-chip` `pr-chip` | pill side padding — the wider-than-vertical horizontal breathing room badge/chip text wants (glyph-advance derived, ~2× the vertical inset). |
92
- | `p-section` | padding for a content panel that sits UNDER a section header row — the even-padding exception, and the reason it is not called p-section: it deliberately under-pays the block start. For an evenly padded content panel use p-section, which is the plain token. |
93
- | `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". |
94
- | `text-micro` `text-lede` `text-heading` `text-title` `text-display` | the type ladder — micro (eyebrow/legend) < body (controls) < lede (the paragraph under a title) < heading < title < display. Derived from --era-text, so the ratio between any two rungs holds at every density; leading and tracking ride with the size, so a rung is one class. |
95
- | `text-body` | — |
96
- | `leading-body` | — |
97
- | `rounded-icon` `rounded-pill` `rounded-chip` `rounded-control` `rounded-bar` `rounded-menu-item` | — |
98
- | `shadow` | — |
99
- | `shadow-sm` `shadow-lg` `shadow-well` `shadow-pressed` `shadow-highlight` | — |
100
- | `bg-well` `text-well` `border-well` `bg-elevated` `text-elevated` `border-elevated` `bg-highlight` `text-highlight` `border-highlight` `bg-overlay` `text-overlay` `border-overlay` | — |
101
- | `p-gutter` `px-gutter` `py-gutter` `gap-gutter` `p-panel` `px-panel` `py-panel` `gap-panel` `p-card` `px-card` `py-card` `gap-card` `p-content` `px-content` `py-content` `gap-content` `p-menu` `px-menu` `py-menu` `gap-menu` `p-field` `px-field` `py-field` `gap-field` | — |
102
- | `p-inset-pill` `px-inset-pill` `py-inset-pill` `gap-inset-pill` `p-inset-chip` `px-inset-chip` `py-inset-chip` `gap-inset-chip` `p-inset-control` `px-inset-control` `py-inset-control` `gap-inset-control` | text-symmetric content padding — (tier − text)/2, the inset that vertically centres body text in its tier; px-inset-control is a text button's side padding, p-inset-control a code block's. |
103
- | `p-icon-inset-pill` `px-icon-inset-pill` `py-icon-inset-pill` `gap-icon-inset-pill` `p-icon-inset-chip` `px-icon-inset-chip` `py-icon-inset-chip` `gap-icon-inset-chip` `p-icon-inset-control` `px-icon-inset-control` `py-icon-inset-control` `gap-icon-inset-control` `p-icon-inset-bar` `px-icon-inset-bar` `py-icon-inset-bar` `gap-icon-inset-bar` `p-pill-inset-chip` `px-pill-inset-chip` `py-pill-inset-chip` `gap-pill-inset-chip` `p-pill-inset-control` `px-pill-inset-control` `py-pill-inset-control` `gap-pill-inset-control` `p-pill-inset-bar` `px-pill-inset-bar` `py-pill-inset-bar` `gap-pill-inset-bar` `p-chip-inset-control` `px-chip-inset-control` `py-chip-inset-control` `gap-chip-inset-control` `p-chip-inset-bar` `px-chip-inset-bar` `py-chip-inset-bar` `gap-chip-inset-bar` `p-control-inset-bar` `px-control-inset-bar` `py-control-inset-bar` `gap-control-inset-bar` | the concentric inset for a nested pair — (h-container − h-child)/2, the gap that makes a child control sit evenly inside its container, and the radius step that keeps their corners concentric. Named for the pair it joins: gap-icon-inset-control seats an icon in a control. |
104
- | `p-glyph-icon` `px-glyph-icon` `py-glyph-icon` `gap-glyph-icon` `p-glyph-pill` `px-glyph-pill` `py-glyph-pill` `gap-glyph-pill` | the glyph size for a tier — the INK inside the box, not the box (size-glyph-pill is the dismiss × in a pill, size-pill is the pill). |
105
- | `bg-field` `text-field` `border-field` `bg-field-highlight` `text-field-highlight` `border-field-highlight` | the recessed field fill — bg-field is a text input's resting well, bg-field-highlight its focused/hovered face. |
106
- | `ease-base` | — |
107
- | `bg-divider` `text-divider` `border-divider` `bg-divider-faded` `text-divider-faded` `border-divider-faded` `bg-background` `text-background` `border-background` `bg-foreground` `text-foreground` `border-foreground` `bg-1` `text-1` `border-1` `bg-2` `text-2` `border-2` `bg-3` `text-3` `border-3` `bg-4` `text-4` `border-4` `bg-5` `text-5` `border-5` `bg-6` `text-6` `border-6` `bg-7` `text-7` `border-7` `bg-8` `text-8` `border-8` `bg-9` `text-9` `border-9` `bg-10` `text-10` `border-10` `bg-11` `text-11` `border-11` `bg-12` `text-12` `border-12` `bg-border` `text-border` `border-border` `bg-surface` `text-surface` `border-surface` `bg-hover` `text-hover` `border-hover` `bg-fg` `text-fg` `border-fg` `bg-bright` `text-bright` `border-bright` `bg-muted` `text-muted` `border-muted` | — |
108
- | `bg-fill` `text-fill` `border-fill` `bg-link` `text-link` `border-link` `bg-primary` `text-primary` `border-primary` `bg-primary-fg` `text-primary-fg` `border-primary-fg` `bg-primary-hover` `text-primary-hover` `border-primary-hover` `bg-destructive` `text-destructive` `border-destructive` `bg-destructive-fg` `text-destructive-fg` `border-destructive-fg` `bg-success` `text-success` `border-success` `bg-success-fg` `text-success-fg` `border-success-fg` `bg-warning` `text-warning` `border-warning` `bg-warning-fg` `text-warning-fg` `border-warning-fg` `bg-info` `text-info` `border-info` `bg-info-fg` `text-info-fg` `border-info-fg` | — |
109
-
110
- ## era-interactive
111
-
112
- **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.
113
-
114
- Shared interactive-state recipe: quiet focus, disabled affordance.
115
- Equivalent to `focus:outline-none disabled:cursor-not-allowed disabled:opacity-50`.
116
-
117
- Declared in `index.css`.
118
-
119
- ```css
120
- @utility era-interactive {
121
- &:focus {
122
- outline: none;
123
- }
124
- &:disabled {
125
- cursor: not-allowed;
126
- opacity: 0.5;
127
- }
128
- }
129
- ```
130
-
131
- ## era-link
132
-
133
- **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).
134
-
135
- Inline text link. An understated, permanent underline that darkens from
136
- muted to the text colour on hover — no layout shift, no appearing/vanishing
137
- underline. The colour fade rides the MOTION axis (--era-duration/-ease), so
138
- it snaps at data-motion="instant", eases at "normal", and sweeps longer at
139
- "extra" — matching every other era transition. Font-agnostic: it styles
140
- decoration + colour only, so it reads correctly in mono, sans, or serif, and
141
- the 0.2em offset scales with the type size. Keyboard focus mirrors hover so
142
- the affordance is reachable without a pointer.
143
-
144
- Declared in `index.css`.
145
-
146
- ```css
147
- @utility era-link {
148
- /* The text is bright at rest and never changes — the link reads as a link
149
- * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —
150
- * the link's own ink knocked back to 30% so it barely whispers — that
151
- * resolves to the full ink on hover.
152
- *
153
- * currentColor, not --color-muted/--color-fg: the underline belongs to the
154
- * TEXT it underlines. On themes whose link ink is a colour (signalnine's
155
- * navy, an accent-tinted bright), the neutral tokens drew a whitish line
156
- * under coloured text — a second hue where there should be one. Derived
157
- * from currentColor, the underline tracks whatever the link resolves to,
158
- * in every theme, including a caller overriding `color` locally. */
159
- color: var(--color-link);
160
- text-decoration-line: underline;
161
- text-decoration-color: color-mix(in oklch, currentColor 30%, 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: currentColor;
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, 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.
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 (measured at 14px; the
204
- mechanism is font-relative, so dense's 12px inherits the same guarantee with
205
- a proportionally smaller residual).
206
-
207
- The hard rules concern overflow clipping, and there are TWO of them.
208
-
209
- 1. NEVER on a NATIVE EDITABLE — <input>, <textarea>, <select>. These clip to
210
- a UA-owned inner editor box that IS the trimmed line box, so a fixed
211
- height does NOT hold it open: whatever you type gets its ascenders and
212
- descenders sliced off (a "d" renders as an "o"). This rule used to read
213
- the other way round — it claimed a fixed-height <input> was the trim's
214
- intended home — and the Input component shipped chopped text as a result.
215
- A tier-height input centres its own text; it needs no trim.
216
-
217
- 2. NEVER on an AUTO-height element that clips — a `truncate` child, an
218
- auto-height box with overflow-hidden. With no height to hold it open the
219
- box collapses to the x-height band (~7.5px at 14px) and the clip chops the
220
- glyphs into a middle stripe. Put the trim on the fixed-height ROW instead
221
- and let the child centre via the row's items-center.
222
-
223
- A fixed-height NON-editable element is the safe home: the ink overflows the
224
- trimmed LINE box but stays inside the padding box, which is what actually
225
- clips. Both rules are enforced at runtime by typography/collapsed-text-trim.
226
-
227
- Declared in `index.css`.
228
-
229
- ```css
230
- @utility era-text-trim {
231
- line-height: 1;
232
- text-box: trim-both ex alphabetic;
233
- }
234
- ```
235
-
236
- ## truncate
237
-
238
- **Use it for:** 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.
239
-
240
- Tailwind's `truncate`, with the vertical clip fixed at the source.
241
-
242
- truncate is overflow:hidden, and overflow clips in BOTH axes — so whatever
243
- line box the element ends up with is also its ink clip. Era rows put
244
- era-text-trim on the fixed-height row (a Bar, a list row), and its
245
- `line-height: 1` INHERITS into the truncating child, which then has no fixed
246
- height of its own: the child's box collapses to 1em while the font's real ink
247
- extent is ~1.15-1.2em, and the descenders (the "g" hook, the "y" tail) get
248
- sliced off at the box edge. Same bug on any `leading-none` + `truncate` pair.
249
-
250
- The fix belongs here rather than at each call site: a horizontally-truncating
251
- element must never be the thing that clips vertically. `line-height: normal`
252
- restores the font's own ascent+descent box — exactly the ink extent — and
253
- `text-box: normal` cancels a trim if one is set alongside. Rows stay the same
254
- height (they are tier-fixed and centre the child), and the child now has room
255
- for its own glyphs.
256
-
257
- Declared in `index.css`.
258
-
259
- ```css
260
- @utility truncate {
261
- overflow: hidden;
262
- text-overflow: ellipsis;
263
- white-space: nowrap;
264
- line-height: normal;
265
- text-box: normal;
266
- }
267
- ```
268
-
269
- ## era-text-trim-caps
270
-
271
- **Use it for:** 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`.
272
-
273
- ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
274
- misjudges caps-only strings — cap/alphabetic edges are the right box.
275
-
276
- Declared in `index.css`.
277
-
278
- ```css
279
- @utility era-text-trim-caps {
280
- line-height: 1;
281
- text-box: trim-both cap alphabetic;
282
- }
283
- ```
284
-
285
- ## era-ink-center
286
-
287
- **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.
288
-
289
- The EDITABLE counterpart of era-text-trim — for native <input>s, which the
290
- trim is forbidden on (it collapses the UA's inner editor box and slices the
291
- glyphs; see typography/collapsed-text-trim).
292
-
293
- Without help, a tier-height input centres its text by FONT BOX, and the ui
294
- stacks' font box is deeply asymmetric: ascent ≈1.07em against descent
295
- ≈0.29em. Centring that box parks the visible band (caps, x-height) ~0.115em
296
- LOW — so a field's text sat visibly below the trimmed label beside it.
297
-
298
- The value is OPTICAL, not the derivation's: pure x-band centring computes
299
- to ~0.23em of padding, but pixel-measured against real Title-Case text it
300
- over-lifts — the eye weighs the cap-to-baseline mass, which x-band
301
- centring parks ~2px high (caps 2.75px off the top against 5.7px of air
302
- under the baseline, measured at dense; the general form of that offset,
303
- and why era-label-ink's note about "System" is not a contradiction of it,
304
- is derived at era-label-ink below). 0.08em splits the difference
305
- between the x-band ideal and the cap-band ideal, landing caps ~3.7px in an
306
- 18px field with ~4.7px under the baseline — the reading where neither
307
- band's partisans win and neither complains.
308
-
309
- The lift is padding, not a translateY: an icon-less Input paints the field
310
- chrome itself, and a transform would drag the field along with the ink.
311
- padding-bottom shrinks the content box from below, and a single-line input
312
- re-centres its line box in what remains — the ink rises by exactly half the
313
- padding, the chrome doesn't move, and the caret and selection follow the
314
- text. In em on purpose: the correction is a fraction of the type, so it
315
- holds across every density and text size with one value.
316
-
317
- WHEN SOMEONE REPORTS A CORRECTLY-CENTRED FIELD AS "TOO HIGH", check the
318
- STRING before the geometry. A placeholder like "Filter…" has no descender and
319
- no cap, so its ink occupies only the x-height band — and a box that centres
320
- that band exactly still reads high, because the eye centres on the mass it
321
- can see and expects the space a descender would have filled. Measured at the
322
- pill tier on the shipped mono stack, "Filter…" sits within half a pixel of
323
- centre while looking a little high; the pill suite documents the same effect
324
- from the other side (see OFF_BAND_LOW in ink-centering.spec.ts, where "soon"
325
- cannot be symmetric and is bounded by sign instead of magnitude).
326
-
327
- That is not a licence to ignore the report — era-ink-center IS wrong by
328
- 1.5–2px on the sans and serif stacks (pinned in FIELD_KNOWN_SKEW) — only a
329
- reason to measure the ink before touching the constant.
330
-
331
- Declared in `index.css`.
332
-
333
- ```css
334
- @utility era-ink-center {
335
- padding-bottom: 0.08em;
336
- }
337
- ```
338
-
339
- ## era-token-ink
340
-
341
- **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, CENTRED horizontally and with no icon sibling. For a left-aligned sentence-case label use era-label-ink; when an icon really must sit beside the text, see the note there.
342
-
343
- TOKEN INK — the ink centring for a pill whose content is a TOKEN: an id, a
344
- count, a status, a tag. Font-independent, unlike everything it replaces.
345
-
346
- THE BUG IT FIXES, which had been shipping library-wide and invisibly:
347
- `text-box-trim` DOES NOTHING ON A FLEX CONTAINER. Era's controls are all
348
- flex, and a flex container's bare text becomes an ANONYMOUS flex item — a box
349
- no selector can reach and no non-inherited property can touch. So
350
- `era-text-trim` on a Badge, a Chip, a Button or a Bar contributed exactly one
351
- thing: `line-height: 1`. The ink was then centred by `align-items: center`
352
- on the LINE BOX, which is precisely the font-metric-dependent centring the
353
- trim exists to eliminate. Measured on the shipped mono stack: a badge reading
354
- "AC-2.1" sat 2px high in an 18px pill, and no amount of tuning the trim's
355
- BAND changed it, because the trim was never running.
356
-
357
- The mechanism here restores it: a BLOCK box (where text-box-trim does apply)
358
- whose trimmed line box is then centred by `align-content` — block-container
359
- alignment, so no flex formatting context and no anonymous item. The browser
360
- computes the band from the font's own metrics, which is what makes it hold
361
- across ui-monospace, system-ui and ui-serif instead of needing a constant
362
- per stack.
363
-
364
- CAP band, not the x-height band the general trim uses, because the content
365
- class is different: a token is caps and digits (both cap-height), and
366
- centring the x-band parks them high by (cap − x)/2. Measured on the shipped
367
- stacks, cap-band centring takes an id/count/status from −2.0px to 0.0.
368
- Prose and sentence-case labels keep era-text-trim; this is for tokens.
369
-
370
- Declared in `index.css`.
371
-
372
- ```css
373
- @utility era-token-ink {
374
- display: block;
375
- align-content: center;
376
- text-align: center;
377
- line-height: 1;
378
- text-box: trim-both cap alphabetic;
379
- }
380
- ```
381
-
382
- ## era-label-ink
383
-
384
- **Use it for:** the ink centring for a fixed-height control holding ONE line of sentence-case text and no icon — a menu item, a nav trigger, a button label. Never alongside `truncate` (the trimmed box becomes the clip).
385
-
386
- LABEL INK — era-token-ink's mixed-case sibling, and the general answer to
387
- "this fixed-height control holds one line of text".
388
-
389
- Same mechanism, same reason: `text-box-trim` needs a BLOCK box, so the row
390
- becomes one and `align-content` (block-container alignment) does the
391
- centring. What differs is the band and the alignment, because the content
392
- class differs — a label is sentence case, so the x-height band is the visual
393
- mass to centre (cap-band reads low on "System"), and it is laid out against
394
- the row's padding rather than centred in it.
395
-
396
- THAT PARENTHESIS AND era-ink-center's "x-band centring parks ~2px high" READ
397
- AS A CONTRADICTION AND ARE NOT. Both are true, of their own example string,
398
- and each was generalised one step too far. Under an ex band the ink offset is
399
-
400
- (xh − ascent + descent) / 2
401
-
402
- which predicts a controlled A/B — one real button, one class set, only the
403
- STRING changing — within 0.3px:
404
-
405
- Save asc 9 desc 0 1.28px high SAVE asc 9 desc 0 1.28px high
406
- Savg asc 9 desc 3 0.22px low savg asc 7 desc 3 1.22px low
407
-
408
- "System" has a descender and reads low under the cap band; "Save" has none
409
- and reads high under the ex band. Case is not the variable — "Save" and
410
- "SAVE" are identical to the hundredth of a pixel. No fixed band centres both
411
- a descender-less and a descender-bearing label, because they differ by ~1.5px
412
- in the same box.
413
-
414
- WHICH IS WHY THE BAND IS FIXED ANYWAY, and this is the part that decides it.
415
- A band gives every label the SAME baseline whatever it spells — measured
416
- spread across Save/Savg/save/savg/SAVE/PDF/Cancel/"Copy page" is 0.00px —
417
- while ink-extent centring would move each label independently and leave a bar
418
- of buttons visibly ragged. A uniform offset the eye cannot see beats a local
419
- misalignment it can. So the offset is accepted, not corrected, and the only
420
- escape is per-label and opt-in (Button's `caps`), which buys optical centring
421
- for a lone label at the cost of ~1.06px against its neighbours.
422
-
423
- AND THE GUARANTEE SPANS BOTH INK PATHS. This utility and era-text-trim are
424
- two mechanisms — block+align-content here, a blockified flex item there — and
425
- every measurement above was taken on the trim path. Measured since: the
426
- text-only path holds the same 0.00px spread AND lands on the same baseline,
427
- 15.22px from the button top at control tier. So an icon button and a text
428
- button put their labels on one line, and `not-has-[svg]` costs nothing but
429
- which element carries the trim.
430
-
431
- Measured by signalnine.org and reproduced here; see label-band.spec.ts, which
432
- pins the 0.00px spread, the cost of breaking it, and the agreement between
433
- the two paths.
434
-
435
- WHY IT EXISTS AT ALL: era's controls are flex, and a flex container's bare
436
- text is an ANONYMOUS flex item — a box no selector reaches and no
437
- non-inherited property touches. So `era-text-trim` on a flex row is inert,
438
- contributing only its `line-height: 1`, and the ink falls back to line-box
439
- centring by font metrics. era-token-ink already fixed that for centred
440
- tokens; this fixes it for everything else.
441
-
442
- THE ICON CASE IS NOT SOLVED, and cannot be from CSS: an icon beside the text
443
- needs the box to stay flex, which re-creates the anonymous item. A component
444
- whose children are an opaque slot therefore guards it —
445
- `not-has-[svg]:era-label-ink` — and ink-centres the text-only case while
446
- leaving the icon case on line-box centring. Splitting icon and label into
447
- separate slots is what would close it; typography/inert-trim reports every
448
- site still on the fallback.
449
-
450
- Declared in `index.css`.
451
-
452
- ```css
453
- @utility era-label-ink {
454
- display: block;
455
- align-content: center;
456
- line-height: 1;
457
- text-box: trim-both ex alphabetic;
458
- }
459
- ```
460
-
461
- ## surface-backdrop
462
-
463
- **Use it for:** the surface-aware backdrop treatment without choosing a fill or relief. Prefer material-popover/material-panel when the element is a complete surface.
464
-
465
- Declared in `index.css`.
466
-
467
- ```css
468
- @utility surface-backdrop {
469
- backdrop-filter: var(--era-backdrop-filter);
470
- -webkit-backdrop-filter: var(--era-backdrop-filter);
471
- }
472
- ```
473
-
474
- ## material-popover
475
-
476
- **Use it for:** a floating transient surface: menus, popovers, tooltips, pickers, toasts. Binds its raised fill, floating relief, and backdrop treatment atomically.
477
-
478
- Declared in `index.css`.
479
-
480
- ```css
481
- @utility material-popover {
482
- background-color: var(--era-surface-bg-elevated);
483
- box-shadow: var(--era-shadow-lg);
484
- backdrop-filter: var(--era-backdrop-filter);
485
- -webkit-backdrop-filter: var(--era-backdrop-filter);
486
- }
487
- ```
488
-
489
- ## material-panel
490
-
491
- **Use it for:** a floating persistent surface: dialogs, sheets, panes, inspectors. Binds its resting fill, floating relief, and backdrop treatment atomically.
492
-
493
- Declared in `index.css`.
494
-
495
- ```css
496
- @utility material-panel {
497
- background-color: var(--era-surface-bg);
498
- box-shadow: var(--era-shadow-lg);
499
- backdrop-filter: var(--era-backdrop-filter);
500
- -webkit-backdrop-filter: var(--era-backdrop-filter);
501
- }
502
- ```
503
-
504
- ## pressable
505
-
506
- **Use it for:** the physical press contract on a control whose direct content plane is marked data-era-press-content. The surface owns relief/travel; the component may scale travel with --era-press-scale.
507
-
508
- Declared in `index.css`.
509
-
510
- ```css
511
- @utility pressable {
512
- &:active {
513
- box-shadow: var(--era-shadow-pressed);
514
- }
515
-
516
- &:active > [data-era-press-content] {
517
- translate: var(--era-press-effective-x, var(--era-press-sink-x))
518
- var(--era-press-effective-y, var(--era-press-sink-y));
519
- }
520
- }
521
- ```
522
-
523
- ## tier-icon · tier-pill · tier-chip · tier-control · tier-bar
524
-
525
- BOUNDED GEOMETRY — height and radius are one decision.
526
-
527
- A tier is a geometric type, not two classes a caller must remember to keep in
528
- sync. `tier-control` establishes h-control AND rd-control; the radius ladder
529
- then guarantees rd_outer − rd_inner = (h_outer − h_inner)/2 for every pair.
530
- Width remains the component's decision: a row stretches, a button adds
531
- min-w-control, and a square control adds w-control.
532
-
533
- Declared in `index.css`.
534
-
535
- ```css
536
- @utility tier-icon {
537
- height: var(--era-h-icon);
538
- border-radius: var(--era-rd-icon);
539
- }
540
-
541
- @utility tier-pill {
542
- height: var(--era-h-pill);
543
- border-radius: var(--era-rd-pill);
544
- }
545
-
546
- @utility tier-chip {
547
- height: var(--era-h-chip);
548
- border-radius: var(--era-rd-chip);
549
- }
550
-
551
- @utility tier-control {
552
- height: var(--era-h-control);
553
- border-radius: var(--era-rd-control);
554
- }
555
-
556
- @utility tier-bar {
557
- height: var(--era-h-bar);
558
- border-radius: var(--era-rd-bar);
559
- }
560
- ```
561
-
562
- ## h-line
563
-
564
- **Use it for:** the height of one line of the element's own text — a skeleton standing in for a line, a spacer that must match a line box.
565
-
566
- One LINE BOX tall — the height of the text this stands in for.
567
-
568
- Skeleton's `line` used h-(--era-text), which is the body FONT SIZE: never the
569
- height of a rendered line (that is font-size x leading), and wrong again by
570
- the ladder's ratio for any rung other than body. `1lh` resolves against the
571
- element's OWN computed line-height, so putting a text-* class on the skeleton
572
- makes it match that tier exactly, at every density, and it follows a leading
573
- change for free.
574
-
575
- Declared in `index.css`.
576
-
577
- ```css
578
- @utility h-line {
579
- height: 1lh;
580
- }
581
- ```
582
-
583
- ## era-track-well
584
-
585
- **Use it for:** instead of `shadow-well` on a track whose value is painted by a child that covers the box — a progress/meter/slider track, a custom gauge. For a well holding ordinary content, `shadow-well` is still right.
586
-
587
- A WELL THAT SURVIVES BEING FILLED.
588
-
589
- `shadow-well` is an INSET shadow, and an inset shadow belongs to the
590
- element's background layer — it paints under the element's own descendants.
591
- That is invisible on a well holding text (a checkbox, a code block: the
592
- chrome runs around the content) and wrong the moment something paints the
593
- whole box. Meter, Progress and the Slider track all do exactly that: an
594
- opaque child, `translateX`-ed to the value. So the well survived only where
595
- the track was EMPTY — a half-full meter on bevel had a chisel to the right
596
- of the value and none to the left, which reads as a hole with a sticker
597
- over it rather than a level inside a well.
598
-
599
- The fix is painting order, not geometry: the same `--era-shadow-well` on a
600
- pseudo-element that is the element's LAST child, so it lands above the fill.
601
- The fill still spans the full box — nothing is inset, no radius is
602
- re-derived, and 0% and 100% keep their exact ends. The `position: relative`
603
- is load-bearing (the overlay resolves against this box) and `inherit` on the
604
- radius keeps it honest under `data-corners="square"`.
605
-
606
- Surface-agnostic on purpose. It reads the same token every well reads, so
607
- glass's hairline rim is restored by the same line that restores bevel's
608
- chisel, and flat — where the token is `none` — renders exactly as before.
609
- Reported against bevel by a consumer; it was never a bevel bug.
610
-
611
- Declared in `index.css`.
612
-
613
- ```css
614
- @utility era-track-well {
615
- position: relative;
616
- &::after {
617
- content: '';
618
- position: absolute;
619
- inset: 0;
620
- border-radius: inherit;
621
- box-shadow: var(--era-shadow-well);
622
- pointer-events: none;
623
- }
624
- }
625
- ```
626
-
627
- ## era-skeleton-pulse
628
-
629
- Skeleton's pulse, on the MOTION AXIS.
630
-
631
- It used to be a literal `2s ease-in-out` on the component, so it neither died
632
- at data-motion="instant" nor honoured prefers-reduced-motion — on the one
633
- component most likely to be on screen in bulk, and animating. That is an
634
- accessibility defect rather than a preference: a page of pulsing blocks is
635
- exactly what someone with a vestibular disorder turns motion off to avoid.
636
-
637
- Same derivation as era-shimmer: 10x the axis duration, so it is a slow pulse
638
- at normal, slower at extra, and at instant the duration resolves to 0s and it
639
- freezes on its base frame — full opacity, a plain filled block.
640
-
641
- Declared in `index.css`.
642
-
643
- ```css
644
- @utility era-skeleton-pulse {
645
- animation: era-skeleton-pulse calc(var(--era-duration) * 10) var(--era-ease) infinite;
646
- }
647
- ```
648
-
649
- ## era-shimmer
650
-
651
- **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.
652
-
653
- In-progress text shimmer — a bright sweep through muted text (the standard
654
- "working…" label treatment in streaming UIs). Timing derives from the motion
655
- axis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the
656
- duration is 0s and the sweep freezes on its base frame — plain muted text —
657
- so reduced-motion and data-motion="instant" both kill it for free.
658
-
659
- Declared in `index.css`.
660
-
661
- ```css
662
- @utility era-shimmer {
663
- background: linear-gradient(
664
- 110deg,
665
- var(--color-muted) 0% 43%,
666
- var(--color-bright) 50%,
667
- var(--color-muted) 57% 100%
668
- );
669
- background-size: 200% 100%;
670
- background-clip: text;
671
- -webkit-text-fill-color: transparent;
672
- animation: era-shimmer calc(var(--era-duration) * 10) cubic-bezier(0.7, 0, 1, 0.4) infinite;
673
- }
674
- ```
675
-
676
- ## era-collapse
677
-
678
- **Use it for:** the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden).
679
-
680
- The open/close animation for a bits-ui Collapsible.Content — the height
681
- sweep every disclosure in the library uses (a Step's body, an error panel's
682
- detail, a swarm node's children).
683
-
684
- It has to be a keyframe animation rather than a transition because the
685
- natural height is only knowable at open time: bits measures the content and
686
- publishes `--bits-collapsible-content-height`, which the `to` frame reads.
687
- The class also owns `overflow: hidden` — animating height without it just
688
- spills the content out of the shrinking box.
689
-
690
- Timing rides the motion axis, so `instant` collapses it to a cut (0s) and
691
- `extra` stretches it, for free. Two components carried a byte-identical copy of
692
- these keyframes under two different names before this existed (a Step's body
693
- and the error panel's detail), and the swarm tree would have made a third; the
694
- panel that needs different motion should override `animation`, not re-declare
695
- it.
696
-
697
- Declared in `index.css`.
698
-
699
- ```css
700
- @utility era-collapse {
701
- overflow: hidden;
702
- &[data-state='open'] {
703
- animation: era-collapse-open var(--era-duration) var(--era-ease);
704
- }
705
- &[data-state='closed'] {
706
- animation: era-collapse-close var(--era-duration) var(--era-ease);
707
- }
708
- }
709
- ```
710
-
711
- ## h-icon · h-pill · h-chip · h-control · h-bar
712
-
713
- **Use it for:** the tier-height ladder — h-icon (icon) · h-pill (nested pill) · h-chip (standalone pill) · h-control (control) · h-bar (bar); the named form of h-(--era-h-*).
714
-
715
- Declared in `index.css`.
716
-
717
- ```css
718
- @utility h-icon {
719
- height: var(--era-h-icon);
720
- }
721
- @utility h-pill {
722
- height: var(--era-h-pill);
723
- }
724
- @utility h-chip {
725
- height: var(--era-h-chip);
726
- }
727
- @utility h-control {
728
- height: var(--era-h-control);
729
- }
730
- @utility h-bar {
731
- height: var(--era-h-bar);
732
- }
733
- ```
734
-
735
- ## min-h-icon · min-h-pill · min-h-chip · min-h-control · min-h-bar
736
-
737
- **Use it for:** minimum tier height for containers that may grow — a bar that is at least lg-tier but can wrap (min-h-bar).
738
-
739
- Declared in `index.css`.
740
-
741
- ```css
742
- @utility min-h-icon {
743
- min-height: var(--era-h-icon);
744
- }
745
- @utility min-h-pill {
746
- min-height: var(--era-h-pill);
747
- }
748
- @utility min-h-chip {
749
- min-height: var(--era-h-chip);
750
- }
751
- @utility min-h-control {
752
- min-height: var(--era-h-control);
753
- }
754
- @utility min-h-bar {
755
- min-height: var(--era-h-bar);
756
- }
757
- ```
758
-
759
- ## w-icon · w-pill · w-chip · w-control · w-bar
760
-
761
- **Use it for:** a tier-sized WIDTH — the horizontal twin of the h-* ladder, for a rail or a column measured in tiers rather than in content.
762
-
763
- Declared in `index.css`.
764
-
765
- ```css
766
- @utility w-icon {
767
- width: var(--era-h-icon);
768
- }
769
- @utility w-pill {
770
- width: var(--era-h-pill);
771
- }
772
- @utility w-chip {
773
- width: var(--era-h-chip);
774
- }
775
- @utility w-control {
776
- width: var(--era-h-control);
777
- }
778
- @utility w-bar {
779
- width: var(--era-h-bar);
780
- }
781
- ```
782
-
783
- ## min-w-icon · min-w-pill · min-w-chip · min-w-control · min-w-bar
784
-
785
- **Use it for:** a minimum tier WIDTH — the floor that makes a single-glyph control render as a square rather than a skinny pill (min-w-control on an icon button, min-w-pill on a badge). These could not exist before the ladder was renamed: the old rungs were sm/md/lg, and Tailwind's container scale already owns min-w-md and min-w-lg, so the names collided and every call site had to write min-w-(--era-h-control) by hand. icon/pill/chip/control/bar collide with nothing.
786
-
787
- Declared in `index.css`.
788
-
789
- ```css
790
- @utility min-w-icon {
791
- min-width: var(--era-h-icon);
792
- }
793
- @utility min-w-pill {
794
- min-width: var(--era-h-pill);
795
- }
796
- @utility min-w-chip {
797
- min-width: var(--era-h-chip);
798
- }
799
- @utility min-w-control {
800
- min-width: var(--era-h-control);
801
- }
802
- @utility min-w-bar {
803
- min-width: var(--era-h-bar);
804
- }
805
- ```
806
-
807
- ## size-icon · size-pill · size-chip · size-control · size-bar
808
-
809
- **Use it for:** a tier-sized square — size-icon is THE icon size (a Lucide glyph beside body text), size-control a square control face.
810
-
811
- Declared in `index.css`.
812
-
813
- ```css
814
- @utility size-icon {
815
- width: var(--era-h-icon);
816
- height: var(--era-h-icon);
817
- }
818
- @utility size-pill {
819
- width: var(--era-h-pill);
820
- height: var(--era-h-pill);
821
- }
822
- @utility size-chip {
823
- width: var(--era-h-chip);
824
- height: var(--era-h-chip);
825
- }
826
- @utility size-control {
827
- width: var(--era-h-control);
828
- height: var(--era-h-control);
829
- }
830
- @utility size-bar {
831
- width: var(--era-h-bar);
832
- height: var(--era-h-bar);
833
- }
834
- ```
835
-
836
- ## px-pill · px-chip · pl-pill · pr-pill · pl-chip · pr-chip
837
-
838
- **Use it for:** pill side padding — the wider-than-vertical horizontal breathing room badge/chip text wants (glyph-advance derived, ~2× the vertical inset).
839
-
840
- Declared in `index.css`.
841
-
842
- ```css
843
- @utility px-pill {
844
- padding-inline: var(--era-pill-px);
845
- }
846
- @utility px-chip {
847
- padding-inline: var(--era-chip-px);
848
- }
849
- @utility pl-pill {
850
- padding-left: var(--era-pill-px);
851
- }
852
- @utility pr-pill {
853
- padding-right: var(--era-pill-px);
854
- }
855
- @utility pl-chip {
856
- padding-left: var(--era-chip-px);
857
- }
858
- @utility pr-chip {
859
- padding-right: var(--era-chip-px);
860
- }
861
- ```
862
-
863
- ## p-section
864
-
865
- **Use it for:** padding for a content panel that sits UNDER a section header row — the even-padding exception, and the reason it is not called p-section: it deliberately under-pays the block start. For an evenly padded content panel use p-section, which is the plain token.
866
-
867
- A content panel always follows a section header ROW, and that row already
868
- contributes its own slack: the heading's text sits in an h-bar bar, so there
869
- is (h-bar − text)/2 of empty bar under its glyphs before the panel even
870
- starts. Padding the panel evenly on top of that spends the space twice — the
871
- header ends up floating between the section above and the paragraph it
872
- introduces rather than binding to it.
873
-
874
- So the block-start pays only the REMAINDER. The target is the indent: the
875
- space under a heading should measure the same as the space beside the text,
876
- which is --era-pad-content. The header row already supplies (h-bar − text)/2 of
877
- it, so the panel adds what's left, and heading-to-paragraph reads equal to
878
- the paragraph's own inset. Subtracting one --era-gap (the previous rule) left
879
- the header's slack unaccounted for and the heading still detached.
880
-
881
- Both terms are density-derived, so the equality holds at every mode; the
882
- max() is the floor for a hypothetical mode where the row's slack would
883
- exceed the inset outright.
884
-
885
- Declared in `index.css`.
886
-
887
- ```css
888
- @utility p-section {
889
- padding: var(--era-pad-content);
890
- padding-top: max(0px, calc(var(--era-pad-content) - (var(--era-h-bar) - var(--era-text)) / 2));
891
- }
892
- ```
893
-
894
- ## duration-base
895
-
896
- **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".
897
-
898
- Declared in `index.css`.
899
-
900
- ```css
901
- @utility duration-base {
902
- transition-duration: var(--era-duration);
903
- }
904
- ```
905
-
906
- ## text-micro · text-lede · text-heading · text-title · text-display
907
-
908
- **Use it for:** the type ladder — micro (eyebrow/legend) < body (controls) < lede (the paragraph under a title) < heading < title < display. Derived from --era-text, so the ratio between any two rungs holds at every density; leading and tracking ride with the size, so a rung is one class.
909
-
910
- THE TYPE LADDER — the one axis that was not on the density system.
911
-
912
- era had exactly one size, `text-body`, so a heading had nowhere to go and
913
- consumers reached for Tailwind's fixed `text-2xl` or a bare <h1>. Both are
914
- rem-fixed while `--era-text` re-tiers, so heading prominence DEGRADED as
915
- density increased: measured in a consumer app, an <h1> against body text
916
- was 1.33x at dense and 1.14x at touch. The page title gets quieter the
917
- tighter you set the page, which is backwards, and it is the only place in
918
- era where a size relationship does not hold across the axis.
919
-
920
- Named by ROLE, like the tier ladder and for the same reason: size names
921
- invert (this library already shipped an `xxs` bigger than its `xs`), and a
922
- role tells you where the rung belongs. Ratios are simple fractions of the
923
- atom rather than a geometric scale — again like the tier ladder, whose
924
- rungs are sp+6, 2sp+6, 3sp, 4sp rather than a constant multiplier. They
925
- land on whole pixels at dense and hold their ratio at every mode.
926
-
927
- micro 5/6 10 / 11.7 / 11.7 / 11.7 an eyebrow, a legend, a tag
928
- body 1 12 / 14 / 14 / 14 control and UI text
929
- lede 5/4 15 / 17.5 the paragraph under a title
930
- heading 3/2 18 / 21 a section heading
931
- title 2 24 / 28 a page or card title
932
- display 3 36 / 42 a hero
933
-
934
- `micro` is not invented: the library hand-writes text-[0.625rem] ten times
935
- for exactly this role, and 5/6 of dense's 12px is that value.
936
-
937
- Leading and tracking ride with the size, so a rung is one class. Both
938
- tighten as the rung grows, which is ordinary optical setting and matters
939
- more here than usual — era's default stack is monospace, whose display
940
- sizes read loose without it.
941
-
942
-
943
- body and micro deliberately carry NO line-height: `text-body` is worn by
944
- every control in the library, where leading is owned by era-text-trim and
945
- leading-none. Giving it a default would re-space every control in era.
946
- Use `leading-body` for running text, as before.
947
-
948
- Declared in `index.css`.
949
-
950
- ```css
951
- @theme inline {
952
- --text-micro: calc(var(--era-text) * 5 / 6);
953
- --text-micro--letter-spacing: 0.04em;
954
- --text-lede: calc(var(--era-text) * 5 / 4);
955
- --text-lede--line-height: 1.5;
956
- --text-heading: calc(var(--era-text) * 3 / 2);
957
- --text-heading--line-height: 1.3;
958
- --text-heading--letter-spacing: -0.01em;
959
- --text-title: calc(var(--era-text) * 2);
960
- --text-title--line-height: 1.15;
961
- --text-title--letter-spacing: -0.02em;
962
- --text-display: calc(var(--era-text) * 3);
963
- --text-display--line-height: 1.05;
964
- --text-display--letter-spacing: -0.03em;
965
- }
966
- ```
967
-
968
- ## text-body
969
-
970
- Density-derived body size: `text-body` replaces text-[length:var(--era-text)]
971
-
972
- Declared in `index.css`.
973
-
974
- ```css
975
- @theme inline {
976
- --text-body: var(--era-text);
977
- }
978
- ```
979
-
980
- ## leading-body
981
-
982
- Prose leading — `leading-body` is the pair to `text-body`, and every piece
983
- of text in the library that can wrap to a second line carries it. Enforced
984
- at runtime by typography/prose-leading.
985
-
986
- Declared in `index.css`.
987
-
988
- ```css
989
- @theme inline {
990
- --leading-body: var(--era-leading);
991
- }
992
- ```
993
-
994
- ## rounded-icon · rounded-pill · rounded-chip · rounded-control · rounded-bar · rounded-menu-item
995
-
996
- Radius ladder — Tailwind's own rounded-* names now walk era's concentric
997
- ladder (rd-T = rd-xs + (h-T − h-icon)/2), ride the corners toggle, and
998
- collapse to 0 on square. rounded-menu-item is the menu-row radius
999
- (rd-md − panel-inset) that keeps rows concentric inside a rounded-control
1000
- panel on every surface.
1001
-
1002
- Declared in `index.css`.
1003
-
1004
- ```css
1005
- @theme inline {
1006
- --radius-icon: var(--era-rd-icon);
1007
- --radius-pill: var(--era-rd-pill);
1008
- --radius-chip: var(--era-rd-chip);
1009
- --radius-control: var(--era-rd-control);
1010
- --radius-bar: var(--era-rd-bar);
1011
- --radius-menu-item: var(--era-item-rd);
1012
- }
1013
- ```
1014
-
1015
- ## shadow
1016
-
1017
- Surface chrome — bare `shadow` is the resting control edge, shadow-lg the
1018
- floating panel, shadow-well the recessed container, shadow-pressed the
1019
- active/latched face, shadow-highlight the material half of hover feedback.
1020
- Flat renders them as none, bevel as chisels, glass as frosted rims — the
1021
- class stays the same.
1022
-
1023
- Declared in `index.css`.
1024
-
1025
- ```css
1026
- @theme inline {
1027
- --shadow: var(--era-shadow);
1028
- }
1029
- ```
1030
-
1031
- ## shadow-sm · shadow-lg · shadow-well · shadow-pressed · shadow-highlight
1032
-
1033
- v4 marks bare `shadow` a deprecated spelling of shadow-sm, and lints
1034
- auto-fix one to the other — alias both to the control edge so either
1035
- spelling renders era chrome.
1036
-
1037
- Declared in `index.css`.
1038
-
1039
- ```css
1040
- @theme inline {
1041
- --shadow-sm: var(--era-shadow);
1042
- --shadow-lg: var(--era-shadow-lg);
1043
- --shadow-well: var(--era-shadow-well);
1044
- --shadow-pressed: var(--era-shadow-pressed);
1045
- --shadow-highlight: var(--era-highlight-shadow);
1046
- }
1047
- ```
1048
-
1049
- ## bg-well · text-well · border-well · bg-elevated · text-elevated · border-elevated · bg-highlight · text-highlight · … (12 in all)
1050
-
1051
- Surface fills — bg-well is the surface's own resting fill (pairs with
1052
- shadow-well for recessed chrome), bg-elevated the raised tier,
1053
- bg-highlight the transient
1054
- hover/focus/keyboard wash (pairs with shadow-highlight; latched states
1055
- stay on opaque bg-hover), bg-overlay the modal scrim.
1056
-
1057
- Declared in `index.css`.
1058
-
1059
- ```css
1060
- @theme inline {
1061
- --color-well: var(--era-surface-bg);
1062
- --color-elevated: var(--era-surface-bg-elevated);
1063
- --color-highlight: var(--era-highlight);
1064
- --color-overlay: var(--era-overlay-bg);
1065
- }
1066
- ```
1067
-
1068
- ## p-gutter · px-gutter · py-gutter · gap-gutter · p-panel · px-panel · py-panel · gap-panel · … (24 in all)
1069
-
1070
- THE SPACING SCALE — one entry per token, from which Tailwind derives the
1071
- WHOLE directional family: p/px/py/pt/pr/pb/pl/ps/pe, every margin, gap and
1072
- gap-x/y, inset/top/right/bottom/left, translate, and the sizing prefixes.
1073
-
1074
- These were hand-written `@utility` blocks, which could only ever cover the
1075
- spellings someone remembered to write: `px-gutter` existed and `mt-gutter`
1076
- did not, `p-inset-control` existed and `pl-inset-control` did not, so the
1077
- library fell back to `mt-(--era-gap)` in exactly the places the named API
1078
- was supposed to cover. One line here replaces twenty blocks and leaves no
1079
- spelling missing.
1080
-
1081
- ONLY genuine spacing belongs here. The namespace is shared across
1082
- h/w/p/m/gap, so a name registered here means the same length in ALL of
1083
- them — true of a gap or an inset, false of a TIER HEIGHT. Tiers stay
1084
- hand-written per prefix below, and the two vocabularies deliberately share
1085
- no word: icon/pill/chip/control/bar are heights; gutter, card, panel,
1086
- content, menu, field and the inset-* names are spacing. Registering a tier
1087
- name here would silently make `px-control` mean 24px of side padding.
1088
-
1089
- Declared in `index.css`.
1090
-
1091
- ```css
1092
- @theme inline {
1093
- --spacing-gutter: var(--era-gap);
1094
- --spacing-panel: var(--era-pad-panel);
1095
- --spacing-card: var(--era-pad-card);
1096
- --spacing-content: var(--era-pad-content);
1097
- --spacing-menu: var(--era-panel-inset);
1098
- --spacing-field: var(--era-field-px);
1099
- }
1100
- ```
1101
-
1102
- ## p-inset-pill · px-inset-pill · py-inset-pill · gap-inset-pill · p-inset-chip · px-inset-chip · py-inset-chip · gap-inset-chip · … (12 in all)
1103
-
1104
- **Use it for:** text-symmetric content padding — (tier − text)/2, the inset that vertically centres body text in its tier; px-inset-control is a text button's side padding, p-inset-control a code block's.
1105
-
1106
- Declared in `index.css`.
1107
-
1108
- ```css
1109
- @theme inline {
1110
- --spacing-inset-pill: var(--era-inset-pill);
1111
- --spacing-inset-chip: var(--era-inset-chip);
1112
- --spacing-inset-control: var(--era-inset-control);
1113
- }
1114
- ```
1115
-
1116
- ## p-icon-inset-pill · px-icon-inset-pill · py-icon-inset-pill · gap-icon-inset-pill · p-icon-inset-chip · px-icon-inset-chip · py-icon-inset-chip · gap-icon-inset-chip · … (40 in all)
1117
-
1118
- **Use it for:** the concentric inset for a nested pair — (h-container − h-child)/2, the gap that makes a child control sit evenly inside its container, and the radius step that keeps their corners concentric. Named for the pair it joins: gap-icon-inset-control seats an icon in a control.
1119
-
1120
- Declared in `index.css`.
1121
-
1122
- ```css
1123
- @theme inline {
1124
- --spacing-icon-inset-pill: var(--era-icon-inset-pill);
1125
- --spacing-icon-inset-chip: var(--era-icon-inset-chip);
1126
- --spacing-icon-inset-control: var(--era-icon-inset-control);
1127
- --spacing-icon-inset-bar: var(--era-icon-inset-bar);
1128
- --spacing-pill-inset-chip: var(--era-pill-inset-chip);
1129
- --spacing-pill-inset-control: var(--era-pill-inset-control);
1130
- --spacing-pill-inset-bar: var(--era-pill-inset-bar);
1131
- --spacing-chip-inset-control: var(--era-chip-inset-control);
1132
- --spacing-chip-inset-bar: var(--era-chip-inset-bar);
1133
- --spacing-control-inset-bar: var(--era-control-inset-bar);
1134
- }
1135
- ```
1136
-
1137
- ## p-glyph-icon · px-glyph-icon · py-glyph-icon · gap-glyph-icon · p-glyph-pill · px-glyph-pill · py-glyph-pill · gap-glyph-pill
1138
-
1139
- **Use it for:** the glyph size for a tier — the INK inside the box, not the box (size-glyph-pill is the dismiss × in a pill, size-pill is the pill).
1140
-
1141
- Declared in `index.css`.
1142
-
1143
- ```css
1144
- @theme inline {
1145
- --spacing-glyph-icon: var(--era-glyph-icon);
1146
- --spacing-glyph-pill: var(--era-glyph-pill);
1147
- }
1148
- ```
1149
-
1150
- ## bg-field · text-field · border-field · bg-field-highlight · text-field-highlight · border-field-highlight
1151
-
1152
- **Use it for:** the recessed field fill — bg-field is a text input's resting well, bg-field-highlight its focused/hovered face.
1153
-
1154
- Declared in `index.css`.
1155
-
1156
- ```css
1157
- @theme inline {
1158
- --color-field: var(--era-field-bg);
1159
- --color-field-highlight: var(--era-field-highlight);
1160
- }
1161
- ```
1162
-
1163
- ## ease-base
1164
-
1165
- Motion — bare transition utilities default to the motion axis, so
1166
- `transition-colors` alone is on-design (0s at instant, longer at extra).
1167
- ease-base/duration-base name the axis explicitly for overrides.
1168
-
1169
- Declared in `index.css`.
1170
-
1171
- ```css
1172
- @theme inline {
1173
- --default-transition-duration: var(--era-duration);
1174
- --default-transition-timing-function: var(--era-ease);
1175
- --ease-base: var(--era-ease);
1176
- }
1177
- ```
1178
-
1179
- ## bg-divider · text-divider · border-divider · bg-divider-faded · text-divider-faded · border-divider-faded · bg-background · text-background · … (66 in all)
1180
-
1181
- Surface divider colors: border-divider, bg-divider-faded, …
1182
-
1183
- Declared in `index.css`.
1184
-
1185
- ```css
1186
- @theme inline {
1187
- --color-divider: var(--era-divider-color);
1188
- --color-divider-faded: var(--era-divider-faded);
1189
- --color-background: var(--color-1);
1190
- --color-foreground: var(--color-fg);
1191
- --color-1: var(--color-1);
1192
- --color-2: var(--color-2);
1193
- --color-3: var(--color-3);
1194
- --color-4: var(--color-4);
1195
- --color-5: var(--color-5);
1196
- --color-6: var(--color-6);
1197
- --color-7: var(--color-7);
1198
- --color-8: var(--color-8);
1199
- --color-9: var(--color-9);
1200
- --color-10: var(--color-10);
1201
- --color-11: var(--color-11);
1202
- --color-12: var(--color-12);
1203
- --color-border: var(--color-border);
1204
- --color-surface: var(--color-surface);
1205
- --color-hover: var(--color-hover);
1206
- --color-fg: var(--color-fg);
1207
- --color-bright: var(--color-bright);
1208
- --color-muted: var(--color-muted);
1209
- }
1210
- ```
1211
-
1212
- ## bg-fill · text-fill · border-fill · bg-link · text-link · border-link · bg-primary · text-primary · … (39 in all)
1213
-
1214
- The standing pill fill — --era-fill, the latched counterpart of the
1215
- transient highlight. Surface-owned: a composing wash on flat/bevel, the
1216
- opaque swatch on glass.
1217
-
1218
- Declared in `index.css`.
1219
-
1220
- ```css
1221
- @theme inline {
1222
- --color-fill: var(--era-fill);
1223
- --color-link: var(--color-link);
1224
- --color-primary: var(--color-primary);
1225
- --color-primary-fg: var(--color-primary-fg);
1226
- --color-primary-hover: var(--color-primary-hover);
1227
- --color-destructive: var(--color-destructive);
1228
- --color-destructive-fg: var(--color-destructive-fg);
1229
- --color-success: var(--color-success);
1230
- --color-success-fg: var(--color-success-fg);
1231
- --color-warning: var(--color-warning);
1232
- --color-warning-fg: var(--color-warning-fg);
1233
- --color-info: var(--color-info);
1234
- --color-info-fg: var(--color-info-fg);
1235
- }
1236
- ```