@urbicon-ui/blocks 6.40.4 → 6.43.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.
- package/dist/components/Chat/A2UIView/A2UINode.svelte +20 -39
- package/dist/components/Chat/A2UIView/A2UIView.svelte +47 -50
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.d.ts +17 -0
- package/dist/components/Chat/A2UIView/a2ui-basic-catalog.js +69 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.d.ts +95 -0
- package/dist/components/Chat/A2UIView/a2ui-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.d.ts +40 -0
- package/dist/components/Chat/A2UIView/a2ui-node-common.js +72 -0
- package/dist/components/Chat/A2UIView/a2ui-prompt.d.ts +21 -10
- package/dist/components/Chat/A2UIView/a2ui-prompt.js +170 -24
- package/dist/components/Chat/A2UIView/a2ui-registry.d.ts +1 -1
- package/dist/components/Chat/A2UIView/a2ui-registry.js +2 -1
- package/dist/components/Chat/A2UIView/a2ui-render.d.ts +30 -0
- package/dist/components/Chat/A2UIView/a2ui-render.js +61 -23
- package/dist/components/Chat/A2UIView/a2ui-router.d.ts +136 -0
- package/dist/components/Chat/A2UIView/a2ui-router.js +264 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.d.ts +45 -0
- package/dist/components/Chat/A2UIView/a2ui-schema.js +133 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.d.ts +120 -0
- package/dist/components/Chat/A2UIView/a2ui-stream.js +406 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.d.ts +65 -0
- package/dist/components/Chat/A2UIView/a2ui-transcript.js +101 -0
- package/dist/components/Chat/A2UIView/a2ui-validate.d.ts +33 -1
- package/dist/components/Chat/A2UIView/a2ui-validate.js +167 -63
- package/dist/components/Chat/A2UIView/a2ui.types.d.ts +50 -0
- package/dist/components/Chat/A2UIView/a2ui.types.js +38 -1
- package/dist/components/Chat/A2UIView/index.d.ts +35 -2
- package/dist/components/Chat/A2UIView/index.js +13 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte +831 -0
- package/dist/components/Chat/A2UIView/urbicon/UrbiconA2UINode.svelte.d.ts +4 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.d.ts +14 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-catalog.js +87 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.d.ts +78 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon-registry.js +783 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.d.ts +43 -0
- package/dist/components/Chat/A2UIView/urbicon/a2ui-urbicon.variants.js +33 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.d.ts +7 -0
- package/dist/components/Chat/A2UIView/urbicon/urbicon-axes.gen.js +86 -0
- package/dist/components/Chat/ChatMessage/chat-message.variants.d.ts +19 -19
- package/dist/components/FileUpload/fileUpload.variants.js +15 -4
- package/dist/components/PinInput/PinInput.svelte +1 -0
- package/dist/components/PinInput/pin-input.variants.js +14 -1
- package/dist/components/TimeInput/time-input.variants.js +16 -1
- package/dist/internal/field-chrome.d.ts +18 -1
- package/dist/internal/field-chrome.js +18 -1
- package/dist/primitives/Avatar/avatar.variants.js +4 -1
- package/dist/primitives/Badge/badge.variants.js +11 -2
- package/dist/primitives/Button/button.variants.js +4 -1
- package/dist/primitives/Card/card.variants.js +4 -1
- package/dist/primitives/Checkbox/checkbox.variants.js +7 -1
- package/dist/primitives/Combobox/Combobox.svelte +9 -1
- package/dist/primitives/Combobox/combobox.variants.d.ts +25 -0
- package/dist/primitives/Combobox/combobox.variants.js +32 -3
- package/dist/primitives/Combobox/index.d.ts +8 -2
- package/dist/primitives/Dialog/dialog.variants.js +4 -1
- package/dist/primitives/Drawer/drawer.variants.js +4 -1
- package/dist/primitives/Input/input.variants.js +17 -1
- package/dist/primitives/Menu/menu.variants.js +5 -1
- package/dist/primitives/Progress/progress.variants.js +5 -2
- package/dist/primitives/RadioGroup/radioGroup.variants.js +68 -9
- package/dist/primitives/Select/select.variants.js +13 -4
- package/dist/primitives/Slider/Slider.svelte +4 -1
- package/dist/primitives/Slider/slider.variants.js +9 -2
- package/dist/primitives/Textarea/textarea.variants.js +15 -1
- package/dist/primitives/Toast/toast.variants.js +5 -1
- package/dist/primitives/Toggle/toggle.variants.js +92 -18
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
- package/dist/primitives/Tooltip/tooltip.variants.js +5 -1
- package/dist/utils/persistent-state.svelte.d.ts +25 -1
- package/dist/utils/persistent-state.svelte.js +98 -10
- package/docs/MIGRATION-v5.md +29 -0
- package/package.json +3 -3
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { FIELD_MESSAGE_TONES, fieldErrorFrame } from '../../internal/field-chrome.js';
|
|
1
2
|
import { tv } from '../../utils/variants.js';
|
|
3
|
+
// The trigger is the focusable element, so the error ring lives on it directly.
|
|
4
|
+
const focus = 'focus-visible';
|
|
2
5
|
export const selectVariants = tv({
|
|
3
6
|
slots: {
|
|
4
7
|
wrapper: ['flex flex-col w-full gap-1.5'],
|
|
@@ -146,13 +149,19 @@ export const selectVariants = tv({
|
|
|
146
149
|
// Declared BEFORE `error` so the error tone wins the message-color
|
|
147
150
|
// bucket in every call shape — `{ error: true }` alone must read red.
|
|
148
151
|
messageType: {
|
|
149
|
-
error: { message:
|
|
150
|
-
helper: { message:
|
|
152
|
+
error: { message: FIELD_MESSAGE_TONES.error },
|
|
153
|
+
helper: { message: FIELD_MESSAGE_TONES.helper }
|
|
151
154
|
},
|
|
155
|
+
// Same error frame as every other field (shared fragment, so a token fix
|
|
156
|
+
// can't miss Select again). Unlike Input/Textarea/PinInput/TimeInput,
|
|
157
|
+
// Select has no `intent` axis, so nothing competes for the border/ring
|
|
158
|
+
// buckets and the frame can stay on the axis. If an `intent` axis is ever
|
|
159
|
+
// added here, move this to the compound stage — see the precedence note in
|
|
160
|
+
// input.variants.ts.
|
|
152
161
|
error: {
|
|
153
162
|
true: {
|
|
154
|
-
trigger:
|
|
155
|
-
message:
|
|
163
|
+
trigger: fieldErrorFrame(focus),
|
|
164
|
+
message: FIELD_MESSAGE_TONES.error
|
|
156
165
|
}
|
|
157
166
|
},
|
|
158
167
|
required: {
|
|
@@ -160,7 +160,7 @@
|
|
|
160
160
|
if (status === 'insideValidOnly') return 'var(--color-warning-subtle)';
|
|
161
161
|
if (status === 'outsideValidDanger') return 'var(--color-danger-subtle)';
|
|
162
162
|
if (status === 'outsideValidWarning') return 'var(--color-warning-subtle)';
|
|
163
|
-
return 'var(--color-surface-
|
|
163
|
+
return 'var(--color-surface-interactive)';
|
|
164
164
|
};
|
|
165
165
|
|
|
166
166
|
const points = [min, ...zoneBoundaries, max];
|
|
@@ -460,6 +460,7 @@
|
|
|
460
460
|
? `${label} minimum`
|
|
461
461
|
: bt('accessibility.minimum') || 'Minimum'}
|
|
462
462
|
aria-describedby={describedBy}
|
|
463
|
+
aria-invalid={error ? 'true' : undefined}
|
|
463
464
|
aria-disabled={disabled || undefined}
|
|
464
465
|
class={unstyled ? (slotClasses?.thumb ?? '') : styles.thumb({ class: slotClasses?.thumb })}
|
|
465
466
|
style="left: {thumbStartPosition}%"
|
|
@@ -479,6 +480,7 @@
|
|
|
479
480
|
? `${label} maximum`
|
|
480
481
|
: bt('accessibility.maximum') || 'Maximum'}
|
|
481
482
|
aria-describedby={describedBy}
|
|
483
|
+
aria-invalid={error ? 'true' : undefined}
|
|
482
484
|
aria-disabled={disabled || undefined}
|
|
483
485
|
class={unstyled ? (slotClasses?.thumb ?? '') : styles.thumb({ class: slotClasses?.thumb })}
|
|
484
486
|
style="left: {thumbEndPosition}%"
|
|
@@ -495,6 +497,7 @@
|
|
|
495
497
|
aria-labelledby={ariaLabelledby}
|
|
496
498
|
aria-label={ariaLabelledby ? undefined : label || bt('accessibility.slider') || 'Slider'}
|
|
497
499
|
aria-describedby={describedBy}
|
|
500
|
+
aria-invalid={error ? 'true' : undefined}
|
|
498
501
|
aria-disabled={disabled || undefined}
|
|
499
502
|
class={unstyled ? (slotClasses?.thumb ?? '') : styles.thumb({ class: slotClasses?.thumb })}
|
|
500
503
|
style="left: {thumbPosition}%"
|
|
@@ -7,8 +7,11 @@ export const sliderVariants = tv({
|
|
|
7
7
|
valueText: ['text-sm tabular-nums text-text-tertiary'],
|
|
8
8
|
base: ['relative flex items-center w-full touch-none select-none cursor-pointer'],
|
|
9
9
|
// tier: commit — slider is a continuous-value pill (track + range + thumb).
|
|
10
|
+
// Track fill is `surface-interactive`, matching Toggle's unchecked track:
|
|
11
|
+
// `surface-subtle` resolves to the SAME neutral step as `surface-elevated`
|
|
12
|
+
// in both modes, so the groove vanished on any elevated card/popover.
|
|
10
13
|
track: [
|
|
11
|
-
'relative w-full overflow-hidden rounded-commit bg-surface-
|
|
14
|
+
'relative w-full overflow-hidden rounded-commit bg-surface-interactive',
|
|
12
15
|
'transition-[background-color] duration-[var(--blocks-duration-fast)]'
|
|
13
16
|
],
|
|
14
17
|
range: [
|
|
@@ -20,7 +23,11 @@ export const sliderVariants = tv({
|
|
|
20
23
|
'absolute rounded-commit bg-surface-base border-2',
|
|
21
24
|
'shadow-[var(--blocks-shadow-sm)]',
|
|
22
25
|
'-translate-x-1/2 -translate-y-1/2 top-1/2',
|
|
23
|
-
|
|
26
|
+
// `scale`, NOT `transform`: Tailwind 4 emits `scale-*` as the discrete
|
|
27
|
+
// `scale:` property — `hover:scale-110` is the only animated transform
|
|
28
|
+
// here, so it is the only one listed. The centring `-translate-*` above
|
|
29
|
+
// is static (the thumb travels via `left`), so `translate` stays out.
|
|
30
|
+
'transition-[box-shadow,scale] duration-[var(--blocks-duration-fast)]',
|
|
24
31
|
'hover:scale-110 hover:shadow-[var(--blocks-shadow-md)]',
|
|
25
32
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-surface-base'
|
|
26
33
|
],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { fieldErrorFrame } from '../../internal/field-chrome.js';
|
|
1
2
|
import { tv } from '../../utils/variants.js';
|
|
2
3
|
export const textareaVariants = tv({
|
|
3
4
|
slots: {
|
|
@@ -83,9 +84,11 @@ export const textareaVariants = tv({
|
|
|
83
84
|
error: { message: 'text-danger' },
|
|
84
85
|
helper: { message: 'text-text-tertiary' }
|
|
85
86
|
},
|
|
87
|
+
// The error FRAME is not declared here — it lives in the compound stage
|
|
88
|
+
// below, where it beats `intent` by construction. See the precedence note
|
|
89
|
+
// on the first compound entry.
|
|
86
90
|
error: {
|
|
87
91
|
true: {
|
|
88
|
-
base: 'border-danger focus-visible:border-danger focus-visible:ring-danger/20',
|
|
89
92
|
message: 'text-danger'
|
|
90
93
|
}
|
|
91
94
|
},
|
|
@@ -101,6 +104,17 @@ export const textareaVariants = tv({
|
|
|
101
104
|
}
|
|
102
105
|
},
|
|
103
106
|
compoundVariants: [
|
|
107
|
+
// ── Validation precedence: `error` beats `intent`, explicitly ────────────
|
|
108
|
+
// Both axes paint the SAME three buckets (border-color plus the focused
|
|
109
|
+
// border/ring tint), so exactly one of them can win. Emitting the error
|
|
110
|
+
// frame here rather than on the `error` axis makes that rule structural:
|
|
111
|
+
// compounds always fold after every axis, so `error: true` overrides
|
|
112
|
+
// whatever `intent` painted regardless of how the axes are ordered above
|
|
113
|
+
// (it used to hinge purely on `error` being DECLARED after `intent`).
|
|
114
|
+
{
|
|
115
|
+
error: true,
|
|
116
|
+
class: { base: fieldErrorFrame('focus-visible') }
|
|
117
|
+
},
|
|
104
118
|
{
|
|
105
119
|
variant: 'ghost',
|
|
106
120
|
intent: ['success', 'warning', 'danger'],
|
|
@@ -10,7 +10,11 @@ export const toastVariants = tv({
|
|
|
10
10
|
'pointer-events-auto flex items-start gap-3 w-full max-w-sm',
|
|
11
11
|
'rounded-contain p-4',
|
|
12
12
|
'shadow-[var(--blocks-shadow-lg)]',
|
|
13
|
-
|
|
13
|
+
// No transform entry: the enter/exit motion is Svelte's `transition:fly`,
|
|
14
|
+
// which runs as a CSS *animation* (keyframes) and ignores
|
|
15
|
+
// `transition-property` entirely. Nothing on this slot sets a
|
|
16
|
+
// scale/translate/rotate utility, so a `transform` entry was dead weight.
|
|
17
|
+
'transition-[color,background-color,box-shadow,opacity] duration-[var(--blocks-duration-normal)] ease-[var(--blocks-ease-smooth)]'
|
|
14
18
|
],
|
|
15
19
|
icon: 'shrink-0 mt-0.5 w-5 h-5',
|
|
16
20
|
content: 'flex-1 min-w-0',
|
|
@@ -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,18 @@ export const toggleVariants = tv({
|
|
|
104
123
|
variant: 'default',
|
|
105
124
|
checked: false,
|
|
106
125
|
class: {
|
|
107
|
-
|
|
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
|
+
// Deliberately NOT a fill step: the off track sits on
|
|
131
|
+
// `surface-interactive`, which resolves to the SAME value as
|
|
132
|
+
// `surface-hover` in light mode (both neutral-100) and as
|
|
133
|
+
// `surface-active` in dark (both neutral-700) — in the library
|
|
134
|
+
// default *and* under the docs Rooms skin. Either fill token would
|
|
135
|
+
// therefore be a silent no-op in one of the two modes; the border is
|
|
136
|
+
// the only step that reads in both.
|
|
137
|
+
track: 'bg-surface-interactive group-hover:border-border-emphasis',
|
|
108
138
|
thumb: 'translate-x-0'
|
|
109
139
|
}
|
|
110
140
|
},
|
|
@@ -113,7 +143,11 @@ export const toggleVariants = tv({
|
|
|
113
143
|
variant: 'default',
|
|
114
144
|
checked: false,
|
|
115
145
|
withBorder: true,
|
|
116
|
-
|
|
146
|
+
// `border-strong` is the top of the border ladder, so the off-state
|
|
147
|
+
// hover above would *weaken* it (strong → emphasis). Pin the hover
|
|
148
|
+
// bucket to strong: an opt-in permanent boundary must not soften when
|
|
149
|
+
// the pointer arrives.
|
|
150
|
+
class: { track: 'border-border-strong group-hover:border-border-strong' }
|
|
117
151
|
},
|
|
118
152
|
{
|
|
119
153
|
variant: 'default',
|
|
@@ -122,41 +156,58 @@ export const toggleVariants = tv({
|
|
|
122
156
|
class: { track: 'ring-1 ring-inset ring-surface-base/40' }
|
|
123
157
|
},
|
|
124
158
|
// ── Checked track per intent (Switch-Pill only) ──
|
|
159
|
+
// Hover/active darken through the intent interaction-layer tokens — the
|
|
160
|
+
// same `bg-<intent>-hover` / `bg-<intent>-active` ladder Button and
|
|
161
|
+
// Checkbox use — via `group-*` so hovering/pressing the label counts too.
|
|
162
|
+
// The border stays on the base intent stop (as in Checkbox), leaving a
|
|
163
|
+
// hairline rim that makes the fill step legible.
|
|
125
164
|
{
|
|
126
165
|
variant: 'default',
|
|
127
166
|
checked: true,
|
|
128
167
|
intent: 'primary',
|
|
129
|
-
class: {
|
|
168
|
+
class: {
|
|
169
|
+
track: 'bg-primary border-primary group-hover:bg-primary-hover group-active:bg-primary-active'
|
|
170
|
+
}
|
|
130
171
|
},
|
|
131
172
|
{
|
|
132
173
|
variant: 'default',
|
|
133
174
|
checked: true,
|
|
134
175
|
intent: 'secondary',
|
|
135
|
-
class: {
|
|
176
|
+
class: {
|
|
177
|
+
track: 'bg-secondary border-secondary group-hover:bg-secondary-hover group-active:bg-secondary-active'
|
|
178
|
+
}
|
|
136
179
|
},
|
|
137
180
|
{
|
|
138
181
|
variant: 'default',
|
|
139
182
|
checked: true,
|
|
140
183
|
intent: 'success',
|
|
141
|
-
class: {
|
|
184
|
+
class: {
|
|
185
|
+
track: 'bg-success border-success group-hover:bg-success-hover group-active:bg-success-active'
|
|
186
|
+
}
|
|
142
187
|
},
|
|
143
188
|
{
|
|
144
189
|
variant: 'default',
|
|
145
190
|
checked: true,
|
|
146
191
|
intent: 'warning',
|
|
147
|
-
class: {
|
|
192
|
+
class: {
|
|
193
|
+
track: 'bg-warning border-warning group-hover:bg-warning-hover group-active:bg-warning-active'
|
|
194
|
+
}
|
|
148
195
|
},
|
|
149
196
|
{
|
|
150
197
|
variant: 'default',
|
|
151
198
|
checked: true,
|
|
152
199
|
intent: 'danger',
|
|
153
|
-
class: {
|
|
200
|
+
class: {
|
|
201
|
+
track: 'bg-danger border-danger group-hover:bg-danger-hover group-active:bg-danger-active'
|
|
202
|
+
}
|
|
154
203
|
},
|
|
155
204
|
{
|
|
156
205
|
variant: 'default',
|
|
157
206
|
checked: true,
|
|
158
207
|
intent: 'neutral',
|
|
159
|
-
class: {
|
|
208
|
+
class: {
|
|
209
|
+
track: 'bg-neutral border-neutral group-hover:bg-neutral-hover group-active:bg-neutral-active'
|
|
210
|
+
}
|
|
160
211
|
},
|
|
161
212
|
// ── Error state: danger boundary on the unchecked track (Switch-Pill) ──
|
|
162
213
|
// Mirrors Checkbox ("error overrides unchecked border"): the off state
|
|
@@ -165,11 +216,16 @@ export const toggleVariants = tv({
|
|
|
165
216
|
// The dot-variant twin lives at the END of the compound list so it
|
|
166
217
|
// folds over the dot-unchecked `border-border-default` (order is
|
|
167
218
|
// semantic — later compounds win conflicting buckets).
|
|
219
|
+
// `group-hover:border-danger` pins the hover bucket as well — without it
|
|
220
|
+
// the off-state hover step above would repaint the error boundary neutral
|
|
221
|
+
// the moment the pointer arrives, which is exactly when the mark matters.
|
|
168
222
|
{
|
|
169
223
|
variant: 'default',
|
|
170
224
|
error: true,
|
|
171
225
|
checked: false,
|
|
172
|
-
class: {
|
|
226
|
+
class: {
|
|
227
|
+
track: 'border-danger group-hover:border-danger peer-focus-visible:ring-danger/40'
|
|
228
|
+
}
|
|
173
229
|
},
|
|
174
230
|
// ── Thumb translation per size when checked (Switch-Pill only) ──
|
|
175
231
|
{ variant: 'default', checked: true, size: 'xs', class: { thumb: 'translate-x-4' } },
|
|
@@ -193,50 +249,68 @@ export const toggleVariants = tv({
|
|
|
193
249
|
{
|
|
194
250
|
variant: 'dot',
|
|
195
251
|
checked: false,
|
|
196
|
-
|
|
252
|
+
// Off-state hover: `border-default` → `border-emphasis`, the exact
|
|
253
|
+
// ladder Checkbox's unchecked `outlined` box walks (the dot is
|
|
254
|
+
// outline-only, so its boundary is the whole control).
|
|
255
|
+
class: { track: 'border-border-default group-hover:border-border-emphasis' }
|
|
197
256
|
},
|
|
257
|
+
// Same intent interaction layer as the Switch-Pill above — the dot is a
|
|
258
|
+
// filled intent surface too, just a 14px one.
|
|
198
259
|
{
|
|
199
260
|
variant: 'dot',
|
|
200
261
|
checked: true,
|
|
201
262
|
intent: 'primary',
|
|
202
|
-
class: {
|
|
263
|
+
class: {
|
|
264
|
+
track: 'border-primary bg-primary group-hover:bg-primary-hover group-active:bg-primary-active'
|
|
265
|
+
}
|
|
203
266
|
},
|
|
204
267
|
{
|
|
205
268
|
variant: 'dot',
|
|
206
269
|
checked: true,
|
|
207
270
|
intent: 'secondary',
|
|
208
|
-
class: {
|
|
271
|
+
class: {
|
|
272
|
+
track: 'border-secondary bg-secondary group-hover:bg-secondary-hover group-active:bg-secondary-active'
|
|
273
|
+
}
|
|
209
274
|
},
|
|
210
275
|
{
|
|
211
276
|
variant: 'dot',
|
|
212
277
|
checked: true,
|
|
213
278
|
intent: 'success',
|
|
214
|
-
class: {
|
|
279
|
+
class: {
|
|
280
|
+
track: 'border-success bg-success group-hover:bg-success-hover group-active:bg-success-active'
|
|
281
|
+
}
|
|
215
282
|
},
|
|
216
283
|
{
|
|
217
284
|
variant: 'dot',
|
|
218
285
|
checked: true,
|
|
219
286
|
intent: 'warning',
|
|
220
|
-
class: {
|
|
287
|
+
class: {
|
|
288
|
+
track: 'border-warning bg-warning group-hover:bg-warning-hover group-active:bg-warning-active'
|
|
289
|
+
}
|
|
221
290
|
},
|
|
222
291
|
{
|
|
223
292
|
variant: 'dot',
|
|
224
293
|
checked: true,
|
|
225
294
|
intent: 'danger',
|
|
226
|
-
class: {
|
|
295
|
+
class: {
|
|
296
|
+
track: 'border-danger bg-danger group-hover:bg-danger-hover group-active:bg-danger-active'
|
|
297
|
+
}
|
|
227
298
|
},
|
|
228
299
|
{
|
|
229
300
|
variant: 'dot',
|
|
230
301
|
checked: true,
|
|
231
302
|
intent: 'neutral',
|
|
232
|
-
class: {
|
|
303
|
+
class: {
|
|
304
|
+
track: 'border-neutral bg-neutral group-hover:bg-neutral-hover group-active:bg-neutral-active'
|
|
305
|
+
}
|
|
233
306
|
},
|
|
234
307
|
// ── Error state, dot variant (must fold over dot-unchecked border) ──
|
|
308
|
+
// Pins the hover bucket too, for the same reason as the Switch-Pill twin.
|
|
235
309
|
{
|
|
236
310
|
variant: 'dot',
|
|
237
311
|
error: true,
|
|
238
312
|
checked: false,
|
|
239
|
-
class: { track: 'border-danger' }
|
|
313
|
+
class: { track: 'border-danger group-hover:border-danger' }
|
|
240
314
|
}
|
|
241
315
|
],
|
|
242
316
|
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" |
|
|
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" |
|
|
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" |
|
|
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' },
|
|
@@ -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
|
/**
|
|
@@ -30,33 +30,88 @@ function debounce(func, wait) {
|
|
|
30
30
|
/**
|
|
31
31
|
* Create a persistent state that automatically syncs with storage
|
|
32
32
|
* Uses Svelte 5 $state() for reactivity
|
|
33
|
+
*
|
|
34
|
+
* Besides `value`, the returned object exposes `hasStoredValue` — whether an
|
|
35
|
+
* entry for this key exists in storage. That is what lets a consumer tell a
|
|
36
|
+
* *stored empty* value (`[]`, `''`, `null` — the user cleared it) from *nothing
|
|
37
|
+
* stored at all*, so a cleared state can win over a default/seed instead of
|
|
38
|
+
* being re-applied on every load. Two rules keep that signal meaningful: a save
|
|
39
|
+
* that would not change the stored bytes is skipped, and an instance nobody
|
|
40
|
+
* wrote to never creates an entry for its own default (so `reset()` is not
|
|
41
|
+
* undone by the auto-save, and untouched state stays out of storage). Writing
|
|
42
|
+
* the default *back* — clearing — is a real write and does create the entry.
|
|
43
|
+
*
|
|
44
|
+
* Values must round-trip through the configured `serialize`/`deserialize`;
|
|
45
|
+
* `Set`/`Map` do not under the `JSON.stringify` default (they serialize to
|
|
46
|
+
* `{}`), so pass converting functions or store plain arrays/objects.
|
|
33
47
|
*/
|
|
34
48
|
export function createPersistentState(config) {
|
|
35
49
|
const { key, defaultValue, storage: storageType = 'localStorage', serialize = JSON.stringify, deserialize = JSON.parse, debounceMs = 300, version = 1 } = config;
|
|
36
50
|
const storageKey = `urbicon_${key}_v${version}`;
|
|
37
51
|
const storage = getStorage(storageType);
|
|
38
52
|
// Initialize state with stored value or default
|
|
39
|
-
const
|
|
40
|
-
const state = $state({ value:
|
|
53
|
+
const initial = loadFromStorage();
|
|
54
|
+
const state = $state({ value: initial.value, hasStored: initial.stored });
|
|
55
|
+
// A faithful mirror of what storage holds: the exact stored string, or `null`
|
|
56
|
+
// when the key is absent (or unparseable). It must never claim the default is
|
|
57
|
+
// stored when it is not — that would make the absent → stored-empty
|
|
58
|
+
// transition unwritable, which is exactly the state this module exists to
|
|
59
|
+
// preserve (the user cleared the axis).
|
|
60
|
+
let lastWritten = initial.raw;
|
|
61
|
+
// Whether the consumer ever wrote through this instance. Only used to keep an
|
|
62
|
+
// *untouched* instance from creating an entry for its own default.
|
|
63
|
+
let touched = false;
|
|
64
|
+
const defaultSerialized = trySerialize(defaultValue);
|
|
65
|
+
function trySerialize(value) {
|
|
66
|
+
try {
|
|
67
|
+
return serialize(value);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
41
73
|
function loadFromStorage() {
|
|
42
74
|
if (!storage)
|
|
43
|
-
return defaultValue;
|
|
75
|
+
return { value: defaultValue, stored: false, raw: null };
|
|
44
76
|
try {
|
|
45
77
|
const stored = storage.getItem(storageKey);
|
|
46
78
|
if (stored === null)
|
|
47
|
-
return defaultValue;
|
|
48
|
-
return deserialize(stored);
|
|
79
|
+
return { value: defaultValue, stored: false, raw: null };
|
|
80
|
+
return { value: deserialize(stored), stored: true, raw: stored };
|
|
49
81
|
}
|
|
50
82
|
catch (error) {
|
|
51
83
|
console.warn(`Failed to load persistent state for key "${key}":`, error);
|
|
52
|
-
|
|
84
|
+
// A corrupt entry counts as *absent*, never as "stored": consumers use
|
|
85
|
+
// `hasStoredValue` to let a stored value win over their own default or
|
|
86
|
+
// seed, and unparseable junk must not win — it would block that default
|
|
87
|
+
// forever. The junk itself is left in place until something is actually
|
|
88
|
+
// written (the first real write overwrites it); an untouched instance
|
|
89
|
+
// does not clean it up, and does not need to — it reads as absent.
|
|
90
|
+
return { value: defaultValue, stored: false, raw: null };
|
|
53
91
|
}
|
|
54
92
|
}
|
|
55
93
|
function saveToStorage(value) {
|
|
56
94
|
if (!storage)
|
|
57
95
|
return;
|
|
58
96
|
try {
|
|
59
|
-
|
|
97
|
+
const next = serialize(value);
|
|
98
|
+
// Storage already holds exactly this — nothing to do.
|
|
99
|
+
if (next === lastWritten)
|
|
100
|
+
return;
|
|
101
|
+
// Never *create* an entry for a value nobody touched. Without this the
|
|
102
|
+
// auto-save effect below would write every key with its own default on
|
|
103
|
+
// mount, making `hasStoredValue` useless (an untouched default would be
|
|
104
|
+
// indistinguishable from a deliberately cleared one) and re-creating the
|
|
105
|
+
// entry `reset()` just removed. Once the consumer has written — including
|
|
106
|
+
// writing the default back, i.e. clearing — the write goes through, even
|
|
107
|
+
// if the key does not exist yet. `forceSave()` obeys the same rule on
|
|
108
|
+
// purpose: a blanket flush (the table exposes one) must not create an
|
|
109
|
+
// entry for every untouched axis, which would retire all their seeds.
|
|
110
|
+
if (!touched && lastWritten === null && next === defaultSerialized)
|
|
111
|
+
return;
|
|
112
|
+
storage.setItem(storageKey, next);
|
|
113
|
+
lastWritten = next;
|
|
114
|
+
state.hasStored = true;
|
|
60
115
|
}
|
|
61
116
|
catch (error) {
|
|
62
117
|
console.warn(`Failed to save persistent state for key "${key}":`, error);
|
|
@@ -73,17 +128,44 @@ export function createPersistentState(config) {
|
|
|
73
128
|
return state.value;
|
|
74
129
|
},
|
|
75
130
|
set value(newValue) {
|
|
131
|
+
// Mark the instance as written-to *before* the state change, so the
|
|
132
|
+
// auto-save effect it triggers knows this is a deliberate value — that is
|
|
133
|
+
// what lets clearing back to the default create an entry.
|
|
134
|
+
touched = true;
|
|
76
135
|
state.value = newValue;
|
|
77
136
|
},
|
|
137
|
+
/**
|
|
138
|
+
* Whether storage currently holds an entry for this key: `true` when
|
|
139
|
+
* construction (or `reload()`) found a parseable entry, and after a write
|
|
140
|
+
* actually reached storage; `false` when the key was absent or corrupt,
|
|
141
|
+
* after `reset()`, and always without a working storage (SSR, private
|
|
142
|
+
* mode). Use it to distinguish a stored empty value from an absent one.
|
|
143
|
+
*/
|
|
144
|
+
get hasStoredValue() {
|
|
145
|
+
return state.hasStored;
|
|
146
|
+
},
|
|
78
147
|
/**
|
|
79
148
|
* Reset to default value and clear storage
|
|
80
149
|
*/
|
|
81
150
|
reset() {
|
|
82
151
|
state.value = defaultValue;
|
|
83
|
-
|
|
152
|
+
try {
|
|
153
|
+
storage?.removeItem(storageKey);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.warn(`Failed to clear persistent state for key "${key}":`, error);
|
|
157
|
+
}
|
|
158
|
+
state.hasStored = false;
|
|
159
|
+
// Back to "nothing stored", and untouched again — so the auto-save
|
|
160
|
+
// triggered by the assignment above does not immediately re-create the
|
|
161
|
+
// key we just removed. A later write (including clearing) still lands.
|
|
162
|
+
lastWritten = null;
|
|
163
|
+
touched = false;
|
|
84
164
|
},
|
|
85
165
|
/**
|
|
86
|
-
* Force immediate save (bypasses debounce)
|
|
166
|
+
* Force immediate save (bypasses debounce). A no-op when storage already
|
|
167
|
+
* holds exactly this value, and — like the auto-save — when the instance
|
|
168
|
+
* was never written to and still holds its default.
|
|
87
169
|
*/
|
|
88
170
|
forceSave() {
|
|
89
171
|
saveToStorage(state.value);
|
|
@@ -92,7 +174,13 @@ export function createPersistentState(config) {
|
|
|
92
174
|
* Reload from storage
|
|
93
175
|
*/
|
|
94
176
|
reload() {
|
|
95
|
-
|
|
177
|
+
const next = loadFromStorage();
|
|
178
|
+
state.value = next.value;
|
|
179
|
+
state.hasStored = next.stored;
|
|
180
|
+
lastWritten = next.raw;
|
|
181
|
+
// The value now mirrors storage again; nothing here came from the
|
|
182
|
+
// consumer, so an absent key stays absent until the next real write.
|
|
183
|
+
touched = false;
|
|
96
184
|
}
|
|
97
185
|
};
|
|
98
186
|
}
|
package/docs/MIGRATION-v5.md
CHANGED
|
@@ -80,6 +80,35 @@ The dead `tableVariants` export is removed (was unused in any consumer).
|
|
|
80
80
|
<Table data={rows} columns={cols} />
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
## 3b. Table state snippets renamed (v6.41)
|
|
84
|
+
|
|
85
|
+
The Table's three state snippets are renamed to match the slot names they
|
|
86
|
+
already had in `slotClasses` (`emptyState` / `loadingState` / `errorState`).
|
|
87
|
+
That frees `loading` and `error` for the state props they are everywhere else
|
|
88
|
+
in the library — the Table can now be told it is loading or has failed, which
|
|
89
|
+
previously only the managed `queryFn` path could do.
|
|
90
|
+
|
|
91
|
+
| Before (snippet) | After (snippet) | Freed prop |
|
|
92
|
+
| ---------------- | --------------- | ---------------------- |
|
|
93
|
+
| `empty` | `emptyState` | — |
|
|
94
|
+
| `loading` | `loadingState` | `loading?: boolean` |
|
|
95
|
+
| `error` | `errorState` | `error?: string\|null` |
|
|
96
|
+
|
|
97
|
+
```svelte
|
|
98
|
+
<!-- before -->
|
|
99
|
+
<Table {items} {columns}>
|
|
100
|
+
{#snippet empty()}<tr><td colspan="99">Nothing here</td></tr>{/snippet}
|
|
101
|
+
</Table>
|
|
102
|
+
|
|
103
|
+
<!-- after — plus the new state props for data you fetch yourself -->
|
|
104
|
+
<Table {items} {columns} loading={pending} error={failure}>
|
|
105
|
+
{#snippet emptyState()}<tr><td colspan="99">Nothing here</td></tr>{/snippet}
|
|
106
|
+
</Table>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
With a managed `queryFn` the table still owns both states and ignores the two
|
|
110
|
+
props (DEV warns).
|
|
111
|
+
|
|
83
112
|
## 4. Dialog + Drawer intent
|
|
84
113
|
|
|
85
114
|
Dialog and Drawer no longer draw a coloured `border-t-[3px]` accent for
|