@signal9/era-ui 30.7.0 → 31.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 (87) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/ai/conversations/conversations-item.svelte +1 -1
  3. package/dist/ai/plan/plan-header.svelte +1 -1
  4. package/dist/ai/prompt-input/prompt-input-attachments.svelte +2 -6
  5. package/dist/ai/swarm/swarm-node.svelte +1 -1
  6. package/dist/ai/todo/todo-header.svelte +1 -1
  7. package/dist/ai/todo/todo-item.svelte +1 -1
  8. package/dist/ai/tool/tool-chip.svelte +1 -1
  9. package/dist/ai/web-preview/web-preview.svelte +1 -1
  10. package/dist/dev/audit/audits/wall-is-isotropic.js +1 -1
  11. package/dist/docs/css-utilities.js +7 -3
  12. package/dist/era-ui.css +1 -1
  13. package/dist/generated-docs/badge.md +1 -1
  14. package/dist/generated-docs/bar.md +22 -4
  15. package/dist/generated-docs/button.md +38 -4
  16. package/dist/generated-docs/card.md +35 -5
  17. package/dist/generated-docs/chip.md +16 -0
  18. package/dist/generated-docs/llms-full.txt +310 -49
  19. package/dist/generated-docs/llms.txt +4 -3
  20. package/dist/generated-docs/manifest.json +45 -21
  21. package/dist/generated-docs/meter.md +79 -1
  22. package/dist/generated-docs/nav.md +8 -1
  23. package/dist/generated-docs/separator.md +18 -0
  24. package/dist/generated-docs/utilities.json +32 -9
  25. package/dist/generated-docs/utilities.md +89 -30
  26. package/dist/styles/density.css +15 -25
  27. package/dist/styles/index.css +32 -27
  28. package/dist/ui/accordion/accordion-trigger.svelte +1 -1
  29. package/dist/ui/alert-dialog/alert-dialog-action.svelte +2 -0
  30. package/dist/ui/alert-dialog/alert-dialog-cancel.svelte +2 -0
  31. package/dist/ui/badge/badge.svelte +8 -2
  32. package/dist/ui/badge/badge.svelte.d.ts +1 -1
  33. package/dist/ui/bar/bar.svelte +95 -26
  34. package/dist/ui/bar/bar.svelte.d.ts +71 -31
  35. package/dist/ui/button/button.svelte +38 -2
  36. package/dist/ui/button/button.svelte.d.ts +29 -0
  37. package/dist/ui/button/index.d.ts +2 -0
  38. package/dist/ui/button/variants.js +4 -9
  39. package/dist/ui/calendar/calendar-day.svelte +1 -1
  40. package/dist/ui/card/card.svelte +22 -9
  41. package/dist/ui/card/card.svelte.d.ts +19 -7
  42. package/dist/ui/chip/chip.svelte +5 -2
  43. package/dist/ui/chip/chip.svelte.d.ts +6 -6
  44. package/dist/ui/collapsible/collapsible-trigger.svelte +1 -1
  45. package/dist/ui/combobox/combobox-trigger.svelte +1 -1
  46. package/dist/ui/command/command-item.svelte +1 -1
  47. package/dist/ui/command/command-link-item.svelte +1 -1
  48. package/dist/ui/command/command-root.svelte +3 -5
  49. package/dist/ui/date-field/date-field-segment.svelte +1 -1
  50. package/dist/ui/date-picker/date-picker-day.svelte +1 -1
  51. package/dist/ui/date-picker/date-picker-segment.svelte +1 -1
  52. package/dist/ui/date-picker/date-picker-trigger.svelte +1 -1
  53. package/dist/ui/date-range-field/date-range-field-segment.svelte +1 -1
  54. package/dist/ui/date-range-picker/date-range-picker-day.svelte +1 -1
  55. package/dist/ui/date-range-picker/date-range-picker-segment.svelte +1 -1
  56. package/dist/ui/date-range-picker/date-range-picker-trigger.svelte +1 -1
  57. package/dist/ui/dialog/dialog-close.svelte +2 -0
  58. package/dist/ui/dialog/dialog-content.svelte +19 -1
  59. package/dist/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -1
  60. package/dist/ui/file-upload/file-upload.svelte +1 -1
  61. package/dist/ui/input/variants.d.ts +6 -6
  62. package/dist/ui/input/variants.js +2 -2
  63. package/dist/ui/kv/kv.svelte +1 -1
  64. package/dist/ui/menubar/menubar-root.svelte +1 -1
  65. package/dist/ui/menubar/menubar-trigger.svelte +1 -1
  66. package/dist/ui/nav/nav-bar.svelte +9 -4
  67. package/dist/ui/nav/nav-bar.svelte.d.ts +2 -1
  68. package/dist/ui/pane/pane-root.svelte +3 -12
  69. package/dist/ui/pane/pane-tab.svelte +1 -1
  70. package/dist/ui/select/select-trigger.svelte +1 -1
  71. package/dist/ui/slider/slider-thumb.svelte +1 -1
  72. package/dist/ui/step/step-summary.svelte +1 -1
  73. package/dist/ui/switch/switch.svelte +2 -2
  74. package/dist/ui/time-field/time-field-segment.svelte +1 -1
  75. package/dist/ui/time-range-field/time-range-field-segment.svelte +1 -1
  76. package/dist/ui/timeline/timeline-step.svelte +1 -1
  77. package/dist/ui/toggle-group/toggle-group-item.svelte +2 -2
  78. package/dist/ui/toggle-group/toggle-group-item.svelte.d.ts +6 -6
  79. package/dist/ui/toggle-group/toggle-group-root.svelte +1 -2
  80. package/dist/ui/toggle-group/toggle-group-root.svelte.d.ts +3 -3
  81. package/dist/ui/tooltip/tooltip-content.svelte +1 -1
  82. package/dist/ui/tree/tree-item.svelte +1 -1
  83. package/dist/ui/video-player/video-player.svelte +1 -1
  84. package/dist/utils/tw-merge-config.d.ts +3 -0
  85. package/dist/utils/tw-merge-config.js +4 -0
  86. package/package.json +4 -3
  87. package/skill/SKILL.md +17 -17
@@ -1,6 +1,28 @@
1
1
  <script lang="ts" module>
2
2
  import { cn, tv } from '../../utils/index.js';
3
3
 
4
+ export type BarSize = 'chip' | 'control' | 'bar';
5
+ export type BarContent = 'icon' | 'pill' | 'chip' | 'control';
6
+ export type BarGeometry =
7
+ | { size: 'chip'; content?: 'icon' | 'pill' }
8
+ | { size: 'control'; content?: 'icon' | 'pill' | 'chip' }
9
+ | { size?: 'bar'; content?: BarContent };
10
+
11
+ const defaultContent: Record<BarSize, BarContent> = {
12
+ chip: 'icon',
13
+ control: 'chip',
14
+ bar: 'control'
15
+ };
16
+
17
+ export function resolveBarContent(size: BarSize, content?: BarContent): BarContent {
18
+ const resolved = content ?? defaultContent[size];
19
+ const order = ['icon', 'pill', 'chip', 'control', 'bar'];
20
+ if (order.indexOf(resolved) >= order.indexOf(size)) {
21
+ throw new Error(`Bar size="${size}" cannot contain tier "${resolved}"`);
22
+ }
23
+ return resolved;
24
+ }
25
+
4
26
  // A Bar is a flex container sized to a tier height, holding smaller controls
5
27
  // at the even concentric gap. The height scales with density; pick the tier
6
28
  // that fits the content:
@@ -14,9 +36,9 @@
14
36
  base: 'flex items-center leading-none',
15
37
  variants: {
16
38
  size: {
17
- chip: 'h-chip rounded-chip',
18
- control: 'h-control rounded-control',
19
- bar: 'h-bar rounded-bar'
39
+ chip: 'tier-chip',
40
+ control: 'tier-control',
41
+ bar: 'tier-bar'
20
42
  },
21
43
  /**
22
44
  * The tier of the CONTROLS inside, because that is what the bar's own
@@ -26,11 +48,10 @@
26
48
  * had each restated the two-tier value by hand, and the geometry spec had
27
49
  * been rewritten twice chasing which token they were restating.
28
50
  *
29
- * - `tier` (default) one tier smaller, the pairing above. Each size gets
30
- * its OWN token, from the compounds below.
31
- * - `pill` the compact pairing: pills and fields (a filter beside a
32
- * pill-tier icon button, a title bar). Two tiers down, so the inset
33
- * differs per size and also comes from the compounds.
51
+ * Name the ACTUAL tier inside. Omitting it means the next canonical rung:
52
+ * chip⊃icon, control⊃chip, bar⊃control. There is deliberately no `tier`
53
+ * shorthand in the public API: it hid which pair supplied the inset and
54
+ * taught the false claim that every adjacent pair had the same value.
34
55
  *
35
56
  * THIS USED TO BE ONE CLASS FOR ALL THREE SIZES — `px-icon-inset-chip`,
36
57
  * on the stated reasoning that "chip⊃icon, control⊃chip and bar⊃control
@@ -53,8 +74,10 @@
53
74
  * compared ACROSS the axes. layout/wall-is-isotropic now does.
54
75
  */
55
76
  content: {
56
- tier: '',
57
- pill: ''
77
+ icon: '',
78
+ pill: '',
79
+ chip: '',
80
+ control: ''
58
81
  },
59
82
  /**
60
83
  * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
@@ -80,27 +103,34 @@
80
103
  }
81
104
  },
82
105
  compoundVariants: [
83
- // content="tier" — one tier down, each pairing named for itself.
84
106
  // chip ⊃ icon = sp/2.
85
107
  {
86
108
  size: 'chip',
87
- content: 'tier',
109
+ content: 'icon',
88
110
  class: 'gap-icon-inset-chip px-icon-inset-chip'
89
111
  },
90
112
  // control ⊃ chip = (sp−6)/2 — the tight-but-present rung, 1/2/3.
91
113
  {
92
114
  size: 'control',
93
- content: 'tier',
115
+ content: 'chip',
94
116
  class: 'gap-chip-inset-control px-chip-inset-control'
95
117
  },
96
- // bar ⊃ control = sp/2, which is exactly --era-gap. era already calls
97
- // this pairing "the gutter" (see the rounded-row derivation in
98
- // styles/index.css), so it takes that name rather than borrowing
99
- // icon-inset-chip's — same number, but a reader can tell why.
118
+ // bar ⊃ control happens to equal the gutter, but it keeps a pair-specific
119
+ // name so geometry never borrows a layout token by numerical coincidence.
100
120
  {
101
121
  size: 'bar',
102
- content: 'tier',
103
- class: 'gap-gutter px-gutter'
122
+ content: 'control',
123
+ class: 'gap-control-inset-bar px-control-inset-bar'
124
+ },
125
+ {
126
+ size: 'control',
127
+ content: 'icon',
128
+ class: 'gap-icon-inset-control px-icon-inset-control'
129
+ },
130
+ {
131
+ size: 'bar',
132
+ content: 'icon',
133
+ class: 'gap-icon-inset-bar px-icon-inset-bar'
104
134
  },
105
135
  // control ⊃ pill = (3sp − sp − 10)/2 — the named token.
106
136
  {
@@ -115,6 +145,11 @@
115
145
  content: 'pill',
116
146
  class: 'gap-pill-inset-bar px-pill-inset-bar'
117
147
  },
148
+ {
149
+ size: 'bar',
150
+ content: 'chip',
151
+ class: 'gap-chip-inset-bar px-chip-inset-bar'
152
+ },
118
153
  // chip ⊃ pill — its own token, not the text inset it used to borrow. The two
119
154
  // were the same number only while type was 14px at every density; dense's
120
155
  // 12px broke the tie and left this pairing a pixel wide.
@@ -124,7 +159,7 @@
124
159
  class: 'gap-pill-inset-chip px-pill-inset-chip'
125
160
  }
126
161
  ],
127
- defaultVariants: { size: 'bar', content: 'tier', divider: false }
162
+ defaultVariants: { size: 'bar', content: 'control', divider: false }
128
163
  });
129
164
  </script>
130
165
 
@@ -135,8 +170,9 @@
135
170
  let {
136
171
  ref = $bindable(null),
137
172
  size = 'bar',
138
- content = 'tier',
173
+ content,
139
174
  divider = false,
175
+ topology = 'nested',
140
176
  children,
141
177
  class: className,
142
178
  ...restProps
@@ -146,16 +182,49 @@
146
182
  size?: 'chip' | 'control' | 'bar';
147
183
  /**
148
184
  * Tier of the controls inside — drives the bar's padding and gap so they
149
- * stay concentric to them. `tier` (default) is one tier smaller; `pill` is
150
- * the compact pairing (pills/fields in an sm or md bar).
185
+ * stay concentric to them. Omit this for the next canonical rung, or name
186
+ * the actual tier when the contents are smaller.
187
+ */
188
+ content?: BarContent;
189
+ /**
190
+ * The flush cap-a-panel chrome: an outset rule below, and a rounded
191
+ * BOTTOM only — the top defers to the panel's own radius.
192
+ *
193
+ * THE BOTTOM RADIUS IS LOAD-BEARING FOR THE RULE. The divider is an
194
+ * outset `box-shadow`, so it follows the box's corners: the rounded
195
+ * bottom is what curves the line up at the ends and makes the bar read
196
+ * as a long pill. Overriding the radius therefore changes the RULE, not
197
+ * just the corners. Measured against `cn`:
198
+ *
199
+ * - `rounded-none` drops both — a square bar with a dead-straight rule.
200
+ * That is the intended square mode (an in-app Notes bar passes it).
201
+ * - `rounded-b-*` or `rounded-b-none` replaces the curve — this is the
202
+ * one that surprises, because the rule changes shape when the caller
203
+ * thought they were only squaring a corner.
204
+ * - `rounded-t-*` is SAFE: it wins against `rounded-t-none` and leaves
205
+ * `rounded-b-control` standing, so the curve survives.
206
+ *
207
+ * A consumer lost the curve this way. The coupling was already written
208
+ * down — on the variant, inside `<script module>`, where someone
209
+ * hovering this prop never reaches it. Docs are only as good as the
210
+ * place the reader is standing.
151
211
  */
152
- content?: 'tier' | 'pill';
153
- /** The flush cap-a-panel chrome: outset divider below, bottom rd-md. */
154
212
  divider?: boolean;
213
+ /** `docked` removes one or more exposed curves; strict radius nesting does not apply. */
214
+ topology?: 'nested' | 'docked';
155
215
  children?: Snippet;
156
216
  } = $props();
217
+
218
+ const resolvedContent = $derived(resolveBarContent(size, content));
157
219
  </script>
158
220
 
159
- <div bind:this={ref} class={cn(barVariants({ size, content, divider }), className)} {...restProps}>
221
+ <div
222
+ bind:this={ref}
223
+ data-era-geometry={divider ? 'docked' : topology}
224
+ data-era-tier={size}
225
+ data-era-contains={resolvedContent}
226
+ class={cn(barVariants({ size, content: resolvedContent, divider }), className)}
227
+ {...restProps}
228
+ >
160
229
  {@render children?.()}
161
230
  </div>
@@ -1,8 +1,21 @@
1
+ export type BarSize = 'chip' | 'control' | 'bar';
2
+ export type BarContent = 'icon' | 'pill' | 'chip' | 'control';
3
+ export type BarGeometry = {
4
+ size: 'chip';
5
+ content?: 'icon' | 'pill';
6
+ } | {
7
+ size: 'control';
8
+ content?: 'icon' | 'pill' | 'chip';
9
+ } | {
10
+ size?: 'bar';
11
+ content?: BarContent;
12
+ };
13
+ export declare function resolveBarContent(size: BarSize, content?: BarContent): BarContent;
1
14
  export declare const barVariants: import("tailwind-variants").TVReturnType<{
2
15
  size: {
3
- chip: "h-chip rounded-chip";
4
- control: "h-control rounded-control";
5
- bar: "h-bar rounded-bar";
16
+ chip: "tier-chip";
17
+ control: "tier-control";
18
+ bar: "tier-bar";
6
19
  };
7
20
  /**
8
21
  * The tier of the CONTROLS inside, because that is what the bar's own
@@ -12,11 +25,10 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
12
25
  * had each restated the two-tier value by hand, and the geometry spec had
13
26
  * been rewritten twice chasing which token they were restating.
14
27
  *
15
- * - `tier` (default) one tier smaller, the pairing above. Each size gets
16
- * its OWN token, from the compounds below.
17
- * - `pill` the compact pairing: pills and fields (a filter beside a
18
- * pill-tier icon button, a title bar). Two tiers down, so the inset
19
- * differs per size and also comes from the compounds.
28
+ * Name the ACTUAL tier inside. Omitting it means the next canonical rung:
29
+ * chip⊃icon, control⊃chip, bar⊃control. There is deliberately no `tier`
30
+ * shorthand in the public API: it hid which pair supplied the inset and
31
+ * taught the false claim that every adjacent pair had the same value.
20
32
  *
21
33
  * THIS USED TO BE ONE CLASS FOR ALL THREE SIZES — `px-icon-inset-chip`,
22
34
  * on the stated reasoning that "chip⊃icon, control⊃chip and bar⊃control
@@ -39,8 +51,10 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
39
51
  * compared ACROSS the axes. layout/wall-is-isotropic now does.
40
52
  */
41
53
  content: {
42
- tier: "";
54
+ icon: "";
43
55
  pill: "";
56
+ chip: "";
57
+ control: "";
44
58
  };
45
59
  /**
46
60
  * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
@@ -66,9 +80,9 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
66
80
  };
67
81
  }, undefined, "flex items-center leading-none", {
68
82
  size: {
69
- chip: "h-chip rounded-chip";
70
- control: "h-control rounded-control";
71
- bar: "h-bar rounded-bar";
83
+ chip: "tier-chip";
84
+ control: "tier-control";
85
+ bar: "tier-bar";
72
86
  };
73
87
  /**
74
88
  * The tier of the CONTROLS inside, because that is what the bar's own
@@ -78,11 +92,10 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
78
92
  * had each restated the two-tier value by hand, and the geometry spec had
79
93
  * been rewritten twice chasing which token they were restating.
80
94
  *
81
- * - `tier` (default) one tier smaller, the pairing above. Each size gets
82
- * its OWN token, from the compounds below.
83
- * - `pill` the compact pairing: pills and fields (a filter beside a
84
- * pill-tier icon button, a title bar). Two tiers down, so the inset
85
- * differs per size and also comes from the compounds.
95
+ * Name the ACTUAL tier inside. Omitting it means the next canonical rung:
96
+ * chip⊃icon, control⊃chip, bar⊃control. There is deliberately no `tier`
97
+ * shorthand in the public API: it hid which pair supplied the inset and
98
+ * taught the false claim that every adjacent pair had the same value.
86
99
  *
87
100
  * THIS USED TO BE ONE CLASS FOR ALL THREE SIZES — `px-icon-inset-chip`,
88
101
  * on the stated reasoning that "chip⊃icon, control⊃chip and bar⊃control
@@ -105,8 +118,10 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
105
118
  * compared ACROSS the axes. layout/wall-is-isotropic now does.
106
119
  */
107
120
  content: {
108
- tier: "";
121
+ icon: "";
109
122
  pill: "";
123
+ chip: "";
124
+ control: "";
110
125
  };
111
126
  /**
112
127
  * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
@@ -132,9 +147,9 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
132
147
  };
133
148
  }, undefined, import("tailwind-variants").TVReturnTypeLike<{
134
149
  size: {
135
- chip: "h-chip rounded-chip";
136
- control: "h-control rounded-control";
137
- bar: "h-bar rounded-bar";
150
+ chip: "tier-chip";
151
+ control: "tier-control";
152
+ bar: "tier-bar";
138
153
  };
139
154
  /**
140
155
  * The tier of the CONTROLS inside, because that is what the bar's own
@@ -144,11 +159,10 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
144
159
  * had each restated the two-tier value by hand, and the geometry spec had
145
160
  * been rewritten twice chasing which token they were restating.
146
161
  *
147
- * - `tier` (default) one tier smaller, the pairing above. Each size gets
148
- * its OWN token, from the compounds below.
149
- * - `pill` the compact pairing: pills and fields (a filter beside a
150
- * pill-tier icon button, a title bar). Two tiers down, so the inset
151
- * differs per size and also comes from the compounds.
162
+ * Name the ACTUAL tier inside. Omitting it means the next canonical rung:
163
+ * chip⊃icon, control⊃chip, bar⊃control. There is deliberately no `tier`
164
+ * shorthand in the public API: it hid which pair supplied the inset and
165
+ * taught the false claim that every adjacent pair had the same value.
152
166
  *
153
167
  * THIS USED TO BE ONE CLASS FOR ALL THREE SIZES — `px-icon-inset-chip`,
154
168
  * on the stated reasoning that "chip⊃icon, control⊃chip and bar⊃control
@@ -171,8 +185,10 @@ export declare const barVariants: import("tailwind-variants").TVReturnType<{
171
185
  * compared ACROSS the axes. layout/wall-is-isotropic now does.
172
186
  */
173
187
  content: {
174
- tier: "";
188
+ icon: "";
175
189
  pill: "";
190
+ chip: "";
191
+ control: "";
176
192
  };
177
193
  /**
178
194
  * The flush header bar capping a panel — Pane.Handle, Dialog.Header,
@@ -205,12 +221,36 @@ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
205
221
  size?: 'chip' | 'control' | 'bar';
206
222
  /**
207
223
  * Tier of the controls inside — drives the bar's padding and gap so they
208
- * stay concentric to them. `tier` (default) is one tier smaller; `pill` is
209
- * the compact pairing (pills/fields in an sm or md bar).
224
+ * stay concentric to them. Omit this for the next canonical rung, or name
225
+ * the actual tier when the contents are smaller.
226
+ */
227
+ content?: BarContent;
228
+ /**
229
+ * The flush cap-a-panel chrome: an outset rule below, and a rounded
230
+ * BOTTOM only — the top defers to the panel's own radius.
231
+ *
232
+ * THE BOTTOM RADIUS IS LOAD-BEARING FOR THE RULE. The divider is an
233
+ * outset `box-shadow`, so it follows the box's corners: the rounded
234
+ * bottom is what curves the line up at the ends and makes the bar read
235
+ * as a long pill. Overriding the radius therefore changes the RULE, not
236
+ * just the corners. Measured against `cn`:
237
+ *
238
+ * - `rounded-none` drops both — a square bar with a dead-straight rule.
239
+ * That is the intended square mode (an in-app Notes bar passes it).
240
+ * - `rounded-b-*` or `rounded-b-none` replaces the curve — this is the
241
+ * one that surprises, because the rule changes shape when the caller
242
+ * thought they were only squaring a corner.
243
+ * - `rounded-t-*` is SAFE: it wins against `rounded-t-none` and leaves
244
+ * `rounded-b-control` standing, so the curve survives.
245
+ *
246
+ * A consumer lost the curve this way. The coupling was already written
247
+ * down — on the variant, inside `<script module>`, where someone
248
+ * hovering this prop never reaches it. Docs are only as good as the
249
+ * place the reader is standing.
210
250
  */
211
- content?: 'tier' | 'pill';
212
- /** The flush cap-a-panel chrome: outset divider below, bottom rd-md. */
213
251
  divider?: boolean;
252
+ /** `docked` removes one or more exposed curves; strict radius nesting does not apply. */
253
+ topology?: 'nested' | 'docked';
214
254
  children?: Snippet;
215
255
  };
216
256
  declare const Bar: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -13,11 +13,13 @@
13
13
  active = false,
14
14
  icon = false,
15
15
  loading = false,
16
+ busy = false,
16
17
  loadingLabel,
17
18
  disabled,
18
19
  lead,
19
20
  trail,
20
21
  caps = false,
22
+ topology = 'bounded',
21
23
  children,
22
24
  class: className,
23
25
  ...restProps
@@ -47,8 +49,35 @@
47
49
  *
48
50
  * The visible label is therefore HIDDEN in flight — the one thing this
49
51
  * trades away, and the reason `loadingLabel` exists.
52
+ *
53
+ * IF YOU DO NOT WANT A SPINNER, use `busy`. Not showing one is a
54
+ * supported posture, not a legacy one — see that prop.
50
55
  */
51
56
  loading?: boolean;
57
+ /**
58
+ * In flight, WITHOUT a spinner: disables the button and sets `aria-busy`,
59
+ * and changes nothing else. The label stays visible and legible.
60
+ *
61
+ * This exists because "show a spinner" is a taste, not a requirement,
62
+ * and era had only one answer. A consumer whose house style is that
63
+ * spinners should not appear had to spell the posture out themselves —
64
+ * `disabled` plus a restated `aria-busy` — and rely on knowing that
65
+ * `restProps` spreads last, which is a real fact about this component
66
+ * that no reader should have to depend on. Two words for two postures
67
+ * is clearer than one word plus a workaround.
68
+ *
69
+ * `loading` implies this: it is `busy` plus the spinner and the hidden
70
+ * label. What you get from `busy` alone is a control that is visibly
71
+ * disabled (the era-interactive opacity) and announced as busy, with
72
+ * its geometry and its ink untouched — which is also why it needs no
73
+ * `loadingLabel`: nothing about the button changed except that you
74
+ * cannot press it.
75
+ *
76
+ * Reported by a consumer whose user asked for no spinners anywhere.
77
+ * They were right that a fix for the width shift did not answer it: a
78
+ * spinner that no longer moves its neighbours is still a spinner.
79
+ */
80
+ busy?: boolean;
52
81
  /** Screen-reader-only label announced while `loading` is true. */
53
82
  loadingLabel?: string;
54
83
  /** Icon rendered BEFORE the label, outside it. Use this rather than putting
@@ -98,10 +127,15 @@
98
127
  * which is the whole reason those snippets exist.
99
128
  */
100
129
  caps?: boolean;
130
+ /** `docked` joins this control to a neighbour and removes one or more curves. */
131
+ topology?: 'bounded' | 'docked';
101
132
  children?: Snippet;
102
133
  } = $props();
103
134
 
104
- const effectiveDisabled = $derived(disabled || loading);
135
+ // `loading` is `busy` plus a spinner, so everything that is true of busy is
136
+ // true of loading. Only the spinner and the hidden label read `loading`.
137
+ const inFlight = $derived(loading || busy);
138
+ const effectiveDisabled = $derived(disabled || inFlight);
105
139
  </script>
106
140
 
107
141
  <!--
@@ -122,9 +156,11 @@
122
156
  <Button.Root
123
157
  bind:ref
124
158
  disabled={effectiveDisabled}
125
- aria-busy={loading || undefined}
159
+ aria-busy={inFlight || undefined}
126
160
  data-state={active ? 'on' : undefined}
127
161
  data-size={size}
162
+ data-era-geometry={variant === 'filled' ? topology : undefined}
163
+ data-era-tier={variant === 'filled' ? size : undefined}
128
164
  class={cn(
129
165
  buttonVariants({ variant, tone, size, icon, active }),
130
166
  // Positioned only while loading — the spinner overlay below needs a
@@ -26,8 +26,35 @@ type $$ComponentProps = Button.RootProps & {
26
26
  *
27
27
  * The visible label is therefore HIDDEN in flight — the one thing this
28
28
  * trades away, and the reason `loadingLabel` exists.
29
+ *
30
+ * IF YOU DO NOT WANT A SPINNER, use `busy`. Not showing one is a
31
+ * supported posture, not a legacy one — see that prop.
29
32
  */
30
33
  loading?: boolean;
34
+ /**
35
+ * In flight, WITHOUT a spinner: disables the button and sets `aria-busy`,
36
+ * and changes nothing else. The label stays visible and legible.
37
+ *
38
+ * This exists because "show a spinner" is a taste, not a requirement,
39
+ * and era had only one answer. A consumer whose house style is that
40
+ * spinners should not appear had to spell the posture out themselves —
41
+ * `disabled` plus a restated `aria-busy` — and rely on knowing that
42
+ * `restProps` spreads last, which is a real fact about this component
43
+ * that no reader should have to depend on. Two words for two postures
44
+ * is clearer than one word plus a workaround.
45
+ *
46
+ * `loading` implies this: it is `busy` plus the spinner and the hidden
47
+ * label. What you get from `busy` alone is a control that is visibly
48
+ * disabled (the era-interactive opacity) and announced as busy, with
49
+ * its geometry and its ink untouched — which is also why it needs no
50
+ * `loadingLabel`: nothing about the button changed except that you
51
+ * cannot press it.
52
+ *
53
+ * Reported by a consumer whose user asked for no spinners anywhere.
54
+ * They were right that a fix for the width shift did not answer it: a
55
+ * spinner that no longer moves its neighbours is still a spinner.
56
+ */
57
+ busy?: boolean;
31
58
  /** Screen-reader-only label announced while `loading` is true. */
32
59
  loadingLabel?: string;
33
60
  /** Icon rendered BEFORE the label, outside it. Use this rather than putting
@@ -77,6 +104,8 @@ type $$ComponentProps = Button.RootProps & {
77
104
  * which is the whole reason those snippets exist.
78
105
  */
79
106
  caps?: boolean;
107
+ /** `docked` joins this control to a neighbour and removes one or more curves. */
108
+ topology?: 'bounded' | 'docked';
80
109
  children?: Snippet;
81
110
  };
82
111
  declare const Button: import("svelte").Component<$$ComponentProps, {}, "ref">;
@@ -8,5 +8,7 @@ export interface ButtonVariants {
8
8
  icon?: boolean;
9
9
  active?: boolean;
10
10
  loading?: boolean;
11
+ /** In flight without a spinner — see button.svelte. */
12
+ busy?: boolean;
11
13
  loadingLabel?: string;
12
14
  }
@@ -40,27 +40,22 @@ export const buttonVariants = tv({
40
40
  {
41
41
  variant: 'filled',
42
42
  size: 'control',
43
- class: 'h-control min-w-control rounded-control'
43
+ class: 'min-w-control tier-control'
44
44
  },
45
45
  {
46
46
  variant: 'filled',
47
47
  size: 'chip',
48
- class: 'h-chip min-w-chip rounded-chip'
48
+ class: 'min-w-chip tier-chip'
49
49
  },
50
50
  {
51
51
  variant: 'filled',
52
52
  size: 'pill',
53
- class: 'h-pill min-w-pill rounded-pill'
53
+ class: 'min-w-pill tier-pill'
54
54
  },
55
55
  {
56
56
  variant: 'filled',
57
57
  size: 'icon',
58
- class: 'h-icon min-w-icon rounded-icon'
59
- },
60
- {
61
- variant: 'filled',
62
- size: 'icon',
63
- class: 'h-icon min-w-icon rounded-icon'
58
+ class: 'min-w-icon tier-icon'
64
59
  },
65
60
  // filled insets; icon buttons go square instead. When an icon is present,
66
61
  // gap widens to match the wall inset so the icon sits in rhythm with it.
@@ -15,7 +15,7 @@
15
15
  <Calendar.Day
16
16
  bind:ref
17
17
  class={cn(
18
- 'era-token-ink size-control cursor-default rounded-control text-body text-fg hover:bg-highlight focus:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[outside-month]:pointer-events-none data-[outside-month]:text-muted/40 data-[selected]:bg-hover data-[selected]:text-bright data-[unavailable]:text-muted data-[unavailable]:line-through',
18
+ 'era-token-ink tier-control w-control cursor-default text-body text-fg hover:bg-highlight focus:outline-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[outside-month]:pointer-events-none data-[outside-month]:text-muted/40 data-[selected]:bg-hover data-[selected]:text-bright data-[unavailable]:text-muted data-[unavailable]:line-through',
19
19
  className
20
20
  )}
21
21
  {...restProps}
@@ -39,10 +39,11 @@
39
39
  // chisel, that is the difference between a control and a picture of
40
40
  // one. Inert on flat (the token is `none`) and on glass (it resolves
41
41
  // to the resting shadow), so only the surface that has a pressed face
42
- // shows it. No content sink to go with it: the sink moves a control's
43
- // single content wrapper, and a card's children are the consumer's.
42
+ // shows it. Move one library-owned content layer with the face: transforming
43
+ // consumer children separately would pull a card's sections apart, while no
44
+ // wrapper leaves the frame depressed around stationary ink.
44
45
  interactive: {
45
- true: 'cursor-pointer hover:bg-highlight hover:shadow-highlight focus-visible:bg-highlight active:shadow-pressed era-interactive aria-disabled:cursor-not-allowed aria-disabled:opacity-50'
46
+ true: 'cursor-pointer hover:bg-highlight hover:shadow-highlight focus-visible:bg-highlight active:shadow-pressed [&:active>[data-card-content]]:translate-x-(--era-press-sink-x) [&:active>[data-card-content]]:translate-y-(--era-press-sink-y) era-interactive aria-disabled:cursor-not-allowed aria-disabled:opacity-50'
46
47
  }
47
48
  },
48
49
  defaultVariants: { tone: 'elevated', sectioned: false, interactive: false }
@@ -124,10 +125,22 @@
124
125
  * A CARD THAT IS PRESSABLE SHOULD USUALLY BE `elevated`. Passing `href`
125
126
  * or `onclick` turns on the interactive face — hover, focus, press — but
126
127
  * it deliberately does NOT override the tier you asked for, so a
127
- * `surface` link card has no resting lift and reads flatter than the
128
- * static cards around it. That combination was reported as a bug and it
129
- * is a choice: a tone that silently changed under an event handler would
130
- * be a worse one.
128
+ * `surface` link card does not get the raised tier's separation and can
129
+ * read flatter than the static cards around it. That combination was
130
+ * reported as a bug and it is a choice: a tone that silently changed
131
+ * under an event handler would be a worse one.
132
+ *
133
+ * HOW THE TIERS DIFFER IS SURFACE-DEPENDENT, so do not go looking for a
134
+ * fill difference on every surface. `base` mixes the elevated fill 6%
135
+ * toward `--color-bright` and adds a resting shadow, so there the tiers
136
+ * differ in COLOUR. `bevel` sets `--era-surface-bg-elevated` to
137
+ * `--era-surface-bg` outright — elevation is a chisel, not a tone, which
138
+ * is bevel's whole premise — so `surface` and `elevated` paint the
139
+ * IDENTICAL fill there and differ only in relief. A consumer measured
140
+ * exactly that on 30.8.0 bevel: both tones `oklch(0.195 0 none)` in dark,
141
+ * `0.863` in light, one variable, side by side. `glass` splits the tiers
142
+ * its own way again. So the tier is real on every surface; the thing
143
+ * carrying it is not always the fill.
131
144
  */
132
145
  tone?: 'elevated' | 'surface' | 'well' | 'flat';
133
146
  /** Rendered above the body with a subtle divider. */
@@ -178,7 +191,7 @@
178
191
  class={cn(cardVariants({ tone, sectioned, interactive: true }), className)}
179
192
  {...restProps}
180
193
  >
181
- {@render body()}
194
+ <div data-card-content>{@render body()}</div>
182
195
  </a>
183
196
  <!-- eslint-enable svelte/no-navigation-without-resolve -->
184
197
  {:else if onclick != null}
@@ -198,7 +211,7 @@
198
211
  class={cn(cardVariants({ tone, sectioned, interactive: true }), className)}
199
212
  {...restProps}
200
213
  >
201
- {@render body()}
214
+ <div data-card-content>{@render body()}</div>
202
215
  </button>
203
216
  {:else}
204
217
  <div