@urbicon-ui/blocks 6.23.0 → 6.25.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/README.md +14 -0
- package/dist/components/Calendar/CalendarTimeGrid.svelte +2 -1
- package/dist/components/Calendar/calendar.variants.js +5 -3
- package/dist/components/CommandPalette/CommandPalette.svelte +32 -16
- package/dist/components/CommandPalette/CommandPalette.svelte.d.ts +1 -1
- package/dist/components/CommandPalette/commandPalette.variants.d.ts +10 -0
- package/dist/components/CommandPalette/commandPalette.variants.js +9 -7
- package/dist/components/CommandPalette/index.d.ts +19 -0
- package/dist/components/CompositionBar/CompositionBar.svelte +1 -1
- package/dist/components/CompositionBar/composition-bar.variants.js +1 -1
- package/dist/components/Planner/planner.variants.js +1 -1
- package/dist/components/Sankey/sankey.variants.js +1 -1
- package/dist/components/Sparkline/Sparkline.svelte +10 -4
- package/dist/components/Sparkline/index.d.ts +9 -1
- package/dist/internal/charts/variants.js +2 -2
- package/dist/internal/date-grid/DateGridScaffold.svelte +9 -2
- package/dist/internal/date-grid/date-grid.svelte.d.ts +4 -2
- package/dist/internal/date-grid/date-grid.svelte.js +63 -7
- package/dist/primitives/Avatar/Avatar.svelte +38 -24
- package/dist/primitives/Avatar/index.d.ts +1 -1
- package/dist/primitives/Badge/Badge.svelte +8 -2
- package/dist/primitives/Badge/index.d.ts +67 -32
- package/dist/primitives/Button/Button.svelte +12 -25
- package/dist/primitives/ButtonGroup/ButtonGroup.svelte +5 -1
- package/dist/primitives/Checkbox/Checkbox.svelte +1 -1
- package/dist/primitives/Collapsible/Collapsible.svelte +10 -0
- package/dist/primitives/Collapsible/index.d.ts +13 -2
- package/dist/primitives/ConfirmDialog/ConfirmDialog.svelte +14 -0
- package/dist/primitives/ConfirmDialog/index.d.ts +15 -3
- package/dist/primitives/Dialog/Dialog.svelte +31 -8
- package/dist/primitives/Drawer/Drawer.svelte +29 -7
- package/dist/primitives/Input/Input.svelte +7 -1
- package/dist/primitives/JourneyTimeline/journey-timeline.variants.js +3 -3
- package/dist/primitives/Menu/Menu.svelte +8 -0
- package/dist/primitives/Menu/index.d.ts +11 -13
- package/dist/primitives/Pagination/Pagination.svelte +28 -15
- package/dist/primitives/Popover/Popover.svelte +107 -4
- package/dist/primitives/Popover/index.d.ts +15 -2
- package/dist/primitives/Popover/index.js +1 -1
- package/dist/primitives/Popover/popover.variants.d.ts +25 -0
- package/dist/primitives/Popover/popover.variants.js +35 -0
- package/dist/primitives/Progress/progress.variants.js +1 -1
- package/dist/primitives/RadioGroup/RadioGroup.svelte +6 -2
- package/dist/primitives/RadioGroup/RadioItem.svelte +1 -1
- package/dist/primitives/RadioGroup/radioGroup.variants.js +1 -1
- package/dist/primitives/Select/index.d.ts +8 -1
- package/dist/primitives/Slider/Slider.svelte +32 -3
- package/dist/primitives/Stepper/stepper.variants.js +1 -1
- package/dist/primitives/Textarea/Textarea.svelte +5 -1
- package/dist/primitives/Toast/Toaster.svelte +81 -7
- package/dist/primitives/Toggle/Toggle.svelte +6 -2
- package/dist/primitives/Tooltip/tooltip.variants.js +13 -2
- package/dist/style/foundation.css +30 -3
- package/dist/style/interaction.css +19 -2
- package/dist/style/semantic.css +72 -1
- package/dist/style/themes/neutral.css +10 -1
- package/dist/style/themes/ocean.css +5 -1
- package/dist/utils/compose-handlers.d.ts +72 -0
- package/dist/utils/compose-handlers.js +65 -0
- package/dist/utils/figma-token-export.js +1 -1
- package/dist/utils/guide.svelte.js +92 -29
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/overlay-tokens.d.ts +17 -0
- package/dist/utils/overlay-tokens.js +30 -0
- package/dist/utils/use-floating-panel.svelte.js +9 -3
- package/dist/utils/variants.js +5 -1
- package/package.json +3 -3
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
onRemove,
|
|
35
35
|
onclick,
|
|
36
36
|
onHover,
|
|
37
|
-
role
|
|
37
|
+
role,
|
|
38
38
|
...restProps
|
|
39
39
|
}: BadgeProps = $props();
|
|
40
40
|
|
|
@@ -56,6 +56,12 @@
|
|
|
56
56
|
const isInteractive = $derived(purpose === 'chip' || interactive || !!onclick);
|
|
57
57
|
const isRemovable = $derived(removable && !isDot);
|
|
58
58
|
|
|
59
|
+
// An interactive badge (clickable chip / onclick) carries button semantics:
|
|
60
|
+
// it is focusable and Enter/Space-activatable, so screen readers must hear a
|
|
61
|
+
// button, not a `status` region. An explicit `role` always wins; a disabled
|
|
62
|
+
// badge is inert (pointer-events-none, guarded handlers) so it stays `status`.
|
|
63
|
+
const effRole = $derived(role ?? (isInteractive && !disabled ? 'button' : 'status'));
|
|
64
|
+
|
|
59
65
|
const variantProps: BadgeVariants = $derived({
|
|
60
66
|
tier: effectiveTier,
|
|
61
67
|
intent,
|
|
@@ -124,7 +130,7 @@
|
|
|
124
130
|
? [slotClasses?.base, className].filter(Boolean).join(' ')
|
|
125
131
|
: styles.base({ class: [slotClasses?.base, className] })
|
|
126
132
|
]}
|
|
127
|
-
{
|
|
133
|
+
role={effRole}
|
|
128
134
|
data-purpose={purpose}
|
|
129
135
|
tabindex={isInteractive && !disabled ? 0 : undefined}
|
|
130
136
|
onmouseenter={handleMouseEnter}
|
|
@@ -3,24 +3,13 @@ import type { HTMLAttributes } from 'svelte/elements';
|
|
|
3
3
|
import type { MintProp } from '../../mint/index.js';
|
|
4
4
|
import type { BadgePlacement, BadgeSlots, BadgeVariants } from './badge.variants.js';
|
|
5
5
|
/**
|
|
6
|
-
* Shared fields that apply to every Badge variant
|
|
7
|
-
* label-
|
|
6
|
+
* Shared fields that apply to every Badge arm. `variant`, `purpose`, and the
|
|
7
|
+
* label-only props (`children` / `counter` / `removable` / `interactive` /
|
|
8
|
+
* `onRemove`) are declared per-arm instead — that split is what lets *both*
|
|
9
|
+
* dot spellings (`variant="dot"` and the canonical `purpose="dot"`) forbid the
|
|
10
|
+
* label-only props at the type level.
|
|
8
11
|
*/
|
|
9
|
-
interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
10
|
-
/**
|
|
11
|
-
* The badge's semantic purpose — the canonical axis that resolves what the
|
|
12
|
-
* badge *is*, since a bare Badge served five overlapping roles. Orchestrates
|
|
13
|
-
* the low-level visual props so you rarely set them directly:
|
|
14
|
-
* - `status` — a state marker (Active, Failed); pairs with `intent`.
|
|
15
|
-
* - `tag` — a neutral inline label (category, version).
|
|
16
|
-
* - `counter` — a compact numeric pill (replaces the `counter` boolean).
|
|
17
|
-
* - `dot` — a pure indicator, content hidden (replaces `variant="dot"`).
|
|
18
|
-
* - `chip` — a removable, interactive filter chip; pair with `removable`.
|
|
19
|
-
*
|
|
20
|
-
* Leave unset to drive the badge purely by the low-level props (back-compat).
|
|
21
|
-
* When set, `purpose` wins over `variant="dot"` / the `counter` boolean.
|
|
22
|
-
*/
|
|
23
|
-
purpose?: 'status' | 'tag' | 'counter' | 'dot' | 'chip';
|
|
12
|
+
interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter' | 'removable' | 'interactive'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
24
13
|
/** Add a pulsing animation to draw attention (e.g. for live indicators). */
|
|
25
14
|
pulse?: boolean;
|
|
26
15
|
/** Visually disable the badge (reduced opacity, no pointer events). */
|
|
@@ -29,7 +18,7 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
|
|
|
29
18
|
border?: boolean;
|
|
30
19
|
/** Anchor the badge absolutely within a `position: relative` parent. */
|
|
31
20
|
placement?: BadgePlacement;
|
|
32
|
-
/** Click handler. Automatically enables interactive styles. */
|
|
21
|
+
/** Click handler. Automatically enables interactive styles (and `role="button"`). */
|
|
33
22
|
onclick?: (event: MouseEvent) => void;
|
|
34
23
|
/** Called when the hover state changes. */
|
|
35
24
|
onHover?: (hovered: boolean) => void;
|
|
@@ -46,30 +35,73 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
|
|
|
46
35
|
* and make the custom look reusable across the project.
|
|
47
36
|
*/
|
|
48
37
|
preset?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
38
|
+
/**
|
|
39
|
+
* ARIA role. A static badge is announced as `"status"`; an interactive badge
|
|
40
|
+
* (`onclick` or `purpose="chip"`, when not `disabled`) defaults to `"button"`
|
|
41
|
+
* so assistive tech announces its activation semantics. Set explicitly to
|
|
42
|
+
* override — e.g. `"alert"` for time-sensitive notifications. An explicit
|
|
43
|
+
* value always wins over the derived default.
|
|
44
|
+
*/
|
|
45
|
+
role?: 'status' | 'alert' | 'badge' | 'button';
|
|
51
46
|
/** Micro-interaction preset. Only applies when the badge is interactive. */
|
|
52
47
|
mint?: MintProp;
|
|
53
48
|
}
|
|
54
49
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* hover-scale on a 2.5 × 2.5 px dot is never the intended UI.
|
|
50
|
+
* The label-only props a pure-indicator dot forbids. An invisible remove
|
|
51
|
+
* button, counter shape, or hover-scale on a 2.5 × 2.5 px dot is never the
|
|
52
|
+
* intended UI, so every dot arm excludes them at the type level.
|
|
59
53
|
*/
|
|
60
|
-
interface
|
|
61
|
-
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
|
|
62
|
-
variant: 'dot';
|
|
54
|
+
interface BadgeDotForbiddenProps {
|
|
63
55
|
children?: never;
|
|
64
56
|
counter?: never;
|
|
65
57
|
removable?: never;
|
|
66
58
|
interactive?: never;
|
|
67
59
|
onRemove?: never;
|
|
68
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Dot badge selected by the canonical `purpose="dot"` — a pure indicator whose
|
|
63
|
+
* content is hidden. `variant` is inert here (the dot look always wins), and
|
|
64
|
+
* the label-only props are excluded by {@link BadgeDotForbiddenProps}.
|
|
65
|
+
*/
|
|
66
|
+
interface BadgeDotByPurposeProps extends BadgeBaseProps, BadgeDotForbiddenProps {
|
|
67
|
+
/**
|
|
68
|
+
* The canonical dot spelling — forces the pure-indicator look regardless of
|
|
69
|
+
* `variant`. For the label roles use `status` / `tag` / `counter` / `chip`
|
|
70
|
+
* (their own arm).
|
|
71
|
+
*/
|
|
72
|
+
purpose: 'dot';
|
|
73
|
+
/** Inert under `purpose="dot"`; accepted only so a leftover `variant` compiles. */
|
|
74
|
+
variant?: 'filled' | 'outlined' | 'soft' | 'dot';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Dot badge selected by the deprecated `variant="dot"`. Prefer `purpose="dot"`.
|
|
78
|
+
* Same exclusions as {@link BadgeDotByPurposeProps}.
|
|
79
|
+
*/
|
|
80
|
+
interface BadgeDotProps extends BadgeBaseProps, BadgeDotForbiddenProps {
|
|
81
|
+
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
|
|
82
|
+
variant: 'dot';
|
|
83
|
+
/** Only the dot purpose is non-contradictory with `variant="dot"`. */
|
|
84
|
+
purpose?: 'dot';
|
|
85
|
+
}
|
|
69
86
|
/**
|
|
70
87
|
* Label-style badge — accepts content, counter shape, remove button, etc.
|
|
71
88
|
*/
|
|
72
89
|
interface BadgeStandardProps extends BadgeBaseProps {
|
|
90
|
+
/**
|
|
91
|
+
* The badge's semantic purpose — the canonical axis that resolves what the
|
|
92
|
+
* badge *is*, since a bare Badge served overlapping roles. Orchestrates the
|
|
93
|
+
* low-level visual props so you rarely set them directly:
|
|
94
|
+
* - `status` — a state marker (Active, Failed); pairs with `intent`.
|
|
95
|
+
* - `tag` — a neutral inline label (category, version).
|
|
96
|
+
* - `counter` — a compact numeric pill (replaces the `counter` boolean).
|
|
97
|
+
* - `chip` — a removable, interactive filter chip; pair with `removable`.
|
|
98
|
+
*
|
|
99
|
+
* For a pure indicator use `purpose="dot"` (its own arm — it forbids
|
|
100
|
+
* content / counter / remove). Leave unset to drive the badge purely by the
|
|
101
|
+
* low-level props (back-compat); when set, `purpose` wins over the `counter`
|
|
102
|
+
* boolean.
|
|
103
|
+
*/
|
|
104
|
+
purpose?: 'status' | 'tag' | 'counter' | 'chip';
|
|
73
105
|
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. @default 'filled' */
|
|
74
106
|
variant?: 'filled' | 'outlined' | 'soft';
|
|
75
107
|
/** Badge content (text, icons, numbers). */
|
|
@@ -93,10 +125,13 @@ interface BadgeStandardProps extends BadgeBaseProps {
|
|
|
93
125
|
* @related Alert
|
|
94
126
|
* @related Toast
|
|
95
127
|
*
|
|
96
|
-
* Badge props are a discriminated union
|
|
97
|
-
*
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
128
|
+
* Badge props are a discriminated union. The pure-indicator dot — spelled
|
|
129
|
+
* canonically as `purpose="dot"` or via the deprecated `variant="dot"` —
|
|
130
|
+
* forbids `children` / `counter` / `removable` / `interactive` / `onRemove`
|
|
131
|
+
* at the type level, while the label arms (`filled` / `outlined` / `soft`;
|
|
132
|
+
* `purpose` `status` / `tag` / `counter` / `chip`) accept the full surface.
|
|
133
|
+
* An interactive badge (`onclick` or `purpose="chip"`) is announced as a
|
|
134
|
+
* `button`, a static one as `status` (override via `role`).
|
|
100
135
|
*
|
|
101
136
|
* @example Purpose-driven (canonical) — the intent reads from `purpose`
|
|
102
137
|
* ```svelte
|
|
@@ -114,6 +149,6 @@ interface BadgeStandardProps extends BadgeBaseProps {
|
|
|
114
149
|
* </div>
|
|
115
150
|
* ```
|
|
116
151
|
*/
|
|
117
|
-
export type BadgeProps = BadgeDotProps | BadgeStandardProps;
|
|
152
|
+
export type BadgeProps = BadgeDotByPurposeProps | BadgeDotProps | BadgeStandardProps;
|
|
118
153
|
export { default as Badge } from './Badge.svelte';
|
|
119
154
|
export { type BadgePlacement, type BadgeVariants, badgeVariants, PLACEMENT_VALUES } from './badge.variants.js';
|
|
@@ -153,25 +153,15 @@
|
|
|
153
153
|
`blocks-intent-{name}` class on the button root scopes the glow color
|
|
154
154
|
via `mint/styles.css`. */
|
|
155
155
|
|
|
156
|
-
:
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
@keyframes ripple {
|
|
166
|
-
0% {
|
|
167
|
-
transform: scale(0);
|
|
168
|
-
opacity: 1;
|
|
169
|
-
}
|
|
170
|
-
100% {
|
|
171
|
-
transform: scale(1);
|
|
172
|
-
opacity: 0;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
156
|
+
/* The ripple mint owns the ripple end-to-end: `mint/ripple.ts` appends a
|
|
157
|
+
`<span class="blocks-mint-ripple">` on click and drives it with the Web
|
|
158
|
+
Animations API, tinted `currentColor` so it resolves per variant/intent —
|
|
159
|
+
white on a filled button, the intent hue on outlined/ghost. Button used to
|
|
160
|
+
carry a competing `.blocks-ripple` rule here (a hardcoded
|
|
161
|
+
`rgba(255,255,255,0.4)` plus its own @keyframes) that no element ever
|
|
162
|
+
matched — nothing has emitted that class since the initial commit, and it
|
|
163
|
+
would have been invisible on light variants had it matched. Removed rather
|
|
164
|
+
than tokenised; see the same note in `mint/styles.css` (XC-12/PAG-1). */
|
|
175
165
|
|
|
176
166
|
:global(.sr-only) {
|
|
177
167
|
position: absolute;
|
|
@@ -185,10 +175,7 @@
|
|
|
185
175
|
border-width: 0;
|
|
186
176
|
}
|
|
187
177
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
opacity: 0 !important;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
178
|
+
/* No reduced-motion rule for the ripple here either: `mint/ripple.ts` bails at
|
|
179
|
+
click time on `prefers-reduced-motion`, and `mint/styles.css` hides
|
|
180
|
+
`.blocks-mint-ripple` outright — both on the class that actually exists. */
|
|
194
181
|
</style>
|
|
@@ -247,6 +247,10 @@
|
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
const ariaRole = $derived(selection === 'single' ? 'radiogroup' : 'group');
|
|
250
|
+
// ARIA allows `aria-orientation` on `radiogroup`, not on `group` — emitting it
|
|
251
|
+
// on the multi-selection arm is an `aria-allowed-attr` violation. The visual
|
|
252
|
+
// orientation is unaffected; it comes from the variant classes, not from ARIA.
|
|
253
|
+
const ariaOrientation = $derived(ariaRole === 'radiogroup' ? orientation : undefined);
|
|
250
254
|
</script>
|
|
251
255
|
|
|
252
256
|
<div
|
|
@@ -257,7 +261,7 @@
|
|
|
257
261
|
: styles.base({ class: [slotClasses?.base, className] })}
|
|
258
262
|
aria-label={ariaLabel}
|
|
259
263
|
aria-labelledby={ariaLabelledBy}
|
|
260
|
-
aria-orientation={
|
|
264
|
+
aria-orientation={ariaOrientation}
|
|
261
265
|
aria-disabled={disabled || undefined}
|
|
262
266
|
onkeydown={handleKeyDown}
|
|
263
267
|
{...restProps}
|
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
for={id}
|
|
127
127
|
>
|
|
128
128
|
<input
|
|
129
|
+
{...restProps}
|
|
129
130
|
bind:this={inputRef}
|
|
130
131
|
{id}
|
|
131
132
|
type="checkbox"
|
|
@@ -139,7 +140,6 @@
|
|
|
139
140
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
140
141
|
aria-describedby={describedBy}
|
|
141
142
|
onchange={handleChange}
|
|
142
|
-
{...restProps}
|
|
143
143
|
/>
|
|
144
144
|
|
|
145
145
|
<span
|
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
let internalOpen = $state(defaultOpen ?? false);
|
|
43
43
|
const isOpen = $derived(open !== undefined ? open : internalOpen);
|
|
44
44
|
|
|
45
|
+
// Single mutation point. Family contract (COMPONENT-API-CONVENTIONS.md
|
|
46
|
+
// §Open-state vocabulary): the transition is applied optimistically — `open`
|
|
47
|
+
// (or the uncontrolled seed) is written *before* `onOpenChange` fires, once
|
|
48
|
+
// per transition. With `bind:open` that write is the propagation. A consumer
|
|
49
|
+
// passing `open` without `bind:` must mirror every `onOpenChange` back into
|
|
50
|
+
// its state (Svelte can't distinguish `open={x}` from `bind:open={x}` at
|
|
51
|
+
// runtime, so a rejected transition is undetectable from in here). To veto
|
|
52
|
+
// transitions, own them instead: drive `open` from your source of truth and
|
|
53
|
+
// toggle it from a custom `trigger` snippet — see AccordionItem's
|
|
54
|
+
// collapsible=false handling.
|
|
45
55
|
function toggle() {
|
|
46
56
|
if (disabled) return;
|
|
47
57
|
const next = !isOpen;
|
|
@@ -32,11 +32,22 @@ import type { CollapsibleSlots, CollapsibleVariants } from './collapsible.varian
|
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
34
|
export interface CollapsibleProps extends CollapsibleVariants, Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
35
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Whether the content is visible. Supports bind:open. Trigger-driven transitions
|
|
37
|
+
* are applied optimistically: `open` is updated first, then `onOpenChange` reports
|
|
38
|
+
* the change. When passing `open` without `bind:`, mirror every `onOpenChange`
|
|
39
|
+
* back into your state — an ignored change leaves the component and your source
|
|
40
|
+
* of truth diverged. To conditionally reject transitions, drive `open` from your
|
|
41
|
+
* own state and toggle it from a custom `trigger` snippet instead of calling the
|
|
42
|
+
* provided `toggle`.
|
|
43
|
+
*/
|
|
36
44
|
open?: boolean;
|
|
37
45
|
/** Initial open state for uncontrolled usage @default false */
|
|
38
46
|
defaultOpen?: boolean;
|
|
39
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* Callback fired once per trigger-driven open transition, after the state is
|
|
49
|
+
* applied. Not fired for consumer writes via `bind:open`.
|
|
50
|
+
*/
|
|
40
51
|
onOpenChange?: (open: boolean) => void;
|
|
41
52
|
/** Disable the trigger @default false */
|
|
42
53
|
disabled?: boolean;
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
cancelLabel,
|
|
16
16
|
confirmIntent,
|
|
17
17
|
onConfirm,
|
|
18
|
+
onError,
|
|
18
19
|
onCancel,
|
|
19
20
|
loading = false,
|
|
20
21
|
closeOnBackdropClick = true,
|
|
@@ -39,6 +40,19 @@
|
|
|
39
40
|
busy = true;
|
|
40
41
|
await onConfirm();
|
|
41
42
|
open = false;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
// Failure contract: skip the auto-close (dialog stays open) and re-enable
|
|
45
|
+
// (busy cleared in `finally`) so the user can retry or cancel. The
|
|
46
|
+
// rejection is handed to `onError`; without one it is surfaced DEV-only
|
|
47
|
+
// instead of escaping the ignored onclick promise as an unhandled
|
|
48
|
+
// rejection (Combobox queryFn / Toast promise precedent). A throwing
|
|
49
|
+
// `onError` is a consumer bug and deliberately escapes (fail-loud,
|
|
50
|
+
// mirrors createCronRunner).
|
|
51
|
+
if (onError) {
|
|
52
|
+
onError(error);
|
|
53
|
+
} else if (import.meta.env?.DEV) {
|
|
54
|
+
console.error('[ConfirmDialog] onConfirm rejected:', error);
|
|
55
|
+
}
|
|
42
56
|
} finally {
|
|
43
57
|
busy = false;
|
|
44
58
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
2
3
|
import type { DialogIntent } from '../Dialog/index.js';
|
|
3
4
|
import type { DialogSlots } from '../Dialog/dialog.variants.js';
|
|
4
5
|
/**
|
|
@@ -15,7 +16,8 @@ export type ConfirmIntent = Exclude<DialogIntent, 'neutral'>;
|
|
|
15
16
|
*
|
|
16
17
|
* `onConfirm` may be `async`; while the returned promise is pending the
|
|
17
18
|
* dialog locks itself (no backdrop dismiss, no escape, confirm button
|
|
18
|
-
* shows a spinner). Auto-closes on resolve
|
|
19
|
+
* shows a spinner). Auto-closes on resolve; on reject it stays open and
|
|
20
|
+
* re-enables, handing the error to `onError`.
|
|
19
21
|
*
|
|
20
22
|
* @tag overlay
|
|
21
23
|
* @related Dialog
|
|
@@ -36,7 +38,7 @@ export type ConfirmIntent = Exclude<DialogIntent, 'neutral'>;
|
|
|
36
38
|
* />
|
|
37
39
|
* ```
|
|
38
40
|
*/
|
|
39
|
-
export interface ConfirmDialogProps {
|
|
41
|
+
export interface ConfirmDialogProps extends Omit<HTMLDialogAttributes, 'children' | 'open' | 'title' | 'draggable'> {
|
|
40
42
|
/** Controls visibility. Supports bind:open. */
|
|
41
43
|
open?: boolean;
|
|
42
44
|
/** Heading shown in the dialog header. */
|
|
@@ -59,9 +61,19 @@ export interface ConfirmDialogProps {
|
|
|
59
61
|
cancelLabel?: string;
|
|
60
62
|
/**
|
|
61
63
|
* Confirm handler. May return a promise — the dialog stays open and shows
|
|
62
|
-
* a loading state while it resolves, then auto-closes on success.
|
|
64
|
+
* a loading state while it resolves, then auto-closes on success. If the
|
|
65
|
+
* promise rejects the dialog stays open and re-enables; the rejection is
|
|
66
|
+
* reported via {@link ConfirmDialogProps.onError}.
|
|
63
67
|
*/
|
|
64
68
|
onConfirm?: () => void | Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Fired when an async `onConfirm` rejects (or a sync one throws). The
|
|
71
|
+
* dialog stays open and re-enables so the user can retry or cancel — use
|
|
72
|
+
* this to surface the failure (toast, inline message). Without a handler
|
|
73
|
+
* the rejection is logged DEV-only (`console.error`) and swallowed in
|
|
74
|
+
* production; it never escapes as an unhandled promise rejection.
|
|
75
|
+
*/
|
|
76
|
+
onError?: (error: unknown) => void;
|
|
65
77
|
/** Fired when the user cancels (button, backdrop, or Escape). */
|
|
66
78
|
onCancel?: () => void;
|
|
67
79
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { onDestroy, tick } from 'svelte';
|
|
7
7
|
import { fade, scale } from 'svelte/transition';
|
|
8
8
|
import { trapFocus, showDialogModal, closeDialogModal } from '../../utils/overlay';
|
|
9
|
+
import { composeHandlers } from '../../utils/compose-handlers';
|
|
9
10
|
import { overlayStack, getOverlayMotion } from '../../utils';
|
|
10
11
|
import Button from '../Button/Button.svelte';
|
|
11
12
|
import type { DialogProps } from './index';
|
|
@@ -34,6 +35,16 @@
|
|
|
34
35
|
unstyled: unstyledProp = false,
|
|
35
36
|
slotClasses: slotClassesProp = {},
|
|
36
37
|
preset,
|
|
38
|
+
// Pulled out of restProps so the `{...restProps}`-first spread (internal
|
|
39
|
+
// attributes win — see docs/COMPONENT-API-CONVENTIONS.md) can't clobber
|
|
40
|
+
// them, and so neither side loses: the behavioural handlers below are
|
|
41
|
+
// composed (internal first, consumer second) and the two ARIA attributes
|
|
42
|
+
// are merged, rather than one silently replacing the other.
|
|
43
|
+
onclick: onclickProp,
|
|
44
|
+
onkeydown: onkeydownProp,
|
|
45
|
+
onclose: oncloseProp,
|
|
46
|
+
'aria-labelledby': ariaLabelledby,
|
|
47
|
+
'aria-describedby': ariaDescribedby,
|
|
37
48
|
...restProps
|
|
38
49
|
}: DialogProps = $props();
|
|
39
50
|
|
|
@@ -134,6 +145,14 @@
|
|
|
134
145
|
const bodyId = `dialog-body-${uid}`;
|
|
135
146
|
const overlayId = `dialog-${uid}`;
|
|
136
147
|
|
|
148
|
+
// A rendered `title` owns the labelling; without one, a consumer-supplied
|
|
149
|
+
// `aria-labelledby` (e.g. a heading inside `children`) is the fallback, so
|
|
150
|
+
// external labelling survives the restProps-first spread.
|
|
151
|
+
const labelledBy = $derived(titleId ?? ariaLabelledby);
|
|
152
|
+
// Merged, not replaced: a consumer's description is supplemental to the
|
|
153
|
+
// dialog's own body — internal id first, consumer id last (mirrors Input).
|
|
154
|
+
const describedBy = $derived([bodyId, ariaDescribedby].filter(Boolean).join(' ') || undefined);
|
|
155
|
+
|
|
137
156
|
const variantProps: DialogVariants = $derived({ size, placement, intent });
|
|
138
157
|
const styles = $derived(dialogVariants(variantProps));
|
|
139
158
|
const slotClasses = $derived(
|
|
@@ -176,6 +195,12 @@
|
|
|
176
195
|
if (closeOnBackdropClick) requestClose();
|
|
177
196
|
}
|
|
178
197
|
|
|
198
|
+
// Native `close` event (ESC handled by the UA, form[method=dialog], .close()).
|
|
199
|
+
// Named rather than inline so it can be composed with a consumer `onclose`.
|
|
200
|
+
function handleNativeClose() {
|
|
201
|
+
if (open) requestClose();
|
|
202
|
+
}
|
|
203
|
+
|
|
179
204
|
function handleOutroEnd() {
|
|
180
205
|
closeDialogModal(dialogEl, previouslyFocused, releaseScrollLock);
|
|
181
206
|
releaseScrollLock = undefined;
|
|
@@ -230,18 +255,16 @@
|
|
|
230
255
|
|
|
231
256
|
{#if isVisible}
|
|
232
257
|
<dialog
|
|
258
|
+
{...restProps}
|
|
233
259
|
bind:this={dialogEl}
|
|
234
260
|
class={unstyled ? (slotClasses?.dialog ?? '') : styles.dialog({ class: slotClasses?.dialog })}
|
|
235
|
-
onclick={handleBackdropClick}
|
|
236
|
-
onkeydown={handleKeydown}
|
|
237
|
-
onclose={()
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
aria-labelledby={titleId}
|
|
241
|
-
aria-describedby={bodyId}
|
|
261
|
+
onclick={composeHandlers(handleBackdropClick, onclickProp)}
|
|
262
|
+
onkeydown={composeHandlers(handleKeydown, onkeydownProp)}
|
|
263
|
+
onclose={composeHandlers(handleNativeClose, oncloseProp)}
|
|
264
|
+
aria-labelledby={labelledBy}
|
|
265
|
+
aria-describedby={describedBy}
|
|
242
266
|
aria-modal="true"
|
|
243
267
|
data-state={open ? 'open' : 'closed'}
|
|
244
|
-
{...restProps}
|
|
245
268
|
>
|
|
246
269
|
{#if open}
|
|
247
270
|
<div
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { onDestroy, tick } from 'svelte';
|
|
7
7
|
import { fade, fly } from 'svelte/transition';
|
|
8
8
|
import { trapFocus, showDialogModal, closeDialogModal } from '../../utils/overlay';
|
|
9
|
+
import { composeHandlers } from '../../utils/compose-handlers';
|
|
9
10
|
import { overlayStack, getOverlayMotion } from '../../utils';
|
|
10
11
|
import Button from '../Button/Button.svelte';
|
|
11
12
|
import type { DrawerProps } from './index';
|
|
@@ -34,6 +35,15 @@
|
|
|
34
35
|
unstyled: unstyledProp = false,
|
|
35
36
|
slotClasses: slotClassesProp = {},
|
|
36
37
|
preset,
|
|
38
|
+
// Pulled out of restProps so the `{...restProps}`-first spread (internal
|
|
39
|
+
// attributes win — see docs/COMPONENT-API-CONVENTIONS.md) can't clobber
|
|
40
|
+
// them, and so neither side loses: the behavioural handlers below are
|
|
41
|
+
// composed (internal first, consumer second) and the two ARIA attributes
|
|
42
|
+
// are merged, rather than one silently replacing the other. Mirrors Dialog.
|
|
43
|
+
onkeydown: onkeydownProp,
|
|
44
|
+
onclose: oncloseProp,
|
|
45
|
+
'aria-labelledby': ariaLabelledby,
|
|
46
|
+
'aria-describedby': ariaDescribedby,
|
|
37
47
|
...restProps
|
|
38
48
|
}: DrawerProps = $props();
|
|
39
49
|
|
|
@@ -66,6 +76,14 @@
|
|
|
66
76
|
const bodyId = `drawer-body-${uid}`;
|
|
67
77
|
const overlayId = `drawer-${uid}`;
|
|
68
78
|
|
|
79
|
+
// A rendered `title` owns the labelling; without one, a consumer-supplied
|
|
80
|
+
// `aria-labelledby` (e.g. a heading inside `children`) is the fallback, so
|
|
81
|
+
// external labelling survives the restProps-first spread.
|
|
82
|
+
const labelledBy = $derived(titleId ?? ariaLabelledby);
|
|
83
|
+
// Merged, not replaced: a consumer's description is supplemental to the
|
|
84
|
+
// drawer's own body — internal id first, consumer id last (mirrors Input).
|
|
85
|
+
const describedBy = $derived([bodyId, ariaDescribedby].filter(Boolean).join(' ') || undefined);
|
|
86
|
+
|
|
69
87
|
const variantProps: DrawerVariants = $derived({ placement, size, intent, accentEdge });
|
|
70
88
|
const styles = $derived(drawerVariants(variantProps));
|
|
71
89
|
const slotClasses = $derived(
|
|
@@ -159,6 +177,12 @@
|
|
|
159
177
|
if (event.key === 'Tab') trapFocus(event, panelElement);
|
|
160
178
|
}
|
|
161
179
|
|
|
180
|
+
// Native `close` event (ESC handled by the UA, form[method=dialog], .close()).
|
|
181
|
+
// Named rather than inline so it can be composed with a consumer `onclose`.
|
|
182
|
+
function handleNativeClose() {
|
|
183
|
+
if (open) requestClose();
|
|
184
|
+
}
|
|
185
|
+
|
|
162
186
|
function handleOutroEnd() {
|
|
163
187
|
closeDialogModal(dialogElement, previouslyFocused, releaseScrollLock);
|
|
164
188
|
releaseScrollLock = undefined;
|
|
@@ -171,17 +195,15 @@
|
|
|
171
195
|
|
|
172
196
|
{#if isVisible}
|
|
173
197
|
<dialog
|
|
198
|
+
{...restProps}
|
|
174
199
|
bind:this={dialogElement}
|
|
175
200
|
class={unstyled ? (slotClasses?.dialog ?? '') : styles.dialog({ class: slotClasses?.dialog })}
|
|
176
|
-
onkeydown={handleKeydown}
|
|
177
|
-
onclose={()
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
aria-labelledby={titleId}
|
|
181
|
-
aria-describedby={bodyId}
|
|
201
|
+
onkeydown={composeHandlers(handleKeydown, onkeydownProp)}
|
|
202
|
+
onclose={composeHandlers(handleNativeClose, oncloseProp)}
|
|
203
|
+
aria-labelledby={labelledBy}
|
|
204
|
+
aria-describedby={describedBy}
|
|
182
205
|
aria-modal="true"
|
|
183
206
|
data-state={open ? 'open' : 'closed'}
|
|
184
|
-
{...restProps}
|
|
185
207
|
>
|
|
186
208
|
{#if open}
|
|
187
209
|
<div
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
persistVersion = 1,
|
|
44
44
|
persistNamespace,
|
|
45
45
|
onkeydown: userOnKeydown,
|
|
46
|
+
id: idProp,
|
|
46
47
|
'aria-describedby': ariaDescribedby,
|
|
47
48
|
...restProps
|
|
48
49
|
}: InputProps = $props();
|
|
@@ -118,9 +119,14 @@
|
|
|
118
119
|
);
|
|
119
120
|
|
|
120
121
|
// ARIA wiring is shared with every form primitive — see XC-2.
|
|
122
|
+
// A consumer-supplied `id` wins over the generated one: pairing an Input
|
|
123
|
+
// with an external `<label for>` is only possible if the id we are handed
|
|
124
|
+
// actually reaches the `<input>`. `$props.id()` may only appear as a
|
|
125
|
+
// top-level initializer, hence the two-step derive.
|
|
121
126
|
const propsId = $props.id();
|
|
127
|
+
const fieldId = $derived(idProp ?? `input-${propsId}`);
|
|
122
128
|
const ff = useFormField(() => ({
|
|
123
|
-
fieldId
|
|
129
|
+
fieldId,
|
|
124
130
|
helper,
|
|
125
131
|
error,
|
|
126
132
|
required,
|
|
@@ -111,13 +111,13 @@ export const journeyTimelineVariants = tv({
|
|
|
111
111
|
},
|
|
112
112
|
size: {
|
|
113
113
|
sm: {
|
|
114
|
-
meta: 'text-
|
|
114
|
+
meta: 'text-2xs',
|
|
115
115
|
marker: 'size-2.5',
|
|
116
116
|
node: 'gap-x-2.5',
|
|
117
117
|
card: '-mx-1.5 px-1.5 py-1.5',
|
|
118
118
|
title: 'text-xs',
|
|
119
|
-
subtitle: 'text-
|
|
120
|
-
segment: 'text-
|
|
119
|
+
subtitle: 'text-2xs',
|
|
120
|
+
segment: 'text-2xs',
|
|
121
121
|
content: 'pb-4',
|
|
122
122
|
detailContent: 'text-xs pt-2',
|
|
123
123
|
panel: 'p-3 text-xs'
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { getTierContext } from '../../utils/tier-context';
|
|
9
9
|
const ChevronDownIcon = resolveIcon('chevronDown', ChevronDownIconDefault);
|
|
10
10
|
import Popover from '../Popover/Popover.svelte';
|
|
11
|
+
import { popoverMotion } from '../Popover/popover.variants';
|
|
11
12
|
import { setMenuContext, type MenuContext, type MenuRegistryItem } from './menu.context';
|
|
12
13
|
import MenuItemComp from './MenuItem.svelte';
|
|
13
14
|
import MenuSubmenu from './MenuSubmenu.svelte';
|
|
@@ -471,6 +472,12 @@
|
|
|
471
472
|
only painted layer. Without this, the panel renders as two stacked
|
|
472
473
|
surfaces (Popover wrapper + Menu content) and shows a near-empty box
|
|
473
474
|
with double borders.
|
|
475
|
+
|
|
476
|
+
The stripped default ALSO carried the enter/exit motion, so the exact
|
|
477
|
+
fragment (`popoverMotion`) is re-applied via `class` — it survives
|
|
478
|
+
Popover's `unstyled` by design and animates the transparent wrapper,
|
|
479
|
+
which fades/scales the Menu chrome inside it. A Menu-level `unstyled`
|
|
480
|
+
strips it again; consumers rebuild on the panel's `data-state`.
|
|
474
481
|
-->
|
|
475
482
|
<Popover
|
|
476
483
|
bind:open
|
|
@@ -479,6 +486,7 @@
|
|
|
479
486
|
{usePortal}
|
|
480
487
|
autoTrigger={false}
|
|
481
488
|
unstyled
|
|
489
|
+
class={unstyled ? undefined : popoverMotion}
|
|
482
490
|
syncMinWidth={contextTrigger ? false : syncWidth}
|
|
483
491
|
offsetDistance={effectiveTier === 'commit' ? 8 : 4}
|
|
484
492
|
trigger={contextTrigger ? undefined : triggerContent}
|
|
@@ -7,18 +7,9 @@ import type { Placement } from '../../utils/floating.js';
|
|
|
7
7
|
import type { InteractiveTier } from '../../utils/tier-context.js';
|
|
8
8
|
import type { MenuSlots } from './menu.variants.js';
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `Select` (or `Combobox` for searchable). Menu and Select are deliberately
|
|
14
|
-
* disjoint: Menu's ARIA semantics, keyboard model (Tab through items, Enter
|
|
15
|
-
* triggers), and visual chrome match the Action surface family; Select's
|
|
16
|
-
* `role="listbox"` + arrow-navigation match the Form surface family.
|
|
17
|
-
*
|
|
18
|
-
* @tag action
|
|
19
|
-
* @related Select
|
|
20
|
-
* @related Combobox
|
|
21
|
-
* @related Popover
|
|
10
|
+
* Menu-specific props. The catalog JSDoc (`@description`/`@tag`/`@related`)
|
|
11
|
+
* lives on `MenuProps` below — the interface docs-gen actually picks up —
|
|
12
|
+
* so it is not duplicated here.
|
|
22
13
|
*
|
|
23
14
|
* @example Items array with `onSelect` callbacks
|
|
24
15
|
* ```svelte
|
|
@@ -172,7 +163,14 @@ export interface MenuSpecificProps<TItem extends MenuItemType = MenuItemType> {
|
|
|
172
163
|
mint?: MintProp;
|
|
173
164
|
}
|
|
174
165
|
/**
|
|
175
|
-
* @description Action menu triggered by a button with nested
|
|
166
|
+
* @description Action menu (`role="menu"`) triggered by a button, with nested
|
|
167
|
+
* submenus, sections, icons, and separators. Items are verbs the user can
|
|
168
|
+
* invoke — Edit, Delete, Share, Export — and dispatch an `onSelect` callback
|
|
169
|
+
* when activated; Menu holds no selection state. For picking a value from a
|
|
170
|
+
* list use `Select` (or `Combobox` for searchable). Menu and Select are
|
|
171
|
+
* deliberately disjoint: Menu's `role="menu"`/`menuitem` semantics with
|
|
172
|
+
* arrow-key roving and Action-family chrome versus Select's `role="listbox"`
|
|
173
|
+
* value commitment with Form-family chrome.
|
|
176
174
|
*
|
|
177
175
|
* @tag action
|
|
178
176
|
* @related Select
|