@signal9/era-ui 12.1.2 → 13.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 (93) hide show
  1. package/dist/dev/audit-overlay.svelte +1 -1
  2. package/dist/docs/css-utilities.js +6 -1
  3. package/dist/docs/notes.md +12 -11
  4. package/dist/era-ui.css +1 -1
  5. package/dist/generated-docs/badge.md +3 -0
  6. package/dist/generated-docs/button.md +4 -0
  7. package/dist/generated-docs/chip.md +3 -0
  8. package/dist/generated-docs/llms-full.txt +53 -31
  9. package/dist/generated-docs/llms.txt +1 -1
  10. package/dist/generated-docs/manifest.json +17 -17
  11. package/dist/generated-docs/notes.md +12 -11
  12. package/dist/generated-docs/utilities.json +10 -10
  13. package/dist/generated-docs/utilities.md +30 -19
  14. package/dist/os/index.d.ts +0 -1
  15. package/dist/os/index.js +0 -1
  16. package/dist/os/notification-center.svelte +1 -2
  17. package/dist/os/taskbar.svelte +2 -3
  18. package/dist/os/toaster.svelte +2 -3
  19. package/dist/styles/index.css +77 -12
  20. package/dist/styles/layers.css +48 -0
  21. package/dist/styles/surfaces/base.css +4 -2
  22. package/dist/styles/surfaces/glass.css +0 -13
  23. package/dist/theme/index.d.ts +87 -0
  24. package/dist/theme/index.js +86 -0
  25. package/dist/ui/alert-dialog/alert-dialog-content.svelte +2 -2
  26. package/dist/ui/alert-dialog/alert-dialog-overlay.svelte +1 -1
  27. package/dist/ui/badge/badge.svelte +14 -1
  28. package/dist/ui/badge/badge.svelte.d.ts +4 -0
  29. package/dist/ui/button/button.svelte +37 -7
  30. package/dist/ui/button/button.svelte.d.ts +6 -0
  31. package/dist/ui/chip/chip.svelte +14 -1
  32. package/dist/ui/chip/chip.svelte.d.ts +4 -0
  33. package/dist/ui/combobox/combobox-content.svelte +1 -1
  34. package/dist/ui/command-bar/command-bar.svelte +1 -1
  35. package/dist/ui/context-menu/context-menu-content.svelte +1 -1
  36. package/dist/ui/context-menu/context-menu-sub-content.svelte +1 -1
  37. package/dist/ui/date-picker/date-picker-content.svelte +1 -1
  38. package/dist/ui/date-range-picker/date-range-picker-content.svelte +1 -1
  39. package/dist/ui/dialog/dialog-content.svelte +1 -1
  40. package/dist/ui/dialog/dialog-overlay.svelte +5 -1
  41. package/dist/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
  42. package/dist/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  43. package/dist/ui/link-preview/link-preview-content.svelte +1 -1
  44. package/dist/ui/menubar/menubar-content.svelte +1 -1
  45. package/dist/ui/menubar/menubar-sub-content.svelte +1 -1
  46. package/dist/ui/popover/popover-content.svelte +1 -1
  47. package/dist/ui/select/select-content.svelte +1 -1
  48. package/dist/ui/sheet/sheet-content.svelte +1 -1
  49. package/dist/ui/sheet/sheet-content.svelte.d.ts +1 -1
  50. package/dist/ui/tooltip/tooltip-content.svelte +1 -1
  51. package/package.json +9 -12
  52. package/dist/apps/index.d.ts +0 -20
  53. package/dist/apps/index.js +0 -20
  54. package/dist/apps/llm-shell/index.d.ts +0 -2
  55. package/dist/apps/llm-shell/index.js +0 -1
  56. package/dist/apps/llm-shell/llm-shell.svelte +0 -334
  57. package/dist/apps/llm-shell/llm-shell.svelte.d.ts +0 -57
  58. package/dist/apps/llm-shell/llm-swarm.svelte +0 -79
  59. package/dist/apps/llm-shell/llm-swarm.svelte.d.ts +0 -25
  60. package/dist/apps/llm-shell/types.d.ts +0 -103
  61. package/dist/apps/llm-shell/types.js +0 -1
  62. package/dist/apps/notes/editor/bubble-menu.svelte.d.ts +0 -11
  63. package/dist/apps/notes/editor/bubble-menu.svelte.js +0 -165
  64. package/dist/apps/notes/editor/bubble-toolbar.svelte +0 -55
  65. package/dist/apps/notes/editor/bubble-toolbar.svelte.d.ts +0 -22
  66. package/dist/apps/notes/editor/extensions.d.ts +0 -18
  67. package/dist/apps/notes/editor/extensions.js +0 -88
  68. package/dist/apps/notes/editor/floating.d.ts +0 -25
  69. package/dist/apps/notes/editor/floating.js +0 -54
  70. package/dist/apps/notes/editor/link.d.ts +0 -9
  71. package/dist/apps/notes/editor/link.js +0 -22
  72. package/dist/apps/notes/editor/list-cleanup-rule.d.ts +0 -14
  73. package/dist/apps/notes/editor/list-cleanup-rule.js +0 -47
  74. package/dist/apps/notes/editor/slash-command.svelte.d.ts +0 -22
  75. package/dist/apps/notes/editor/slash-command.svelte.js +0 -215
  76. package/dist/apps/notes/editor/slash-menu.svelte +0 -82
  77. package/dist/apps/notes/editor/slash-menu.svelte.d.ts +0 -23
  78. package/dist/apps/notes/index.d.ts +0 -11
  79. package/dist/apps/notes/index.js +0 -13
  80. package/dist/apps/notes/markdown.d.ts +0 -12
  81. package/dist/apps/notes/markdown.js +0 -165
  82. package/dist/apps/notes/note-editor.svelte +0 -176
  83. package/dist/apps/notes/note-editor.svelte.d.ts +0 -36
  84. package/dist/apps/notes/notes-store.svelte.d.ts +0 -64
  85. package/dist/apps/notes/notes-store.svelte.js +0 -234
  86. package/dist/apps/notes/notes.svelte +0 -501
  87. package/dist/apps/notes/notes.svelte.d.ts +0 -53
  88. package/dist/apps/notes/tree.d.ts +0 -70
  89. package/dist/apps/notes/tree.js +0 -185
  90. package/dist/apps/notes/types.d.ts +0 -49
  91. package/dist/apps/notes/types.js +0 -11
  92. package/dist/os/layers.d.ts +0 -23
  93. package/dist/os/layers.js +0 -23
@@ -1,14 +1,48 @@
1
- @import './themes.css';
2
- @import './density.css';
1
+ /* ------------------------------------------------------------------ */
2
+ /* CASCADE LAYERS */
3
+ /* */
4
+ /* era's token CSS is LAYERED so a consumer can override it without */
5
+ /* a specificity fight. Unlayered styles beat every layer, so a */
6
+ /* consumer's own `:root[data-theme='mine']` block — written plainly, */
7
+ /* no !important, no selector padding — now wins against era's, which */
8
+ /* is what defining a theme downstream should feel like. Before this */
9
+ /* everything era shipped was unlayered, so it outranked the whole of */
10
+ /* a consumer's own `@layer`ed Tailwind CSS. */
11
+ /* */
12
+ /* WHAT IS DELIBERATELY NOT LAYERED, because layering would change */
13
+ /* what it means rather than just where it sits: */
14
+ /* */
15
+ /* typography.css and motion.css contain `!important` — the font */
16
+ /* axis hammer and the reduced-motion guarantee. Layer order is */
17
+ /* REVERSED for important declarations, so moving these into a */
18
+ /* layer would make them beat a consumer's unlayered !important */
19
+ /* instead of losing to it. That is a stronger claim than era has */
20
+ /* any business making about someone else's page. */
21
+ /* */
22
+ /* The .era-text-trim.truncate collision guard (below) has to */
23
+ /* outrank two single-class utilities, and utilities are layered. */
24
+ /* Unlayered is precisely what lets it win. */
25
+ /* */
26
+ /* @utility and @theme are Tailwind's to place; it already layers */
27
+ /* them. */
28
+ /* ------------------------------------------------------------------ */
29
+ @layer era.tokens, era.base;
30
+
31
+ @import './themes.css' layer(era.tokens);
32
+ @import './density.css' layer(era.tokens);
33
+ /* Stacking contract — global tiers only; see the file for the local/global split. */
34
+ @import './layers.css' layer(era.tokens);
3
35
  /* Motion axis (data-motion) — owns --era-duration + --era-ease. Imported before
4
- * surfaces so a [data-surface] block can override --era-ease as a delta. */
36
+ * surfaces so a [data-surface] block can override --era-ease as a delta.
37
+ * UNLAYERED: carries the prefers-reduced-motion !important — see above. */
5
38
  @import './motion.css';
6
39
  /* Surfaces: base contract first, then each surface's deltas. Order matters —
7
40
  * the base block and a surface's block target the same element, and the
8
- * surface's declarations must win. */
9
- @import './surfaces/base.css';
10
- @import './surfaces/glass.css';
11
- @import './surfaces/bevel.css';
41
+ * surface's declarations must win. Same layer, so source order still decides. */
42
+ @import './surfaces/base.css' layer(era.tokens);
43
+ @import './surfaces/glass.css' layer(era.tokens);
44
+ @import './surfaces/bevel.css' layer(era.tokens);
45
+ /* UNLAYERED: carries the font-axis !important — see above. */
12
46
  @import './typography.css';
13
47
 
14
48
  /* WHAT IS NOT HERE: vidstack's stylesheets, which the VideoPlayer needs and
@@ -35,13 +69,14 @@
35
69
  @source '../ui';
36
70
  @source '../ai';
37
71
  @source '../os';
38
- @source '../apps';
39
72
 
40
73
  /* Lucide icons use an SVG presentation attribute for stroke width by default.
41
74
  * Set it from an Era token globally so every imported Lucide component tracks
42
75
  * the same optical weight as adjacent body text and can be tuned in one place. */
43
- :where(.lucide) {
44
- stroke-width: var(--era-glyph-stroke-width);
76
+ @layer era.base {
77
+ :where(.lucide) {
78
+ stroke-width: var(--era-glyph-stroke-width);
79
+ }
45
80
  }
46
81
 
47
82
  /* The mask glyph is the one piece of type the UA picks for you, so era picks it
@@ -67,8 +102,10 @@
67
102
  * type="text" and masking it ourselves, which costs password-manager detection
68
103
  * and the screen-reader "password" announcement. Deliberately not done — the
69
104
  * semantics are worth more than the corner. */
70
- :where(input[type='password']) {
71
- -webkit-text-security: var(--era-mask-glyph, disc);
105
+ @layer era.base {
106
+ :where(input[type='password']) {
107
+ -webkit-text-security: var(--era-mask-glyph, disc);
108
+ }
72
109
  }
73
110
 
74
111
  /* Shared interactive-state recipe: quiet focus, disabled affordance.
@@ -351,6 +388,24 @@
351
388
  text-box: trim-both ex alphabetic;
352
389
  }
353
390
 
391
+ /* Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter
392
+ * is a registered utility, gated to glass subtrees.
393
+ *
394
+ * It lives HERE rather than in surfaces/glass.css, where a surface's own
395
+ * chrome would naturally sit, because glass.css is imported into a cascade
396
+ * layer and `@utility` cannot be nested inside one. Surface-scoped RULES stay
397
+ * in their surface file; surface-scoped UTILITIES have to come out.
398
+ *
399
+ * @use the backdrop blur for a floating panel (popover, dialog, taskbar) — it
400
+ * applies only inside a data-surface="glass" subtree and is inert elsewhere,
401
+ * so it is safe to leave on a panel that renders under every surface. */
402
+ @utility glass-blur {
403
+ [data-surface='glass'] & {
404
+ backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
405
+ -webkit-backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
406
+ }
407
+ }
408
+
354
409
  /* In-progress text shimmer — a bright sweep through muted text (the standard
355
410
  * "working…" label treatment in streaming UIs). Timing derives from the motion
356
411
  * axis (10× --era-duration ≈ 1.5s at normal, 2.6s at extra); at instant the
@@ -541,6 +596,16 @@
541
596
  * bg-field-highlight its focused/hovered face. */
542
597
  --color-field: var(--era-field-bg);
543
598
  --color-field-highlight: var(--era-field-highlight);
599
+ /* @use the stacking tier for chrome that renders at the document root —
600
+ * z-chrome (taskbar) < z-drawer (slide-over) < z-overlay (every dialog,
601
+ * menu, popover and scrim) < z-toast < z-debug. A component ordering its
602
+ * OWN parts inside an isolated context keeps small integers instead; see
603
+ * styles/layers.css for why the two are different things. */
604
+ --z-index-chrome: var(--era-z-chrome);
605
+ --z-index-drawer: var(--era-z-drawer);
606
+ --z-index-overlay: var(--era-z-overlay);
607
+ --z-index-toast: var(--era-z-toast);
608
+ --z-index-debug: var(--era-z-debug);
544
609
  /* Motion — bare transition utilities default to the motion axis, so
545
610
  * `transition-colors` alone is on-design (0s at instant, longer at extra).
546
611
  * ease-base/duration-base name the axis explicitly for overrides. */
@@ -0,0 +1,48 @@
1
+ /* ------------------------------------------------------------------ */
2
+ /* THE STACKING CONTRACT */
3
+ /* */
4
+ /* Two kinds of z-index exist in this library, and conflating them is */
5
+ /* what turns stacking into whack-a-mole. */
6
+ /* */
7
+ /* GLOBAL TIERS — elements that render at the document root, either */
8
+ /* because they are shell chrome or because a portal put them there. */
9
+ /* These CAN collide with each other, from anywhere in the tree, so */
10
+ /* their order is a contract and they are named here. Nothing in the */
11
+ /* library may write a raw z-index in this band; scripts/check-layers */
12
+ /* fails the build if it does. */
13
+ /* */
14
+ /* LOCAL STACKING — a component ordering its own parts (a video's */
15
+ /* poster under its controls, a pane's resize edges over its body). */
16
+ /* These stay small integers on purpose: they are meaningless outside */
17
+ /* their own component and naming them with a global tier would claim */
18
+ /* a relationship that does not exist. What makes them safe is not */
19
+ /* their value but ISOLATION — the ancestor establishing a stacking */
20
+ /* context, so the number cannot escape. The OS Desktop is the */
21
+ /* load-bearing example: it is `isolate`, so panes hand out 1..N */
22
+ /* freely and no pane can ever climb over a menu or a toast. */
23
+ /* */
24
+ /* The tiers are spaced to leave room, and 50 is deliberate: bits-ui */
25
+ /* portals its dialogs, menus and tooltips at z-50, so era's own */
26
+ /* floating chrome sits in that same band rather than fighting it. */
27
+ /* Anything that must beat a menu goes above; chrome that a menu must */
28
+ /* open over goes below. */
29
+ /* ------------------------------------------------------------------ */
30
+ :root {
31
+ /* Shell chrome — a taskbar or dock. BELOW the overlay band, so a
32
+ * dropdown anchored to the taskbar opens over the taskbar. */
33
+ --era-z-chrome: 30;
34
+ /* Slide-over panels that hang off the chrome (a notification centre).
35
+ * Above the chrome it emerges from, still under menus. */
36
+ --era-z-drawer: 40;
37
+ /* Every floating panel and every scrim: dialog, popover, select, menu,
38
+ * tooltip, command bar. Shares bits-ui's own tier — within it, DOM order
39
+ * decides, which is what makes a popover opened from inside a dialog land
40
+ * above the dialog without either knowing about the other. */
41
+ --era-z-overlay: 50;
42
+ /* Transient system messages. Above the overlay band on purpose: a toast
43
+ * that a modal can hide is a toast you will miss. */
44
+ --era-z-toast: 55;
45
+ /* Development overlays (the audit harness). Above everything it inspects,
46
+ * because inspecting a dialog means drawing on top of one. */
47
+ --era-z-debug: 60;
48
+ }
@@ -196,8 +196,10 @@
196
196
  * a bevel thumb sliding along a track), scope a rule to the surface and
197
197
  * target the component's stable part, e.g. in the surface's own file:
198
198
  * [data-surface='<name>'] [data-slot='switch-thumb'] { … }
199
- * Keyframes and surface-only utilities live in that surface's file
200
- * (precedent: the `glass-blur` @utility in glass.css).
199
+ * Keyframes live in that surface's file. Surface-only UTILITIES cannot:
200
+ * the surface files are imported into a cascade layer, and `@utility` may
201
+ * not be nested inside one — so they live in styles/index.css alongside
202
+ * the rest (precedent: `glass-blur`, which is gated to glass subtrees).
201
203
  *
202
204
  * Reduced-motion is handled centrally in motion.css — surfaces need not.
203
205
  */
@@ -155,16 +155,3 @@
155
155
  --era-selection-shadow: none;
156
156
  }
157
157
  }
158
-
159
- /* Tailwind v4 cannot resolve var() inside backdrop-blur-[], so the filter
160
- * is a registered utility, gated to glass subtrees.
161
- *
162
- * @use the backdrop blur for a floating panel (popover, dialog, taskbar) — it
163
- * applies only inside a data-surface="glass" subtree and is inert elsewhere,
164
- * so it is safe to leave on a panel that renders under every surface. */
165
- @utility glass-blur {
166
- [data-surface='glass'] & {
167
- backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
168
- -webkit-backdrop-filter: blur(var(--era-blur)) saturate(var(--era-saturate));
169
- }
170
- }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Authoring a theme, with the contract in the type system.
3
+ *
4
+ * A theme was a block of hand-written CSS, and the set of tokens it had to
5
+ * define existed only in the heads of the people who had written one before.
6
+ * Nothing checked it. The failure mode is not a build error — it is a token
7
+ * that quietly inherits the DEFAULT theme's value and looks almost right: era's
8
+ * own `sand` shipped with accent foregrounds inherited from a palette whose
9
+ * floor was half a lightness step away, and the result measured 2.48:1 before
10
+ * anyone noticed it by eye.
11
+ *
12
+ * So the contract is a type. Every token the system's `@theme` block depends on
13
+ * is a required field, `themeCss()` turns a checked object into the CSS text,
14
+ * and omitting one is a compile error rather than a contrast bug.
15
+ *
16
+ * Three tokens are deliberately OPTIONAL, because era defines them once at the
17
+ * root and a theme is not expected to restate them: `bright`, `link` and
18
+ * `muted`. Setting them overrides the default; leaving them out inherits it,
19
+ * which is what era's own themes do.
20
+ *
21
+ * This is an AUTHORING helper, not a runtime. It returns a string. Put it in a
22
+ * `.css.ts`-style build step, a `<style>` in your root layout, or paste the
23
+ * output — era's own themes stay hand-written CSS because their values carry
24
+ * measured rationale in comments that generated output would lose.
25
+ */
26
+ /** The 12-step ramp, background (1) through to the brightest ink (12). */
27
+ export type Scale12 = readonly [
28
+ string,
29
+ string,
30
+ string,
31
+ string,
32
+ string,
33
+ string,
34
+ string,
35
+ string,
36
+ string,
37
+ string,
38
+ string,
39
+ string
40
+ ];
41
+ /** `step(5)` → `var(--color-5)` — point a semantic token at a rung. */
42
+ export declare const step: (n: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12) => string;
43
+ export interface ThemeMode {
44
+ /** Drives `color-scheme`, and with it the UA's own form controls and scrollbars. */
45
+ scheme: 'dark' | 'light';
46
+ /** The ramp. Every other token may reference a rung with `step()`. */
47
+ scale: Scale12;
48
+ /** Chrome. */
49
+ border: string;
50
+ surface: string;
51
+ hover: string;
52
+ fg: string;
53
+ /** Accents, each with the ink that sits ON it — the pair is the point. */
54
+ primary: string;
55
+ primaryFg: string;
56
+ primaryHover: string;
57
+ destructive: string;
58
+ destructiveFg: string;
59
+ success: string;
60
+ successFg: string;
61
+ warning: string;
62
+ warningFg: string;
63
+ /** Optional — era defines these at the root; set to override. */
64
+ bright?: string;
65
+ link?: string;
66
+ muted?: string;
67
+ /**
68
+ * Anything else the theme owns, as bare token names: `{ 'color-ink':
69
+ * 'oklch(0.24 0.03 60)' }`. era's `sand` needs exactly this — its palette has
70
+ * no rung dark enough to serve as accent ink, so it defines its own.
71
+ */
72
+ extra?: Record<string, string>;
73
+ }
74
+ export interface Theme {
75
+ /** Selected with `data-theme="<name>"`. */
76
+ name: string;
77
+ dark: ThemeMode;
78
+ light: ThemeMode;
79
+ }
80
+ /**
81
+ * The CSS for a theme: a dark block, a `prefers-color-scheme: light` block, and
82
+ * the explicit `.light` / `.dark` class escapes that let a subtree opt out.
83
+ *
84
+ * Emitted into `@layer era.tokens` so it sits exactly where era's own themes
85
+ * sit — a consumer's later, unlayered CSS still wins over both.
86
+ */
87
+ export declare function themeCss(theme: Theme): string;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Authoring a theme, with the contract in the type system.
3
+ *
4
+ * A theme was a block of hand-written CSS, and the set of tokens it had to
5
+ * define existed only in the heads of the people who had written one before.
6
+ * Nothing checked it. The failure mode is not a build error — it is a token
7
+ * that quietly inherits the DEFAULT theme's value and looks almost right: era's
8
+ * own `sand` shipped with accent foregrounds inherited from a palette whose
9
+ * floor was half a lightness step away, and the result measured 2.48:1 before
10
+ * anyone noticed it by eye.
11
+ *
12
+ * So the contract is a type. Every token the system's `@theme` block depends on
13
+ * is a required field, `themeCss()` turns a checked object into the CSS text,
14
+ * and omitting one is a compile error rather than a contrast bug.
15
+ *
16
+ * Three tokens are deliberately OPTIONAL, because era defines them once at the
17
+ * root and a theme is not expected to restate them: `bright`, `link` and
18
+ * `muted`. Setting them overrides the default; leaving them out inherits it,
19
+ * which is what era's own themes do.
20
+ *
21
+ * This is an AUTHORING helper, not a runtime. It returns a string. Put it in a
22
+ * `.css.ts`-style build step, a `<style>` in your root layout, or paste the
23
+ * output — era's own themes stay hand-written CSS because their values carry
24
+ * measured rationale in comments that generated output would lose.
25
+ */
26
+ /** `step(5)` → `var(--color-5)` — point a semantic token at a rung. */
27
+ export const step = (n) => `var(--color-${n})`;
28
+ const TOKEN = {
29
+ border: '--color-border',
30
+ surface: '--color-surface',
31
+ hover: '--color-hover',
32
+ fg: '--color-fg',
33
+ primary: '--color-primary',
34
+ primaryFg: '--color-primary-fg',
35
+ primaryHover: '--color-primary-hover',
36
+ destructive: '--color-destructive',
37
+ destructiveFg: '--color-destructive-fg',
38
+ success: '--color-success',
39
+ successFg: '--color-success-fg',
40
+ warning: '--color-warning',
41
+ warningFg: '--color-warning-fg',
42
+ bright: '--color-bright',
43
+ link: '--color-link',
44
+ muted: '--color-muted'
45
+ };
46
+ function modeBody(mode, indent) {
47
+ const lines = [`${indent}color-scheme: ${mode.scheme};`, ''];
48
+ mode.scale.forEach((value, i) => lines.push(`${indent}--color-${i + 1}: ${value};`));
49
+ lines.push('');
50
+ for (const [key, token] of Object.entries(TOKEN)) {
51
+ const value = mode[key];
52
+ if (typeof value === 'string')
53
+ lines.push(`${indent}${token}: ${value};`);
54
+ }
55
+ for (const [name, value] of Object.entries(mode.extra ?? {})) {
56
+ lines.push(`${indent}--${name.replace(/^--/, '')}: ${value};`);
57
+ }
58
+ return lines.join('\n');
59
+ }
60
+ /**
61
+ * The CSS for a theme: a dark block, a `prefers-color-scheme: light` block, and
62
+ * the explicit `.light` / `.dark` class escapes that let a subtree opt out.
63
+ *
64
+ * Emitted into `@layer era.tokens` so it sits exactly where era's own themes
65
+ * sit — a consumer's later, unlayered CSS still wins over both.
66
+ */
67
+ export function themeCss(theme) {
68
+ const { name, dark, light } = theme;
69
+ return `@layer era.tokens {
70
+ \t:root[data-theme='${name}'],
71
+ \t[data-theme='${name}'].dark {
72
+ ${modeBody(dark, '\t\t')}
73
+ \t}
74
+
75
+ \t@media (prefers-color-scheme: light) {
76
+ \t\t:root[data-theme='${name}']:not(.dark) {
77
+ ${modeBody(light, '\t\t\t')}
78
+ \t\t}
79
+ \t}
80
+
81
+ \t[data-theme='${name}'].light {
82
+ ${modeBody(light, '\t\t')}
83
+ \t}
84
+ }
85
+ `;
86
+ }
@@ -11,11 +11,11 @@
11
11
  </script>
12
12
 
13
13
  <AlertDialog.Portal>
14
- <AlertDialog.Overlay class="fixed inset-0 z-50 bg-overlay" />
14
+ <AlertDialog.Overlay class="fixed inset-0 z-overlay bg-overlay" />
15
15
  <AlertDialog.Content
16
16
  bind:ref
17
17
  class={cn(
18
- 'fixed top-1/2 left-1/2 z-50 flex w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col gap-gutter rounded-bar border border-divider bg-well p-card shadow-lg glass-blur',
18
+ 'fixed top-1/2 left-1/2 z-overlay flex w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col gap-gutter rounded-bar border border-divider bg-well p-card shadow-lg glass-blur',
19
19
  className
20
20
  )}
21
21
  {...restProps}
@@ -11,6 +11,6 @@
11
11
 
12
12
  <AlertDialog.Overlay
13
13
  bind:ref
14
- class={cn('fixed inset-0 z-50 bg-overlay', className)}
14
+ class={cn('fixed inset-0 z-overlay bg-overlay', className)}
15
15
  {...restProps}
16
16
  />
@@ -40,16 +40,29 @@
40
40
  let {
41
41
  ref = $bindable(null),
42
42
  tone = 'default',
43
+ lead,
43
44
  children,
44
45
  class: className,
45
46
  ...restProps
46
47
  }: HTMLAttributes<HTMLSpanElement> & {
47
48
  ref?: HTMLSpanElement | null;
48
49
  tone?: 'default' | 'accent' | 'destructive' | 'success' | 'warning';
50
+ /** Icon rendered BEFORE the label, outside it. Use this rather than putting
51
+ * an icon in `children`: the label then gets its own box, which is the only
52
+ * place era's ink centring can reach. */
53
+ lead?: Snippet;
49
54
  children?: Snippet;
50
55
  } = $props();
51
56
  </script>
52
57
 
53
58
  <span bind:this={ref} class={cn(badgeVariants({ tone }), className)} {...restProps}>
54
- {@render children?.()}
59
+ {#if lead}
60
+ {@render lead()}
61
+ {/if}
62
+ <!-- The label box exists ONLY when `lead` is used. Rendering it for every call
63
+ site changed the DOM everywhere: a `display:contents` span has no box, and
64
+ the geometry audits walk real boxes. Opt-in keeps the legacy shape
65
+ byte-identical and gives the new shape a real box for the CAP-band trim a token wants. -->
66
+ {#if lead}<span class="era-text-trim-caps">{@render children?.()}</span
67
+ >{:else}{@render children?.()}{/if}
55
68
  </span>
@@ -28,6 +28,10 @@ import type { HTMLAttributes } from 'svelte/elements';
28
28
  type $$ComponentProps = HTMLAttributes<HTMLSpanElement> & {
29
29
  ref?: HTMLSpanElement | null;
30
30
  tone?: 'default' | 'accent' | 'destructive' | 'success' | 'warning';
31
+ /** Icon rendered BEFORE the label, outside it. Use this rather than putting
32
+ * an icon in `children`: the label then gets its own box, which is the only
33
+ * place era's ink centring can reach. */
34
+ lead?: Snippet;
31
35
  children?: Snippet;
32
36
  };
33
37
  declare const Badge: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -15,6 +15,8 @@
15
15
  loading = false,
16
16
  loadingLabel,
17
17
  disabled,
18
+ lead,
19
+ trail,
18
20
  children,
19
21
  class: className,
20
22
  ...restProps
@@ -32,6 +34,12 @@
32
34
  loading?: boolean;
33
35
  /** Screen-reader-only label announced while `loading` is true. */
34
36
  loadingLabel?: string;
37
+ /** Icon rendered BEFORE the label, outside it. Use this rather than putting
38
+ * an icon in `children`: the label then gets its own box, which is the only
39
+ * place era's ink centring can reach (see era-label-ink). */
40
+ lead?: Snippet;
41
+ /** Icon rendered AFTER the label, outside it. Same reason as `lead`. */
42
+ trail?: Snippet;
35
43
  children?: Snippet;
36
44
  } = $props();
37
45
 
@@ -71,12 +79,22 @@
71
79
  forwards the button's own gap so the icon/label spacing is unchanged.
72
80
  -->
73
81
  <!--
74
- not-has-[svg]:era-label-ink a text-only button ink-centres its label.
75
- The span is otherwise a flex box, and a flex box's bare text is an
76
- ANONYMOUS item that `text-box-trim` can never reach, so the label fell back
77
- to line-box centring by font metrics (the bug era-label-ink documents).
78
- With an icon present the box must stay flex for the gap, and that case is
79
- still on the fallback typography/inert-trim reports it.
82
+ THE LABEL GETS ITS OWN BOX, which is what `lead`/`trail` are for.
83
+
84
+ Bare text in a flex container is an ANONYMOUS flex item, and
85
+ `text-box-trim` can never reach one so a label beside an icon is centred
86
+ by font metrics rather than by its typographic band, and no CSS can change
87
+ that. A <span> that is a flex item is BLOCKIFIED, so the trim does apply to
88
+ it; putting the icon OUTSIDE that span is what removes the anonymous item.
89
+
90
+ WHAT THIS IS AND IS NOT WORTH: measured on a real button across all four
91
+ density modes and all three shipped stacks, the legacy shape and this one
92
+ both centre their ink to 0px. So this is not a pixel fix — it is an API
93
+ fix. It makes the label ADDRESSABLE, which is what turns
94
+ typography/anonymous-text from a law era permanently violates into one it
95
+ can satisfy, and it is the shape every comparable library uses for
96
+ icon+label. The legacy `<Button><Check /> Save</Button>` still renders
97
+ unchanged, and the audit goes on reporting it.
80
98
  -->
81
99
  <span
82
100
  data-button-content
@@ -86,6 +104,18 @@
86
104
  <LoaderCircle class="size-icon shrink-0 animate-spin" aria-hidden="true" />
87
105
  {#if loadingLabel}<span class="sr-only">{loadingLabel}</span>{/if}
88
106
  {/if}
89
- {@render children?.()}
107
+ <!-- The label box exists ONLY when `lead`/`trail` are used. Rendering it
108
+ unconditionally changed the DOM for every legacy call site: a
109
+ `display:contents` span has no box, and the geometry audits walk real
110
+ boxes, so an icon button's nesting measurements went to pieces. Opting
111
+ in keeps the old shape byte-identical and gives the new shape a real
112
+ box for the trim to act on. -->
113
+ {#if lead || trail}
114
+ {@render lead?.()}
115
+ {#if children}<span class="era-text-trim">{@render children()}</span>{/if}
116
+ {@render trail?.()}
117
+ {:else}
118
+ {@render children?.()}
119
+ {/if}
90
120
  </span>
91
121
  </Button.Root>
@@ -14,6 +14,12 @@ type $$ComponentProps = Button.RootProps & {
14
14
  loading?: boolean;
15
15
  /** Screen-reader-only label announced while `loading` is true. */
16
16
  loadingLabel?: string;
17
+ /** Icon rendered BEFORE the label, outside it. Use this rather than putting
18
+ * an icon in `children`: the label then gets its own box, which is the only
19
+ * place era's ink centring can reach (see era-label-ink). */
20
+ lead?: Snippet;
21
+ /** Icon rendered AFTER the label, outside it. Same reason as `lead`. */
22
+ trail?: Snippet;
17
23
  children?: Snippet;
18
24
  };
19
25
  declare const Button: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -68,6 +68,7 @@
68
68
  ref = $bindable(null),
69
69
  size = 'chip',
70
70
  ondismiss,
71
+ lead,
71
72
  children,
72
73
  class: className,
73
74
  ...restProps
@@ -77,6 +78,10 @@
77
78
  * that nests inside an md container (a select trigger, a button). */
78
79
  size?: 'chip' | 'pill';
79
80
  ondismiss?: () => void;
81
+ /** Icon rendered BEFORE the label, outside it. Use this rather than putting
82
+ * an icon in `children`: the label then gets its own box, which is the only
83
+ * place era's ink centring can reach. */
84
+ lead?: Snippet;
80
85
  children?: Snippet;
81
86
  } = $props();
82
87
  </script>
@@ -86,7 +91,15 @@
86
91
  class={cn(chipVariants({ size, dismissible: !!ondismiss }), className)}
87
92
  {...restProps}
88
93
  >
89
- {@render children?.()}
94
+ {#if lead}
95
+ {@render lead()}
96
+ {/if}
97
+ <!-- The label box exists ONLY when `lead` is used. Rendering it for every call
98
+ site changed the DOM everywhere: a `display:contents` span has no box, and
99
+ the geometry audits walk real boxes. Opt-in keeps the legacy shape
100
+ byte-identical and gives the new shape a real box for the trim. -->
101
+ {#if lead}<span class="era-text-trim">{@render children?.()}</span
102
+ >{:else}{@render children?.()}{/if}
90
103
  {#if ondismiss}
91
104
  <!--
92
105
  Stop pointerdown/up from bubbling to ancestor triggers (Select.Trigger,
@@ -34,6 +34,10 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
34
34
  * that nests inside an md container (a select trigger, a button). */
35
35
  size?: 'chip' | 'pill';
36
36
  ondismiss?: () => void;
37
+ /** Icon rendered BEFORE the label, outside it. Use this rather than putting
38
+ * an icon in `children`: the label then gets its own box, which is the only
39
+ * place era's ink centring can reach. */
40
+ lead?: Snippet;
37
41
  children?: Snippet;
38
42
  };
39
43
  declare const Chip: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -15,7 +15,7 @@
15
15
  bind:ref
16
16
  align="start"
17
17
  class={cn(
18
- 'z-50 w-max min-w-(--bits-combobox-anchor-width) overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
18
+ 'z-overlay w-max min-w-(--bits-combobox-anchor-width) overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur data-[side=bottom]:rounded-t-none data-[side=top]:rounded-b-none',
19
19
  className
20
20
  )}
21
21
  {sideOffset}
@@ -266,7 +266,7 @@
266
266
  role="listbox"
267
267
  tabindex="-1"
268
268
  aria-label="Commands"
269
- class="absolute top-full left-0 z-50 max-h-64 w-full overflow-x-hidden overflow-y-auto bg-elevated shadow-lg glass-blur"
269
+ class="absolute top-full left-0 z-overlay max-h-64 w-full overflow-x-hidden overflow-y-auto bg-elevated shadow-lg glass-blur"
270
270
  onmousedown={(e) => e.preventDefault()}
271
271
  >
272
272
  {#if !filtered.length}
@@ -21,7 +21,7 @@
21
21
  // 2px frame that stops a full-width highlight from breaking their chrome.
22
22
  // A fixed gutter was tried here and rejected: it padded flat menus too.
23
23
  class={cn(
24
- 'z-50 w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
24
+ 'z-overlay w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
25
25
  className
26
26
  )}
27
27
  {sideOffset}
@@ -15,7 +15,7 @@
15
15
  <ContextMenu.SubContent
16
16
  bind:ref
17
17
  class={cn(
18
- 'z-50 w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
18
+ 'z-overlay w-max overflow-hidden rounded-control bg-elevated p-menu shadow-lg glass-blur',
19
19
  className
20
20
  )}
21
21
  {sideOffset}
@@ -14,7 +14,7 @@
14
14
  <DatePicker.Content
15
15
  bind:ref
16
16
  align="start"
17
- class={cn('z-50 rounded-bar bg-elevated p-content shadow-lg glass-blur', className)}
17
+ class={cn('z-overlay rounded-bar bg-elevated p-content shadow-lg glass-blur', className)}
18
18
  {sideOffset}
19
19
  {...restProps}
20
20
  >
@@ -14,7 +14,7 @@
14
14
  <DateRangePicker.Content
15
15
  bind:ref
16
16
  align="start"
17
- class={cn('z-50 rounded-bar bg-elevated p-content shadow-lg glass-blur', className)}
17
+ class={cn('z-overlay rounded-bar bg-elevated p-content shadow-lg glass-blur', className)}
18
18
  {sideOffset}
19
19
  {...restProps}
20
20
  >
@@ -30,7 +30,7 @@
30
30
  <Dialog.Content
31
31
  bind:ref
32
32
  class={cn(
33
- 'fixed top-1/2 left-1/2 z-50 flex w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col border border-divider bg-well shadow-lg glass-blur',
33
+ 'fixed top-1/2 left-1/2 z-overlay flex w-full max-w-md -translate-x-1/2 -translate-y-1/2 flex-col border border-divider bg-well shadow-lg glass-blur',
34
34
  header ? 'rounded-control' : 'gap-gutter rounded-bar p-card',
35
35
  className
36
36
  )}