@signal9/era-ui 4.16.6 → 4.16.8

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.
@@ -36,9 +36,9 @@ Classes, not components — they need no import beyond the stylesheet. Reach for
36
36
  - `era-interactive` — the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.
37
37
  - `era-link` — understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text).
38
38
  - `scrollbar-none` — hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.
39
- - `era-text-trim` — vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings.
40
- - `truncate` — single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders.
41
- - `era-text-trim-caps` — the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
39
+ - `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
+ - `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
+ - `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
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.
43
43
  - `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
44
  - `era-collapse` — the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden).
@@ -145,9 +145,9 @@ enough to stay var-form — `min-w-(--era-h-md)` etc. remain the escape hatch.
145
145
  | `era-interactive` | the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants. |
146
146
  | `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). |
147
147
  | `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. |
148
- | `era-text-trim` | vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings. |
149
- | `truncate` | single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders. |
150
- | `era-text-trim-caps` | the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text. |
148
+ | `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
+ | `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
+ | `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
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. |
152
152
  | `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
153
  | `era-collapse` | the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden). |
@@ -205,16 +205,23 @@ Declared in `index.css`.
205
205
  @utility era-link {
206
206
  /* The text is bright at rest and never changes — the link reads as a link
207
207
  * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —
208
- * muted knocked back to 30% so it barely whispers — that resolves to the full
209
- * text colour on hover. */
208
+ * the link's own ink knocked back to 30% so it barely whispers — that
209
+ * resolves to the full ink on hover.
210
+ *
211
+ * currentColor, not --color-muted/--color-fg: the underline belongs to the
212
+ * TEXT it underlines. On themes whose link ink is a colour (signalnine's
213
+ * navy, an accent-tinted bright), the neutral tokens drew a whitish line
214
+ * under coloured text — a second hue where there should be one. Derived
215
+ * from currentColor, the underline tracks whatever the link resolves to,
216
+ * in every theme, including a caller overriding `color` locally. */
210
217
  color: var(--color-link);
211
218
  text-decoration-line: underline;
212
- text-decoration-color: color-mix(in oklch, var(--color-muted) 30%, transparent);
219
+ text-decoration-color: color-mix(in oklch, currentColor 30%, transparent);
213
220
  text-underline-offset: 0.2em;
214
221
  transition: text-decoration-color var(--era-duration) var(--era-ease);
215
222
  &:hover,
216
223
  &:focus-visible {
217
- text-decoration-color: var(--color-fg);
224
+ text-decoration-color: currentColor;
218
225
  }
219
226
  }
220
227
  ```
@@ -239,7 +246,7 @@ Declared in `index.css`.
239
246
 
240
247
  ## era-text-trim
241
248
 
242
- **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; see era-text-trim-caps for ALL-CAPS strings.
249
+ **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.
243
250
 
244
251
  Font-agnostic vertical ink-centering for single-line control text.
245
252
  Measured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):
@@ -286,7 +293,7 @@ Declared in `index.css`.
286
293
 
287
294
  ## truncate
288
295
 
289
- **Use it for:** single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders.
296
+ **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.
290
297
 
291
298
  Tailwind's `truncate`, with the vertical clip fixed at the source.
292
299
 
@@ -319,7 +326,7 @@ Declared in `index.css`.
319
326
 
320
327
  ## era-text-trim-caps
321
328
 
322
- **Use it for:** the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
329
+ **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`.
323
330
 
324
331
  ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
325
332
  misjudges caps-only strings — cap/alphabetic edges are the right box.
@@ -1062,8 +1069,9 @@ assign to move the pane programmatically (e.g. reset to 0,0). |
1062
1069
  | `title?` | `string` | — | Text shown in the handle bar. For richer chrome use `header`. |
1063
1070
  | `icon?` | `Component<IconProps> \| null` | `AppPane` | Lucide icon rendered left of the title. Pass `null` to omit. |
1064
1071
  | `disabled?` | `boolean` | `false` | Disables dragging (reactive). |
1065
- | `plugins?` | `(Plugin \| Compartment)[] \| (() => (Plugin \| Compartment)[])` | `[]` | Extra neodrag plugins (axis, bounds, grid, threshold, …) appended
1066
- after the pane's own. Pass Compartments for reactive plugin sets. |
1072
+ | `plugins?` | `DragPlugin[] \| (() => DragPlugin[])` | `[]` | Tier-2 neodrag plugins (`use`) appended to the drag. Tier-1 concerns
1073
+ (axis/bounds/grid/threshold) are Pane.Root props spread through
1074
+ restProps or use Root directly for those. |
1067
1075
  | `onDragStart?` | `(data: DragEventData) => void` | — | — |
1068
1076
  | `onDrag?` | `(data: DragEventData) => void` | — | — |
1069
1077
  | `onDragEnd?` | `(data: DragEventData) => void` | — | — |
@@ -2,6 +2,36 @@
2
2
 
3
3
  > A Svelte 5 + Bits UI component library. Every surface — heights, paddings, radii, gaps — derives from a single spacing atom, so density is a one-attribute override at any scope.
4
4
 
5
+ ## Sizing philosophy — read this before writing markup
6
+
7
+ Every measurement in era derives from one atom (`--era-sp`) through a fixed ladder. You never pick a number; you pick a TIER, and the number follows from the density axis. Markup that hardcodes `h-[34px]`, `text-[13px]`, `p-2` or `gap-1.5` is wrong even when it looks right at the default density — it stops tracking the axis the moment a subtree re-tiers.
8
+
9
+ **1. Height comes from the tier ladder, never from a pixel value.**
10
+ `h-xs` (icon, 14px @dense) · `h-xxs` (nested pill, 18) · `h-sm` (standalone pill, 22) · `h-md` (control/row, 24) · `h-lg` (bar/header, 32). A square is `size-*` on the same ladder; `size-xs` IS the icon size beside body text.
11
+
12
+ **2. Padding is geometric or optical — the container tells you which.**
13
+ A container with a BOUNDED tier height (bar, row, trigger, pill) takes geometric padding: `px-inset-md` / `px-sm` / `px-pill-sm`, all derived from `(tier − text)/2`. A container with UNBOUNDED height (card, dialog, panel, page section) takes optical padding: `p-panel` · `p-card` · `p-content`, which grow super-linearly so whitespace keeps pace as controls loosen. Never pad a card with a tier inset, and never pad a row with `p-card`.
14
+
15
+ **3. The even-gap law.** Inside any container, `(container_height − child_height) / 2` must equal the gap on ALL four sides. A `h-xxs` control in a `h-md` bar sits on a 3px wall at dense — so the bar's padding AND the gap between its children are that same value. `gap-gutter` / `p-gutter` is the standard inter-element gap (sp/2); the concentric insets (`p-inset-*`) are the text-symmetric ones.
16
+
17
+ **4. Radius follows the corner REGION's tier, not the box's size.**
18
+ `rounded-xs · xxs · sm · md · lg` walk the same ladder as the heights, and the ladder is built so that `rd_outer − rd_inner` equals the concentric gap — nested corners then share a centre of curvature automatically. A big panel whose corner is formed by an `h-md` bar takes `rounded-md`, not `rounded-lg`. Never hardcode a radius: `data-corners="square"` collapses the tokens to 0 and a literal value would survive it.
19
+
20
+ **5. Single-line control text carries `era-text-trim`; wrapping text carries `leading-body`.**
21
+ Flexbox centres the LINE BOX, not the ink, and misses by up to ~2px. `era-text-trim` fixes it — on FIXED-HEIGHT, NON-EDITABLE elements only. Two hard prohibitions, both of which produce silently chopped or mis-centred glyphs:
22
+ - NEVER on an `<input>`/`<textarea>`/`<select>` (the UA clips to the trimmed inner editor box, so typed text loses its ascenders — a "d" renders as an "o"). A tier-height field centres its own text; if you need the optical lift, that is `era-ink-center`.
23
+ - NEVER on the same element as `truncate` or any overflow clip. Put the trim on the fixed-height ROW and let the truncating CHILD centre inside it via `items-center`. (era guards this pairing so it degrades to a no-op instead of chopped text, but the row/child split is the correct authoring.)
24
+
25
+ Text that WRAPS is the opposite regime: it needs leading, so it carries `leading-body` (never a trim). `era-text-trim-caps` is for ALL-CAPS strings only — on mixed case it reads high.
26
+
27
+ **6. Type size is `text-body`.** It is the density-derived body/control size (12px at dense, 14px above). Reach for a Tailwind type step (`text-sm`, `text-xs`) only for genuine display type, never for control or body text.
28
+
29
+ **7. Colour comes from semantic tokens, not the ramp.** `text-fg` (controls/labels) · `text-bright` (emphasis, headings) · `text-muted` (body copy) · `text-destructive` / `-success` / `-warning`. Fills: `bg-hover` for a latched ROW, `bg-fill` for a standing pill, `bg-highlight` for transient hover. These compose across themes; a raw scale step does not.
30
+
31
+ **Verifying your own output.** era ships a runtime audit that measures these laws against rendered DOM — tier conformance, even gaps, concentric radii, chopped glyphs, wrapping text at control leading. If you are generating markup, render it and run the audit rather than eyeballing: it is the same instrument the library's own test suite asserts against.
32
+
33
+ Full derivations: `{{ORIGIN}}/spacing.md` (the ladder and every token), `{{ORIGIN}}/text.md` (type and trim), `{{ORIGIN}}/utilities.md` (every class, what it emits, when to use it).
34
+
5
35
  ## Using the library
6
36
 
7
37
  ```ts
@@ -40,9 +70,9 @@ Classes, not components — they need no import beyond the stylesheet. Reach for
40
70
  - `era-interactive` — the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants.
41
71
  - `era-link` — understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant="link"` (which is a control, not running text).
42
72
  - `scrollbar-none` — hide the scrollbar on an overflowing strip or rail while keeping it scrollable — for a scroll region that should show its bar, use ScrollArea.
43
- - `era-text-trim` — vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings.
44
- - `truncate` — single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders.
45
- - `era-text-trim-caps` — the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
73
+ - `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.
74
+ - `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.
75
+ - `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`.
46
76
  - `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
77
  - `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.
48
78
  - `era-collapse` — the height sweep for a bits-ui Collapsible.Content — put it on the Content element itself (it carries its own overflow: hidden).
@@ -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": 5821,
47
+ "tokenEstimate": 6097,
48
48
  "sections": [
49
49
  "Overview",
50
50
  "era-interactive",
@@ -195,7 +195,7 @@
195
195
  "slug": "pane",
196
196
  "title": "Pane",
197
197
  "summary": "A draggable floating window with a handle bar.",
198
- "tokenEstimate": 441,
198
+ "tokenEstimate": 446,
199
199
  "sections": [
200
200
  "Import",
201
201
  "Props"
@@ -980,7 +980,7 @@
980
980
  "file": "index.css",
981
981
  "useCase": "understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant=\"link\"` (which is a control, not running text).",
982
982
  "description": "Inline text link. An understated, permanent underline that darkens from\nmuted to the text colour on hover — no layout shift, no appearing/vanishing\nunderline. The colour fade rides the MOTION axis (--era-duration/-ease), so\nit snaps at data-motion=\"instant\", eases at \"normal\", and sweeps longer at\n\"extra\" — matching every other era transition. Font-agnostic: it styles\ndecoration + colour only, so it reads correctly in mono, sans, or serif, and\nthe 0.2em offset scales with the type size. Keyboard focus mirrors hover so\nthe affordance is reachable without a pointer.",
983
- "css": "@utility era-link {\n\t/* The text is bright at rest and never changes — the link reads as a link\n\t * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —\n\t * muted knocked back to 30% so it barely whispers — that resolves to the full\n\t * text colour on hover. */\n\tcolor: var(--color-link);\n\ttext-decoration-line: underline;\n\ttext-decoration-color: color-mix(in oklch, var(--color-muted) 30%, transparent);\n\ttext-underline-offset: 0.2em;\n\ttransition: text-decoration-color var(--era-duration) var(--era-ease);\n\t&:hover,\n\t&:focus-visible {\n\t\ttext-decoration-color: var(--color-fg);\n\t}\n}"
983
+ "css": "@utility era-link {\n\t/* The text is bright at rest and never changes — the link reads as a link\n\t * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —\n\t * the link's own ink knocked back to 30% so it barely whispers — that\n\t * resolves to the full ink on hover.\n\t *\n\t * currentColor, not --color-muted/--color-fg: the underline belongs to the\n\t * TEXT it underlines. On themes whose link ink is a colour (signalnine's\n\t * navy, an accent-tinted bright), the neutral tokens drew a whitish line\n\t * under coloured text — a second hue where there should be one. Derived\n\t * from currentColor, the underline tracks whatever the link resolves to,\n\t * in every theme, including a caller overriding `color` locally. */\n\tcolor: var(--color-link);\n\ttext-decoration-line: underline;\n\ttext-decoration-color: color-mix(in oklch, currentColor 30%, transparent);\n\ttext-underline-offset: 0.2em;\n\ttransition: text-decoration-color var(--era-duration) var(--era-ease);\n\t&:hover,\n\t&:focus-visible {\n\t\ttext-decoration-color: currentColor;\n\t}\n}"
984
984
  },
985
985
  {
986
986
  "name": "scrollbar-none",
@@ -998,7 +998,7 @@
998
998
  "era-text-trim"
999
999
  ],
1000
1000
  "file": "index.css",
1001
- "useCase": "vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings.",
1001
+ "useCase": "vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge. Fixed-height, non-editable elements ONLY: 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.",
1002
1002
  "description": "Font-agnostic vertical ink-centering for single-line control text.\nMeasured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):\nthe offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in\na 24px row places ink on a half pixel, ~1px error) and font ascent/descent\nasymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and\nis the Firefox fallback; text-box ex/alphabetic fixes the second by\ncentering the x-height band — the visual mass of MIXED-CASE text (caps and\ndescenders cancel above/below). cap alphabetic is the spec-example recipe\nbut is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.\nWhere text-box is supported it fully determines the box, so the two\ndeclarations never fight. Worst-case residual: 0.44px (measured at 14px; the\nmechanism is font-relative, so dense's 12px inherits the same guarantee with\na proportionally smaller residual).\n\nThe hard rules concern overflow clipping, and there are TWO of them.\n\n1. NEVER on a NATIVE EDITABLE — <input>, <textarea>, <select>. These clip to\n a UA-owned inner editor box that IS the trimmed line box, so a fixed\n height does NOT hold it open: whatever you type gets its ascenders and\n descenders sliced off (a \"d\" renders as an \"o\"). This rule used to read\n the other way round — it claimed a fixed-height <input> was the trim's\n intended home — and the Input component shipped chopped text as a result.\n A tier-height input centres its own text; it needs no trim.\n\n2. NEVER on an AUTO-height element that clips — a `truncate` child, an\n auto-height box with overflow-hidden. With no height to hold it open the\n box collapses to the x-height band (~7.5px at 14px) and the clip chops the\n glyphs into a middle stripe. Put the trim on the fixed-height ROW instead\n and let the child centre via the row's items-center.\n\nA fixed-height NON-editable element is the safe home: the ink overflows the\ntrimmed LINE box but stays inside the padding box, which is what actually\nclips. Both rules are enforced at runtime by typography/collapsed-text-trim.",
1003
1003
  "css": "@utility era-text-trim {\n\tline-height: 1;\n\ttext-box: trim-both ex alphabetic;\n}"
1004
1004
  },
@@ -1008,7 +1008,7 @@
1008
1008
  "truncate"
1009
1009
  ],
1010
1010
  "file": "index.css",
1011
- "useCase": "single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders.",
1011
+ "useCase": "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.",
1012
1012
  "description": "Tailwind's `truncate`, with the vertical clip fixed at the source.\n\ntruncate is overflow:hidden, and overflow clips in BOTH axes — so whatever\nline box the element ends up with is also its ink clip. Era rows put\nera-text-trim on the fixed-height row (a Bar, a list row), and its\n`line-height: 1` INHERITS into the truncating child, which then has no fixed\nheight of its own: the child's box collapses to 1em while the font's real ink\nextent is ~1.15-1.2em, and the descenders (the \"g\" hook, the \"y\" tail) get\nsliced off at the box edge. Same bug on any `leading-none` + `truncate` pair.\n\nThe fix belongs here rather than at each call site: a horizontally-truncating\nelement must never be the thing that clips vertically. `line-height: normal`\nrestores the font's own ascent+descent box — exactly the ink extent — and\n`text-box: normal` cancels a trim if one is set alongside. Rows stay the same\nheight (they are tier-fixed and centre the child), and the child now has room\nfor its own glyphs.",
1013
1013
  "css": "@utility truncate {\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tline-height: normal;\n\ttext-box: normal;\n}"
1014
1014
  },
@@ -1018,7 +1018,7 @@
1018
1018
  "era-text-trim-caps"
1019
1019
  ],
1020
1020
  "file": "index.css",
1021
- "useCase": "the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.",
1021
+ "useCase": "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`.",
1022
1022
  "description": "ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band\nmisjudges caps-only strings — cap/alphabetic edges are the right box.",
1023
1023
  "css": "@utility era-text-trim-caps {\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}"
1024
1024
  },
@@ -22,8 +22,9 @@ assign to move the pane programmatically (e.g. reset to 0,0). |
22
22
  | `title?` | `string` | — | Text shown in the handle bar. For richer chrome use `header`. |
23
23
  | `icon?` | `Component<IconProps> \| null` | `AppPane` | Lucide icon rendered left of the title. Pass `null` to omit. |
24
24
  | `disabled?` | `boolean` | `false` | Disables dragging (reactive). |
25
- | `plugins?` | `(Plugin \| Compartment)[] \| (() => (Plugin \| Compartment)[])` | `[]` | Extra neodrag plugins (axis, bounds, grid, threshold, …) appended
26
- after the pane's own. Pass Compartments for reactive plugin sets. |
25
+ | `plugins?` | `DragPlugin[] \| (() => DragPlugin[])` | `[]` | Tier-2 neodrag plugins (`use`) appended to the drag. Tier-1 concerns
26
+ (axis/bounds/grid/threshold) are Pane.Root props spread through
27
+ restProps or use Root directly for those. |
27
28
  | `onDragStart?` | `(data: DragEventData) => void` | — | — |
28
29
  | `onDrag?` | `(data: DragEventData) => void` | — | — |
29
30
  | `onDragEnd?` | `(data: DragEventData) => void` | — | — |
@@ -18,7 +18,7 @@
18
18
  "file": "index.css",
19
19
  "useCase": "understated, motion-aware inline text link — the canonical style for a link in body copy or prose; prefer it over a hand-rolled underline or `Button variant=\"link\"` (which is a control, not running text).",
20
20
  "description": "Inline text link. An understated, permanent underline that darkens from\nmuted to the text colour on hover — no layout shift, no appearing/vanishing\nunderline. The colour fade rides the MOTION axis (--era-duration/-ease), so\nit snaps at data-motion=\"instant\", eases at \"normal\", and sweeps longer at\n\"extra\" — matching every other era transition. Font-agnostic: it styles\ndecoration + colour only, so it reads correctly in mono, sans, or serif, and\nthe 0.2em offset scales with the type size. Keyboard focus mirrors hover so\nthe affordance is reachable without a pointer.",
21
- "css": "@utility era-link {\n\t/* The text is bright at rest and never changes — the link reads as a link\n\t * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —\n\t * muted knocked back to 30% so it barely whispers — that resolves to the full\n\t * text colour on hover. */\n\tcolor: var(--color-link);\n\ttext-decoration-line: underline;\n\ttext-decoration-color: color-mix(in oklch, var(--color-muted) 30%, transparent);\n\ttext-underline-offset: 0.2em;\n\ttransition: text-decoration-color var(--era-duration) var(--era-ease);\n\t&:hover,\n\t&:focus-visible {\n\t\ttext-decoration-color: var(--color-fg);\n\t}\n}"
21
+ "css": "@utility era-link {\n\t/* The text is bright at rest and never changes — the link reads as a link\n\t * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —\n\t * the link's own ink knocked back to 30% so it barely whispers — that\n\t * resolves to the full ink on hover.\n\t *\n\t * currentColor, not --color-muted/--color-fg: the underline belongs to the\n\t * TEXT it underlines. On themes whose link ink is a colour (signalnine's\n\t * navy, an accent-tinted bright), the neutral tokens drew a whitish line\n\t * under coloured text — a second hue where there should be one. Derived\n\t * from currentColor, the underline tracks whatever the link resolves to,\n\t * in every theme, including a caller overriding `color` locally. */\n\tcolor: var(--color-link);\n\ttext-decoration-line: underline;\n\ttext-decoration-color: color-mix(in oklch, currentColor 30%, transparent);\n\ttext-underline-offset: 0.2em;\n\ttransition: text-decoration-color var(--era-duration) var(--era-ease);\n\t&:hover,\n\t&:focus-visible {\n\t\ttext-decoration-color: currentColor;\n\t}\n}"
22
22
  },
23
23
  {
24
24
  "name": "scrollbar-none",
@@ -36,7 +36,7 @@
36
36
  "era-text-trim"
37
37
  ],
38
38
  "file": "index.css",
39
- "useCase": "vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings.",
39
+ "useCase": "vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge. Fixed-height, non-editable elements ONLY: 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
40
  "description": "Font-agnostic vertical ink-centering for single-line control text.\nMeasured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):\nthe offset has TWO causes — a fractional line box (14px x 1.5lh = 21px in\na 24px row places ink on a half pixel, ~1px error) and font ascent/descent\nasymmetry (+-1px, sign flips per font). line-height: 1 fixes the first and\nis the Firefox fallback; text-box ex/alphabetic fixes the second by\ncentering the x-height band — the visual mass of MIXED-CASE text (caps and\ndescenders cancel above/below). cap alphabetic is the spec-example recipe\nbut is descender-blind (reads high; CSSWG #9148) — measured 4x worse here.\nWhere text-box is supported it fully determines the box, so the two\ndeclarations never fight. Worst-case residual: 0.44px (measured at 14px; the\nmechanism is font-relative, so dense's 12px inherits the same guarantee with\na proportionally smaller residual).\n\nThe hard rules concern overflow clipping, and there are TWO of them.\n\n1. NEVER on a NATIVE EDITABLE — <input>, <textarea>, <select>. These clip to\n a UA-owned inner editor box that IS the trimmed line box, so a fixed\n height does NOT hold it open: whatever you type gets its ascenders and\n descenders sliced off (a \"d\" renders as an \"o\"). This rule used to read\n the other way round — it claimed a fixed-height <input> was the trim's\n intended home — and the Input component shipped chopped text as a result.\n A tier-height input centres its own text; it needs no trim.\n\n2. NEVER on an AUTO-height element that clips — a `truncate` child, an\n auto-height box with overflow-hidden. With no height to hold it open the\n box collapses to the x-height band (~7.5px at 14px) and the clip chops the\n glyphs into a middle stripe. Put the trim on the fixed-height ROW instead\n and let the child centre via the row's items-center.\n\nA fixed-height NON-editable element is the safe home: the ink overflows the\ntrimmed LINE box but stays inside the padding box, which is what actually\nclips. Both rules are enforced at runtime by typography/collapsed-text-trim.",
41
41
  "css": "@utility era-text-trim {\n\tline-height: 1;\n\ttext-box: trim-both ex alphabetic;\n}"
42
42
  },
@@ -46,7 +46,7 @@
46
46
  "truncate"
47
47
  ],
48
48
  "file": "index.css",
49
- "useCase": "single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders.",
49
+ "useCase": "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.",
50
50
  "description": "Tailwind's `truncate`, with the vertical clip fixed at the source.\n\ntruncate is overflow:hidden, and overflow clips in BOTH axes — so whatever\nline box the element ends up with is also its ink clip. Era rows put\nera-text-trim on the fixed-height row (a Bar, a list row), and its\n`line-height: 1` INHERITS into the truncating child, which then has no fixed\nheight of its own: the child's box collapses to 1em while the font's real ink\nextent is ~1.15-1.2em, and the descenders (the \"g\" hook, the \"y\" tail) get\nsliced off at the box edge. Same bug on any `leading-none` + `truncate` pair.\n\nThe fix belongs here rather than at each call site: a horizontally-truncating\nelement must never be the thing that clips vertically. `line-height: normal`\nrestores the font's own ascent+descent box — exactly the ink extent — and\n`text-box: normal` cancels a trim if one is set alongside. Rows stay the same\nheight (they are tier-fixed and centre the child), and the child now has room\nfor its own glyphs.",
51
51
  "css": "@utility truncate {\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n\twhite-space: nowrap;\n\tline-height: normal;\n\ttext-box: normal;\n}"
52
52
  },
@@ -56,7 +56,7 @@
56
56
  "era-text-trim-caps"
57
57
  ],
58
58
  "file": "index.css",
59
- "useCase": "the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.",
59
+ "useCase": "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`.",
60
60
  "description": "ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band\nmisjudges caps-only strings — cap/alphabetic edges are the right box.",
61
61
  "css": "@utility era-text-trim-caps {\n\tline-height: 1;\n\ttext-box: trim-both cap alphabetic;\n}"
62
62
  },
@@ -41,9 +41,9 @@ enough to stay var-form — `min-w-(--era-h-md)` etc. remain the escape hatch.
41
41
  | `era-interactive` | the focus/disabled recipe every era control carries — put it on a custom interactive element instead of hand-writing focus/disabled variants. |
42
42
  | `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). |
43
43
  | `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. |
44
- | `era-text-trim` | vertically center single-line control text (a label beside an icon, a button's own text) without a translateY nudge — fixed-height, non-editable elements only; see era-text-trim-caps for ALL-CAPS strings. |
45
- | `truncate` | single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders. |
46
- | `era-text-trim-caps` | the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text. |
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
+ | `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
+ | `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
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. |
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). |
@@ -101,16 +101,23 @@ Declared in `index.css`.
101
101
  @utility era-link {
102
102
  /* The text is bright at rest and never changes — the link reads as a link
103
103
  * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —
104
- * muted knocked back to 30% so it barely whispers — that resolves to the full
105
- * text colour on hover. */
104
+ * the link's own ink knocked back to 30% so it barely whispers — that
105
+ * resolves to the full ink on hover.
106
+ *
107
+ * currentColor, not --color-muted/--color-fg: the underline belongs to the
108
+ * TEXT it underlines. On themes whose link ink is a colour (signalnine's
109
+ * navy, an accent-tinted bright), the neutral tokens drew a whitish line
110
+ * under coloured text — a second hue where there should be one. Derived
111
+ * from currentColor, the underline tracks whatever the link resolves to,
112
+ * in every theme, including a caller overriding `color` locally. */
106
113
  color: var(--color-link);
107
114
  text-decoration-line: underline;
108
- text-decoration-color: color-mix(in oklch, var(--color-muted) 30%, transparent);
115
+ text-decoration-color: color-mix(in oklch, currentColor 30%, transparent);
109
116
  text-underline-offset: 0.2em;
110
117
  transition: text-decoration-color var(--era-duration) var(--era-ease);
111
118
  &:hover,
112
119
  &:focus-visible {
113
- text-decoration-color: var(--color-fg);
120
+ text-decoration-color: currentColor;
114
121
  }
115
122
  }
116
123
  ```
@@ -135,7 +142,7 @@ Declared in `index.css`.
135
142
 
136
143
  ## era-text-trim
137
144
 
138
- **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; see era-text-trim-caps for ALL-CAPS strings.
145
+ **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.
139
146
 
140
147
  Font-agnostic vertical ink-centering for single-line control text.
141
148
  Measured verdict (4 fonts x 2 dpr, ink from device-pixel screenshots):
@@ -182,7 +189,7 @@ Declared in `index.css`.
182
189
 
183
190
  ## truncate
184
191
 
185
- **Use it for:** single-line horizontal truncation with an ellipsis — safe inside a trimmed or leading-none row; it will not clip ascenders or descenders.
192
+ **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.
186
193
 
187
194
  Tailwind's `truncate`, with the vertical clip fixed at the source.
188
195
 
@@ -215,7 +222,7 @@ Declared in `index.css`.
215
222
 
216
223
  ## era-text-trim-caps
217
224
 
218
- **Use it for:** the era-text-trim variant for ALL-CAPS single-line strings (kbd hints, acronym badges) — never use it for mixed-case text.
225
+ **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`.
219
226
 
220
227
  ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
221
228
  misjudges caps-only strings — cap/alphabetic edges are the right box.
@@ -4,7 +4,7 @@
4
4
  import Minus from '@lucide/svelte/icons/minus';
5
5
  import Maximize from '@lucide/svelte/icons/maximize';
6
6
  import X from '@lucide/svelte/icons/x';
7
- import type { Plugin } from '@neodrag/svelte';
7
+ import type { DragPlugin } from '@neodrag/svelte';
8
8
  import { cn } from '../utils/index.js';
9
9
  import { getPaneManager, type AppPane } from './pane-manager.svelte.js';
10
10
 
@@ -17,24 +17,26 @@
17
17
  // not leave the TOP of the work area at all (y ≥ workArea.y), and on every
18
18
  // other side at least pm.dragKeep px of the pane must stay inside the
19
19
  // work area (which excludes chrome — a strip hidden behind the taskbar
20
- // would be just as lost). Clamped mid-drag the way neodrag's own bounds
21
- // plugin does it: offset + proposed delta clamp re-propose the delta,
22
- // so the cursor simply stops pulling at the fence instead of
23
- // rubber-banding on release. Only a `drag` hook (width read live), and a
24
- // STABLE plugin list an inline array would change identity every render
25
- // and make neodrag reconcile mid-session.
26
- const shellClamp: Plugin = {
20
+ // would be just as lost). next.10's tier-2 seam hands `onMove` the PROPOSED
21
+ // absolute offset (constraints already applied); returning a point replaces
22
+ // it — so the clamp is a pure function of the proposal, the cursor simply
23
+ // stops pulling at the fence, and nothing rubber-bands on release. Width is
24
+ // read live off the node each move (a resize mid-session must re-fence).
25
+ // A STABLE plugin list — an inline array would change identity every render
26
+ // and push a needless options update through the drag.
27
+ const shellClamp: DragPlugin = {
27
28
  name: 'era:shell-clamp',
28
- drag(ctx) {
29
+ onMove(ctx) {
29
30
  const area = pm.workArea;
30
31
  if (!area.width || !area.height) return;
31
32
  const keep = pm.dragKeep;
32
- const width = (ctx.rootNode as HTMLElement).offsetWidth;
33
- const px = ctx.offset.x + (ctx.proposed.x ?? 0);
34
- const py = ctx.offset.y + (ctx.proposed.y ?? 0);
35
- const cx = Math.min(Math.max(px, area.x + keep - width), area.x + area.width - keep);
36
- const cy = Math.min(Math.max(py, area.y), area.y + area.height - keep);
37
- ctx.propose(cx - ctx.offset.x, cy - ctx.offset.y);
33
+ const width = (ctx.node as HTMLElement).offsetWidth;
34
+ const cx = Math.min(
35
+ Math.max(ctx.offset.x, area.x + keep - width),
36
+ area.x + area.width - keep
37
+ );
38
+ const cy = Math.min(Math.max(ctx.offset.y, area.y), area.y + area.height - keep);
39
+ if (cx !== ctx.offset.x || cy !== ctx.offset.y) return { x: cx, y: cy };
38
40
  }
39
41
  };
40
42
  const panePlugins = [shellClamp];
@@ -91,16 +91,23 @@
91
91
  @utility era-link {
92
92
  /* The text is bright at rest and never changes — the link reads as a link
93
93
  * before you touch it. Only the UNDERLINE reacts: a faint hint at rest —
94
- * muted knocked back to 30% so it barely whispers — that resolves to the full
95
- * text colour on hover. */
94
+ * the link's own ink knocked back to 30% so it barely whispers — that
95
+ * resolves to the full ink on hover.
96
+ *
97
+ * currentColor, not --color-muted/--color-fg: the underline belongs to the
98
+ * TEXT it underlines. On themes whose link ink is a colour (signalnine's
99
+ * navy, an accent-tinted bright), the neutral tokens drew a whitish line
100
+ * under coloured text — a second hue where there should be one. Derived
101
+ * from currentColor, the underline tracks whatever the link resolves to,
102
+ * in every theme, including a caller overriding `color` locally. */
96
103
  color: var(--color-link);
97
104
  text-decoration-line: underline;
98
- text-decoration-color: color-mix(in oklch, var(--color-muted) 30%, transparent);
105
+ text-decoration-color: color-mix(in oklch, currentColor 30%, transparent);
99
106
  text-underline-offset: 0.2em;
100
107
  transition: text-decoration-color var(--era-duration) var(--era-ease);
101
108
  &:hover,
102
109
  &:focus-visible {
103
- text-decoration-color: var(--color-fg);
110
+ text-decoration-color: currentColor;
104
111
  }
105
112
  }
106
113
 
@@ -151,8 +158,10 @@
151
158
  * clips. Both rules are enforced at runtime by typography/collapsed-text-trim.
152
159
  *
153
160
  * @use vertically center single-line control text (a label beside an icon, a
154
- * button's own text) without a translateY nudge — fixed-height, non-editable
155
- * elements only; see era-text-trim-caps for ALL-CAPS strings.
161
+ * button's own text) without a translateY nudge. Fixed-height, non-editable
162
+ * elements ONLY: never on an <input>/<textarea>, and never on the same element
163
+ * as `truncate` or any overflow clip — put it on the fixed-height ROW and let
164
+ * the truncating child centre inside it. See era-text-trim-caps for ALL-CAPS.
156
165
  */
157
166
  @utility era-text-trim {
158
167
  line-height: 1;
@@ -176,8 +185,9 @@
176
185
  * height (they are tier-fixed and centre the child), and the child now has room
177
186
  * for its own glyphs.
178
187
  *
179
- * @use single-line horizontal truncation with an ellipsis — safe inside a
180
- * trimmed or leading-none row; it will not clip ascenders or descenders. */
188
+ * @use single-line horizontal truncation with an ellipsis — put it on the
189
+ * truncating CHILD inside a trimmed or leading-none row, never on the same
190
+ * element as era-text-trim; it will not clip ascenders or descenders. */
181
191
  @utility truncate {
182
192
  overflow: hidden;
183
193
  text-overflow: ellipsis;
@@ -186,11 +196,38 @@
186
196
  text-box: normal;
187
197
  }
188
198
 
199
+ /* THE COLLISION GUARD — `truncate` and `era-text-trim` on the SAME element.
200
+ *
201
+ * Two utilities, same specificity, opposite intents: the trim collapses the box
202
+ * to the x-height band, and truncate clips to whatever box it lands on. Class
203
+ * ORDER in the attribute cannot break the tie — CSS resolves same-specificity
204
+ * conflicts by stylesheet order, and Tailwind emits era-text-trim last, so the
205
+ * trim silently won and the clip chopped the glyphs into a middle stripe
206
+ * (measured: an 8.58px box holding 16px of ink). Writing the classes the other
207
+ * way round changed nothing, which is what made it so hard to see.
208
+ *
209
+ * A real consumer hit exactly this. So the pairing is now DEFINED rather than
210
+ * accidental, with a compound selector that outranks both single-class
211
+ * utilities: truncate wins the vertical, the glyphs stay whole, and the
212
+ * element centres the way the docs say it should anyway — via its
213
+ * fixed-height parent's items-center.
214
+ *
215
+ * This is a safety net, not a blessing: the correct authoring is still trim on
216
+ * the ROW, truncate on the CHILD (typography/collapsed-text-trim still reports
217
+ * the pair so the runtime audit can point at it). It exists so the failure mode
218
+ * is a no-op instead of unreadable text. */
219
+ .era-text-trim.truncate,
220
+ .era-text-trim-caps.truncate {
221
+ line-height: normal;
222
+ text-box: normal;
223
+ }
224
+
189
225
  /* ALL-CAPS single-line text (kbd hints, acronym badges): the x-height band
190
226
  * misjudges caps-only strings — cap/alphabetic edges are the right box.
191
227
  *
192
228
  * @use the era-text-trim variant for ALL-CAPS single-line strings (kbd hints,
193
- * acronym badges) — never use it for mixed-case text. */
229
+ * acronym badges) — never for mixed-case text (a label like "System" reads
230
+ * high; use era-text-trim), and never on the same element as `truncate`. */
194
231
  @utility era-text-trim-caps {
195
232
  line-height: 1;
196
233
  text-box: trim-both cap alphabetic;