@urbicon-ui/blocks 6.40.4 → 6.43.2

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 (107) hide show
  1. package/README.md +1 -1
  2. package/dist/components/Calendar/calendar.variants.d.ts +63 -63
  3. package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
  4. package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
  5. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
  6. package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
  7. package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
  8. package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
  9. package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
  10. package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
  11. package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
  12. package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
  13. package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
  14. package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
  15. package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
  16. package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
  17. package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
  18. package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
  19. package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
  20. package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
  21. package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
  22. package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
  23. package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
  24. package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
  25. package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
  26. package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
  27. package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
  28. package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
  29. package/dist/components/Chat/A2UIView/index.d.ts +35 -2
  30. package/dist/components/Chat/A2UIView/index.js +13 -0
  31. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
  32. package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
  33. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
  34. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
  35. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
  36. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
  37. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
  38. package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
  39. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
  40. package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
  41. package/dist/components/Chat/ChatMessage/ChatMessage.svelte +93 -67
  42. package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +26 -19
  43. package/dist/components/Chat/ChatMessage/chat-message.variants.js +45 -12
  44. package/dist/components/Chat/ChatMessage/index.d.ts +20 -5
  45. package/dist/components/Chat/CodeBlock/CodeBlock.svelte +59 -33
  46. package/dist/components/Chat/CodeBlock/code-block.variants.d.ts +7 -0
  47. package/dist/components/Chat/CodeBlock/code-block.variants.js +28 -13
  48. package/dist/components/Chat/CodeBlock/index.d.ts +39 -4
  49. package/dist/components/Chat/PromptInput/PromptInput.svelte +9 -16
  50. package/dist/components/Chat/PromptInput/prompt-input.variants.js +16 -15
  51. package/dist/components/Chat/ToolCallCard/ToolCallCard.svelte +9 -4
  52. package/dist/components/Chat/ToolCallCard/index.d.ts +4 -3
  53. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.d.ts +0 -3
  54. package/dist/components/Chat/ToolCallCard/tool-call-card.variants.js +4 -4
  55. package/dist/components/CopyButton/CopyButton.svelte +9 -19
  56. package/dist/components/CopyButton/copy-button.variants.d.ts +4 -4
  57. package/dist/components/FileUpload/fileUpload.variants.js +15 -4
  58. package/dist/components/PinInput/PinInput.svelte +1 -0
  59. package/dist/components/PinInput/pin-input.variants.js +14 -1
  60. package/dist/components/TimeInput/time-input.variants.js +16 -1
  61. package/dist/i18n/index.d.ts +398 -2
  62. package/dist/internal/copy-state.svelte.d.ts +60 -0
  63. package/dist/internal/copy-state.svelte.js +76 -0
  64. package/dist/internal/field-chrome.d.ts +18 -1
  65. package/dist/internal/field-chrome.js +19 -2
  66. package/dist/primitives/Alert/alert.variants.d.ts +8 -8
  67. package/dist/primitives/Avatar/avatar.variants.js +4 -1
  68. package/dist/primitives/Badge/badge.variants.js +11 -2
  69. package/dist/primitives/Button/button.variants.js +4 -1
  70. package/dist/primitives/Card/card.variants.js +4 -1
  71. package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
  72. package/dist/primitives/Combobox/Combobox.svelte +9 -1
  73. package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
  74. package/dist/primitives/Combobox/combobox.variants.js +43 -7
  75. package/dist/primitives/Combobox/index.d.ts +8 -2
  76. package/dist/primitives/Dialog/dialog.variants.js +4 -1
  77. package/dist/primitives/Drawer/drawer.variants.js +4 -1
  78. package/dist/primitives/Input/input.variants.js +17 -1
  79. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
  80. package/dist/primitives/Menu/menu.variants.js +5 -1
  81. package/dist/primitives/Progress/progress.variants.js +5 -2
  82. package/dist/primitives/RadioGroup/radioGroup.variants.js +74 -10
  83. package/dist/primitives/Select/select.variants.js +14 -5
  84. package/dist/primitives/Slider/Slider.svelte +4 -1
  85. package/dist/primitives/Slider/slider.variants.js +9 -2
  86. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  87. package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
  88. package/dist/primitives/Stepper/stepper.variants.js +2 -1
  89. package/dist/primitives/Tab/tab.variants.js +4 -1
  90. package/dist/primitives/Textarea/textarea.variants.js +16 -2
  91. package/dist/primitives/Toast/toast.variants.d.ts +12 -12
  92. package/dist/primitives/Toast/toast.variants.js +5 -1
  93. package/dist/primitives/Toggle/toggle.variants.js +94 -18
  94. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  95. package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
  96. package/dist/style/foundation.css +19 -9
  97. package/dist/style/semantic.css +49 -2
  98. package/dist/style/themes/forest.css +2 -2
  99. package/dist/style/themes/neutral.css +2 -2
  100. package/dist/style/themes/ocean.css +2 -2
  101. package/dist/style/themes/rose.css +2 -2
  102. package/dist/style/themes/sunset.css +2 -2
  103. package/dist/utils/figma-token-export.js +7 -3
  104. package/dist/utils/persistent-state.svelte.d.ts +25 -1
  105. package/dist/utils/persistent-state.svelte.js +98 -10
  106. package/docs/MIGRATION-v5.md +29 -0
  107. package/package.json +3 -3
@@ -8,7 +8,26 @@ export const toggleVariants = tv({
8
8
  // rectangle switch that reads as inline-toolbar control.
9
9
  track: [
10
10
  'relative inline-flex items-center',
11
- 'transition-[color,background-color,border-color,box-shadow] duration-[var(--blocks-duration-fast)] ease-out',
11
+ 'transition-[color,background-color,border-color,box-shadow,scale] duration-[var(--blocks-duration-fast)] ease-out',
12
+ // Press feedback on the control surface — the same small-element press
13
+ // cue Checkbox took from Badge/Avatar (`scale-95`), with `group-active`
14
+ // so pressing the label squeezes the switch too. It rides the TRACK,
15
+ // not the thumb, for three reasons:
16
+ // 1. the track is the switch's control surface — Checkbox puts the
17
+ // cue on `box`, and the whole subtree (thumb included) squeezes
18
+ // with it, so one class covers the entire control;
19
+ // 2. the `dot` variant *hides* the thumb, so a thumb-mounted cue
20
+ // would silently vanish exactly where the control is smallest;
21
+ // 3. the thumb already drives motion through `translate` (its
22
+ // resting `-translate-y-1/2` plus the per-size `translate-x-*`).
23
+ // Tailwind 4 compiles `scale-*` to the discrete `scale` property,
24
+ // which CSS applies AFTER `translate` about the element's own
25
+ // centre — so a thumb-mounted cue would shrink the knob in place
26
+ // at whichever end of the track it sits, reading as a rendering
27
+ // glitch rather than a press.
28
+ // `scale` is in the transition list above, and reduced motion collapses
29
+ // `--blocks-duration-fast` to 1ms.
30
+ 'group-active:scale-95',
12
31
  'border border-transparent',
13
32
  'peer-focus-visible:ring-2 peer-focus-visible:ring-primary/50',
14
33
  'peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-surface-base'
@@ -104,7 +123,20 @@ export const toggleVariants = tv({
104
123
  variant: 'default',
105
124
  checked: false,
106
125
  class: {
107
- track: 'bg-surface-interactive',
126
+ // Off-state hover steps the boundary up to `border-emphasis` — the
127
+ // same destination Checkbox's unchecked `outlined` box reaches, on
128
+ // the border the track already reserves (`border border-transparent`
129
+ // in the slot base, so nothing shifts).
130
+ //
131
+ // A fill step is now possible — `surface-interactive-hover` (added
132
+ // 2026-07-25) is the rung the filled fields use, and it reads in both
133
+ // modes. The border step stays anyway, for a Toggle-specific reason:
134
+ // the `dot` variant hides the thumb, so the track IS the control, and a
135
+ // boundary change reads there where a fill wash does not. (The original
136
+ // reason was that no fill token worked at all — `surface-interactive`
137
+ // resolved to `surface-hover` in light and `surface-active` in dark.
138
+ // That is fixed; this is now a choice, not a constraint.)
139
+ track: 'bg-surface-interactive group-hover:border-border-emphasis',
108
140
  thumb: 'translate-x-0'
109
141
  }
110
142
  },
@@ -113,7 +145,11 @@ export const toggleVariants = tv({
113
145
  variant: 'default',
114
146
  checked: false,
115
147
  withBorder: true,
116
- class: { track: 'border-border-strong' }
148
+ // `border-strong` is the top of the border ladder, so the off-state
149
+ // hover above would *weaken* it (strong → emphasis). Pin the hover
150
+ // bucket to strong: an opt-in permanent boundary must not soften when
151
+ // the pointer arrives.
152
+ class: { track: 'border-border-strong group-hover:border-border-strong' }
117
153
  },
118
154
  {
119
155
  variant: 'default',
@@ -122,41 +158,58 @@ export const toggleVariants = tv({
122
158
  class: { track: 'ring-1 ring-inset ring-surface-base/40' }
123
159
  },
124
160
  // ── Checked track per intent (Switch-Pill only) ──
161
+ // Hover/active darken through the intent interaction-layer tokens — the
162
+ // same `bg-<intent>-hover` / `bg-<intent>-active` ladder Button and
163
+ // Checkbox use — via `group-*` so hovering/pressing the label counts too.
164
+ // The border stays on the base intent stop (as in Checkbox), leaving a
165
+ // hairline rim that makes the fill step legible.
125
166
  {
126
167
  variant: 'default',
127
168
  checked: true,
128
169
  intent: 'primary',
129
- class: { track: 'bg-primary border-primary' }
170
+ class: {
171
+ track: 'bg-primary border-primary group-hover:bg-primary-hover group-active:bg-primary-active'
172
+ }
130
173
  },
131
174
  {
132
175
  variant: 'default',
133
176
  checked: true,
134
177
  intent: 'secondary',
135
- class: { track: 'bg-secondary border-secondary' }
178
+ class: {
179
+ track: 'bg-secondary border-secondary group-hover:bg-secondary-hover group-active:bg-secondary-active'
180
+ }
136
181
  },
137
182
  {
138
183
  variant: 'default',
139
184
  checked: true,
140
185
  intent: 'success',
141
- class: { track: 'bg-success border-success' }
186
+ class: {
187
+ track: 'bg-success border-success group-hover:bg-success-hover group-active:bg-success-active'
188
+ }
142
189
  },
143
190
  {
144
191
  variant: 'default',
145
192
  checked: true,
146
193
  intent: 'warning',
147
- class: { track: 'bg-warning border-warning' }
194
+ class: {
195
+ track: 'bg-warning border-warning group-hover:bg-warning-hover group-active:bg-warning-active'
196
+ }
148
197
  },
149
198
  {
150
199
  variant: 'default',
151
200
  checked: true,
152
201
  intent: 'danger',
153
- class: { track: 'bg-danger border-danger' }
202
+ class: {
203
+ track: 'bg-danger border-danger group-hover:bg-danger-hover group-active:bg-danger-active'
204
+ }
154
205
  },
155
206
  {
156
207
  variant: 'default',
157
208
  checked: true,
158
209
  intent: 'neutral',
159
- class: { track: 'bg-neutral border-neutral' }
210
+ class: {
211
+ track: 'bg-neutral border-neutral group-hover:bg-neutral-hover group-active:bg-neutral-active'
212
+ }
160
213
  },
161
214
  // ── Error state: danger boundary on the unchecked track (Switch-Pill) ──
162
215
  // Mirrors Checkbox ("error overrides unchecked border"): the off state
@@ -165,11 +218,16 @@ export const toggleVariants = tv({
165
218
  // The dot-variant twin lives at the END of the compound list so it
166
219
  // folds over the dot-unchecked `border-border-default` (order is
167
220
  // semantic — later compounds win conflicting buckets).
221
+ // `group-hover:border-danger` pins the hover bucket as well — without it
222
+ // the off-state hover step above would repaint the error boundary neutral
223
+ // the moment the pointer arrives, which is exactly when the mark matters.
168
224
  {
169
225
  variant: 'default',
170
226
  error: true,
171
227
  checked: false,
172
- class: { track: 'border-danger peer-focus-visible:ring-danger/40' }
228
+ class: {
229
+ track: 'border-danger group-hover:border-danger peer-focus-visible:ring-danger/40'
230
+ }
173
231
  },
174
232
  // ── Thumb translation per size when checked (Switch-Pill only) ──
175
233
  { variant: 'default', checked: true, size: 'xs', class: { thumb: 'translate-x-4' } },
@@ -193,50 +251,68 @@ export const toggleVariants = tv({
193
251
  {
194
252
  variant: 'dot',
195
253
  checked: false,
196
- class: { track: 'border-border-default' }
254
+ // Off-state hover: `border-default` → `border-emphasis`, the exact
255
+ // ladder Checkbox's unchecked `outlined` box walks (the dot is
256
+ // outline-only, so its boundary is the whole control).
257
+ class: { track: 'border-border-default group-hover:border-border-emphasis' }
197
258
  },
259
+ // Same intent interaction layer as the Switch-Pill above — the dot is a
260
+ // filled intent surface too, just a 14px one.
198
261
  {
199
262
  variant: 'dot',
200
263
  checked: true,
201
264
  intent: 'primary',
202
- class: { track: 'border-primary bg-primary' }
265
+ class: {
266
+ track: 'border-primary bg-primary group-hover:bg-primary-hover group-active:bg-primary-active'
267
+ }
203
268
  },
204
269
  {
205
270
  variant: 'dot',
206
271
  checked: true,
207
272
  intent: 'secondary',
208
- class: { track: 'border-secondary bg-secondary' }
273
+ class: {
274
+ track: 'border-secondary bg-secondary group-hover:bg-secondary-hover group-active:bg-secondary-active'
275
+ }
209
276
  },
210
277
  {
211
278
  variant: 'dot',
212
279
  checked: true,
213
280
  intent: 'success',
214
- class: { track: 'border-success bg-success' }
281
+ class: {
282
+ track: 'border-success bg-success group-hover:bg-success-hover group-active:bg-success-active'
283
+ }
215
284
  },
216
285
  {
217
286
  variant: 'dot',
218
287
  checked: true,
219
288
  intent: 'warning',
220
- class: { track: 'border-warning bg-warning' }
289
+ class: {
290
+ track: 'border-warning bg-warning group-hover:bg-warning-hover group-active:bg-warning-active'
291
+ }
221
292
  },
222
293
  {
223
294
  variant: 'dot',
224
295
  checked: true,
225
296
  intent: 'danger',
226
- class: { track: 'border-danger bg-danger' }
297
+ class: {
298
+ track: 'border-danger bg-danger group-hover:bg-danger-hover group-active:bg-danger-active'
299
+ }
227
300
  },
228
301
  {
229
302
  variant: 'dot',
230
303
  checked: true,
231
304
  intent: 'neutral',
232
- class: { track: 'border-neutral bg-neutral' }
305
+ class: {
306
+ track: 'border-neutral bg-neutral group-hover:bg-neutral-hover group-active:bg-neutral-active'
307
+ }
233
308
  },
234
309
  // ── Error state, dot variant (must fold over dot-unchecked border) ──
310
+ // Pins the hover bucket too, for the same reason as the Switch-Pill twin.
235
311
  {
236
312
  variant: 'dot',
237
313
  error: true,
238
314
  checked: false,
239
- class: { track: 'border-danger' }
315
+ class: { track: 'border-danger group-hover:border-danger' }
240
316
  }
241
317
  ],
242
318
  defaultVariants: {
@@ -1,19 +1,19 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const tooltipVariants: ((props?: {
3
3
  open?: boolean | undefined;
4
- intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | "info" | undefined;
4
+ intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
5
5
  size?: "sm" | "md" | "lg" | undefined;
6
6
  } | undefined) => {
7
7
  base: (props?: ({
8
8
  open?: boolean | undefined;
9
- intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | "info" | undefined;
9
+ intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
10
10
  size?: "sm" | "md" | "lg" | undefined;
11
11
  } & {
12
12
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
13
13
  }) | undefined) => string;
14
14
  arrow: (props?: ({
15
15
  open?: boolean | undefined;
16
- intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | "info" | undefined;
16
+ intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
17
17
  size?: "sm" | "md" | "lg" | undefined;
18
18
  } & {
19
19
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -48,10 +48,14 @@ export const tooltipVariants = tv({
48
48
  true: { base: 'opacity-100 starting:opacity-0' },
49
49
  false: { base: 'opacity-0' }
50
50
  },
51
+ // The standard six-value palette. `info` is deliberately absent: it belongs
52
+ // to the feedback family (Alert, Toast), where it means "informational, and
53
+ // stable under a rebrand" — a tooltip is a container, and `info` sat one
54
+ // hue-step from `primary` (220 vs 240), so the two were barely
55
+ // distinguishable while implying a distinction that wasn't there.
51
56
  intent: {
52
57
  primary: { base: 'bg-primary text-text-on-primary' },
53
58
  secondary: { base: 'bg-secondary text-text-on-primary' },
54
- info: { base: 'bg-info text-text-on-primary' },
55
59
  success: { base: 'bg-success text-text-on-primary' },
56
60
  warning: { base: 'bg-warning text-text-on-warning' },
57
61
  danger: { base: 'bg-danger text-text-on-primary' },
@@ -1,7 +1,7 @@
1
1
  @theme {
2
2
  --color-neutral-0: oklch(1 0 0);
3
- --color-neutral-25: oklch(0.99 0.002 240);
4
- --color-neutral-50: oklch(0.98 0.005 240);
3
+ --color-neutral-25: oklch(0.985 0.003 240);
4
+ --color-neutral-50: oklch(0.965 0.006 240);
5
5
  --color-neutral-100: oklch(0.95 0.008 240);
6
6
  --color-neutral-200: oklch(0.89 0.012 240);
7
7
  --color-neutral-300: oklch(0.83 0.014 240);
@@ -179,13 +179,23 @@
179
179
  --radius-commit: 9999px;
180
180
  --radius-modify: var(--radius-sm);
181
181
  --radius-contain: var(--radius-xs);
182
- /* r-bridge — the "between" radius for adjacency cases: a floating
183
- panel anchored to a commit-tier (pill) trigger reads disconnected
184
- if the panel is r-contain (2 px) and over-sized if it mirrors the
185
- pill at r-commit (9999 px). r-bridge sits at r-md (~6 px) to
186
- visually pair pill triggers with their dropdown bodies (Menu panel
187
- when its trigger Button is commit-tier). Use sparingly most
188
- surfaces should pick one of the three primary tiers above. */
182
+ /* r-bridge — the MIDDLE tier, at r-md (~6 px). Two cases earn it, both
183
+ of them "r-contain is too hard here, r-commit is too soft":
184
+
185
+ 1. Adjacency. A floating panel anchored to a commit-tier (pill)
186
+ trigger reads disconnected at r-contain (2 px) and over-sized
187
+ if it mirrors the pill at r-commit. (Menu panel when its
188
+ trigger Button is commit-tier.)
189
+ 2. Small tinted content surfaces. Optical radius scales with the
190
+ area it turns: 2 px on a 600 px Card reads as a precise edge,
191
+ the same 2 px on a ~200 px chat bubble reads as a rectangle.
192
+ A bubble is content, not architecture — it takes the middle
193
+ tier. (ChatMessage bubble.)
194
+
195
+ Still not a free-for-all: everything that IS a panel, dialog or
196
+ container belongs on one of the three primary tiers above. Reach
197
+ for bridge only when the surface is demonstrably too small for
198
+ r-contain to read as intentional. */
189
199
  --radius-bridge: var(--radius-md);
190
200
 
191
201
  /* Container Query Breakpoints */
@@ -18,7 +18,25 @@
18
18
  above the one below — themes can differentiate them by color (e.g.
19
19
  cream → lighter cream → near-white → white) or rely on shadows
20
20
  alone when the palette ceiling is reached (as in the neutral default
21
- where base and overlay are both white). */
21
+ where base and overlay are both white).
22
+
23
+ LIGHT-MODE SPREAD (2026-07). In light mode `base` sits at the white
24
+ ceiling, so every other rung has to differentiate DOWNWARD — and the
25
+ ramp gave it almost nothing to work with: quiet/elevated were ΔL 0.01
26
+ and 0.02 from base, against 0.03 and 0.08 in dark mode. A tinted
27
+ in-flow surface with no border and no shadow was therefore effectively
28
+ invisible in light mode, which is why components kept reaching for a
29
+ border just to make a surface visible at all (the chat bubble, the
30
+ table-cell progress track). Fixed at the ramp rather than per token —
31
+ `neutral-25` 0.99 → 0.985 and `neutral-50` 0.98 → 0.965 — so quiet,
32
+ elevated and subtle all gain spread without any semantic moving and
33
+ without a new token landing on an occupied value. All five themes share
34
+ those L values (only the hue differs), so the change reaches every
35
+ theme at once. Measured: text-tertiary on the new elevated is 11.32:1,
36
+ far above AA; style/contrast.test.ts is the gate. Guarded below by
37
+ style/semantic.test.ts, which asserts the reading surfaces stay
38
+ mutually distinct in BOTH modes — the failure mode here is silent, a
39
+ collapsed rung looks exactly like a working one in a screenshot. */
22
40
  /* L·1 — base: the page itself. Content, cards, and reading surfaces
23
41
  sit at this level. */
24
42
  --color-surface-base: light-dark(var(--color-neutral-0), var(--color-neutral-900));
@@ -31,7 +49,22 @@
31
49
  /* L·3 — overlay: highest surfaces (Dialog, Drawer, Sheet, Toast).
32
50
  Full shadow + backdrop dimming. */
33
51
  --color-surface-overlay: light-dark(var(--color-neutral-0), var(--color-neutral-850));
52
+ /* `surface-hover` / `-active` are the hover/press steps for elements resting
53
+ on a READING surface (base/quiet/elevated) — a menu item, a table row.
54
+ `surface-interactive` is a resting FILL one step in from the page (a filled
55
+ Input, a Slider track, the Tab list). Those two ladders start at different
56
+ rungs, so they need their own hover step: `surface-interactive` already sits
57
+ at neutral-100, which is exactly where `surface-hover` lands in light mode —
58
+ `bg-surface-interactive hover:bg-surface-hover` was a silent no-op, and in
59
+ dark mode `surface-active` collapses onto `surface-interactive` the same
60
+ way. `-interactive-hover` continues the fill's own ladder: one step further
61
+ from the page in light, one step toward the light in dark. Guarded by
62
+ style/semantic.test.ts so the pair can never collapse again. */
34
63
  --color-surface-interactive: light-dark(var(--color-neutral-100), var(--color-neutral-700));
64
+ --color-surface-interactive-hover: light-dark(
65
+ var(--color-neutral-200),
66
+ var(--color-neutral-650)
67
+ );
35
68
  --color-surface-hover: light-dark(var(--color-neutral-100), var(--color-neutral-750));
36
69
  --color-surface-active: light-dark(var(--color-neutral-200), var(--color-neutral-700));
37
70
  --color-surface-disabled: light-dark(var(--color-neutral-100), var(--color-neutral-800));
@@ -61,7 +94,21 @@
61
94
  --color-text-secondary: light-dark(var(--color-neutral-700), var(--color-neutral-300));
62
95
  --color-text-tertiary: light-dark(var(--color-neutral-600), var(--color-neutral-300));
63
96
  --color-text-quaternary: light-dark(var(--color-neutral-500), var(--color-neutral-400));
64
- --color-text-disabled: light-dark(var(--color-neutral-300), var(--color-neutral-600));
97
+ /* Disabled text the same stop in both modes. WCAG 1.4.3 exempts inactive
98
+ controls, so the old value was not a violation, but at neutral-300 / -600 it
99
+ measured roughly 1.5:1 in light and 2:1 in dark: a disabled field's label is
100
+ the one part that still has to say what the field IS, and it was effectively
101
+ invisible. Now held to the 3:1 UI floor by style/contrast.test.ts across
102
+ every theme × mode × reading surface plus the disabled fill — that test is
103
+ the number, not this comment.
104
+
105
+ Deliberately still the weakest rung of the text ramp, so "disabled" stays
106
+ legible without stopping being a signal. Two consequences, both accepted:
107
+ it shares its light-mode value with text-quaternary (two roles, not two
108
+ steps of one ladder — but see the Calendar entry in technical-debt, where
109
+ they meet), and the floor covers the token alone, not what a component
110
+ composes on top of it (an `opacity-*` wrapper can undo it). */
111
+ --color-text-disabled: light-dark(var(--color-neutral-500), var(--color-neutral-500));
65
112
  /* The on-colors below pair text with a *fill* rather than a surface.
66
113
  text-on-dark and text-on-primary answer fills that are dark in light
67
114
  mode and light in dark mode, so the label has to travel the opposite
@@ -41,8 +41,8 @@
41
41
  * contrast stays unchanged. surface-*, text-* and border-* pick this up
42
42
  * automatically through semantic.css.
43
43
  */
44
- --color-neutral-25: oklch(0.99 0.002 150);
45
- --color-neutral-50: oklch(0.98 0.005 150);
44
+ --color-neutral-25: oklch(0.985 0.003 150);
45
+ --color-neutral-50: oklch(0.965 0.006 150);
46
46
  --color-neutral-100: oklch(0.95 0.008 150);
47
47
  --color-neutral-200: oklch(0.89 0.012 150);
48
48
  --color-neutral-300: oklch(0.83 0.014 150);
@@ -54,8 +54,8 @@
54
54
  * contrast stays unchanged. surface-*, text-* and border-* pick this up
55
55
  * automatically through semantic.css.
56
56
  */
57
- --color-neutral-25: oklch(0.99 0 0);
58
- --color-neutral-50: oklch(0.98 0 0);
57
+ --color-neutral-25: oklch(0.985 0 0);
58
+ --color-neutral-50: oklch(0.965 0 0);
59
59
  --color-neutral-100: oklch(0.95 0 0);
60
60
  --color-neutral-200: oklch(0.89 0 0);
61
61
  --color-neutral-300: oklch(0.83 0 0);
@@ -45,8 +45,8 @@
45
45
  * hue shifts — so WCAG contrast stays unchanged. surface-*, text-* and
46
46
  * border-* pick this up automatically through semantic.css.
47
47
  */
48
- --color-neutral-25: oklch(0.99 0.002 220);
49
- --color-neutral-50: oklch(0.98 0.005 220);
48
+ --color-neutral-25: oklch(0.985 0.003 220);
49
+ --color-neutral-50: oklch(0.965 0.006 220);
50
50
  --color-neutral-100: oklch(0.95 0.008 220);
51
51
  --color-neutral-200: oklch(0.89 0.012 220);
52
52
  --color-neutral-300: oklch(0.83 0.014 220);
@@ -41,8 +41,8 @@
41
41
  * stays unchanged. surface-*, text-* and border-* pick this up
42
42
  * automatically through semantic.css.
43
43
  */
44
- --color-neutral-25: oklch(0.99 0.002 350);
45
- --color-neutral-50: oklch(0.98 0.005 350);
44
+ --color-neutral-25: oklch(0.985 0.003 350);
45
+ --color-neutral-50: oklch(0.965 0.006 350);
46
46
  --color-neutral-100: oklch(0.95 0.008 350);
47
47
  --color-neutral-200: oklch(0.89 0.012 350);
48
48
  --color-neutral-300: oklch(0.83 0.014 350);
@@ -41,8 +41,8 @@
41
41
  * contrast stays unchanged. surface-*, text-* and border-* pick this up
42
42
  * automatically through semantic.css; no per-token overrides needed.
43
43
  */
44
- --color-neutral-25: oklch(0.99 0.002 50);
45
- --color-neutral-50: oklch(0.98 0.005 50);
44
+ --color-neutral-25: oklch(0.985 0.003 50);
45
+ --color-neutral-50: oklch(0.965 0.006 50);
46
46
  --color-neutral-100: oklch(0.95 0.008 50);
47
47
  --color-neutral-200: oklch(0.89 0.012 50);
48
48
  --color-neutral-300: oklch(0.83 0.014 50);
@@ -21,8 +21,8 @@
21
21
  const foundationColors = {
22
22
  neutral: {
23
23
  '0': 'oklch(1 0 0)',
24
- '25': 'oklch(0.99 0.002 240)',
25
- '50': 'oklch(0.98 0.005 240)',
24
+ '25': 'oklch(0.985 0.003 240)',
25
+ '50': 'oklch(0.965 0.006 240)',
26
26
  '100': 'oklch(0.95 0.008 240)',
27
27
  '200': 'oklch(0.89 0.012 240)',
28
28
  '300': 'oklch(0.83 0.014 240)',
@@ -139,6 +139,10 @@ const semanticTokens = {
139
139
  elevated: { value: '{color.neutral.50}', description: 'L2 — lifted surfaces (popover, menu)' },
140
140
  overlay: { value: '{color.neutral.0}', description: 'L3 — dialog/drawer/toast background' },
141
141
  interactive: { value: '{color.neutral.100}', description: 'Interactive control background' },
142
+ 'interactive-hover': {
143
+ value: '{color.neutral.200}',
144
+ description: 'Hover step for an interactive control fill'
145
+ },
142
146
  hover: { value: '{color.neutral.100}', description: 'Hover state background' },
143
147
  active: { value: '{color.neutral.200}', description: 'Active/pressed state background' },
144
148
  disabled: { value: '{color.neutral.100}', description: 'Disabled surface' },
@@ -152,7 +156,7 @@ const semanticTokens = {
152
156
  secondary: { value: '{color.neutral.700}', description: 'Secondary text' },
153
157
  tertiary: { value: '{color.neutral.600}', description: 'Tertiary text (WCAG AA on base)' },
154
158
  quaternary: { value: '{color.neutral.500}', description: 'Quaternary text (lowest emphasis)' },
155
- disabled: { value: '{color.neutral.300}', description: 'Disabled text' },
159
+ disabled: { value: '{color.neutral.500}', description: 'Disabled text' },
156
160
  'on-dark': { value: '{color.neutral.0}', description: 'Text on dark surfaces' },
157
161
  'on-primary': { value: '{color.neutral.0}', description: 'Text on primary color bg' },
158
162
  'on-warning': {
@@ -13,15 +13,39 @@ export interface PersistentStateConfig<T> {
13
13
  /**
14
14
  * Create a persistent state that automatically syncs with storage
15
15
  * Uses Svelte 5 $state() for reactivity
16
+ *
17
+ * Besides `value`, the returned object exposes `hasStoredValue` — whether an
18
+ * entry for this key exists in storage. That is what lets a consumer tell a
19
+ * *stored empty* value (`[]`, `''`, `null` — the user cleared it) from *nothing
20
+ * stored at all*, so a cleared state can win over a default/seed instead of
21
+ * being re-applied on every load. Two rules keep that signal meaningful: a save
22
+ * that would not change the stored bytes is skipped, and an instance nobody
23
+ * wrote to never creates an entry for its own default (so `reset()` is not
24
+ * undone by the auto-save, and untouched state stays out of storage). Writing
25
+ * the default *back* — clearing — is a real write and does create the entry.
26
+ *
27
+ * Values must round-trip through the configured `serialize`/`deserialize`;
28
+ * `Set`/`Map` do not under the `JSON.stringify` default (they serialize to
29
+ * `{}`), so pass converting functions or store plain arrays/objects.
16
30
  */
17
31
  export declare function createPersistentState<T>(config: PersistentStateConfig<T>): {
18
32
  value: T;
33
+ /**
34
+ * Whether storage currently holds an entry for this key: `true` when
35
+ * construction (or `reload()`) found a parseable entry, and after a write
36
+ * actually reached storage; `false` when the key was absent or corrupt,
37
+ * after `reset()`, and always without a working storage (SSR, private
38
+ * mode). Use it to distinguish a stored empty value from an absent one.
39
+ */
40
+ readonly hasStoredValue: boolean;
19
41
  /**
20
42
  * Reset to default value and clear storage
21
43
  */
22
44
  reset(): void;
23
45
  /**
24
- * Force immediate save (bypasses debounce)
46
+ * Force immediate save (bypasses debounce). A no-op when storage already
47
+ * holds exactly this value, and — like the auto-save — when the instance
48
+ * was never written to and still holds its default.
25
49
  */
26
50
  forceSave(): void;
27
51
  /**