@urbicon-ui/blocks 6.21.1 → 6.21.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.
- package/dist/components/NumberInput/NumberInput.svelte +197 -0
- package/dist/components/NumberInput/NumberInput.svelte.d.ts +4 -0
- package/dist/components/NumberInput/index.d.ts +55 -0
- package/dist/components/NumberInput/index.js +1 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +1 -0
- package/dist/i18n/index.d.ts +6 -0
- package/dist/primitives/Badge/Badge.svelte +12 -4
- package/dist/primitives/Badge/index.d.ts +26 -6
- package/dist/primitives/Breadcrumb/Breadcrumb.svelte +11 -6
- package/dist/primitives/Breadcrumb/index.d.ts +2 -2
- package/dist/primitives/Checkbox/Checkbox.svelte +3 -3
- package/dist/primitives/Combobox/Combobox.svelte +478 -104
- package/dist/primitives/Combobox/combobox.variants.d.ts +89 -17
- package/dist/primitives/Combobox/combobox.variants.js +94 -13
- package/dist/primitives/Combobox/index.d.ts +166 -51
- package/dist/primitives/ConfirmDialog/index.d.ts +19 -0
- package/dist/primitives/Dialog/Dialog.svelte +74 -0
- package/dist/primitives/Dialog/index.d.ts +12 -1
- package/dist/primitives/Drawer/Drawer.svelte +2 -1
- package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
- package/dist/primitives/Drawer/drawer.variants.js +45 -10
- package/dist/primitives/Drawer/index.d.ts +13 -4
- package/dist/primitives/FormField/FormField.svelte +6 -6
- package/dist/primitives/FormField/index.d.ts +13 -4
- package/dist/primitives/Input/Input.svelte +8 -4
- package/dist/primitives/Menu/Menu.svelte +66 -4
- package/dist/primitives/Menu/index.d.ts +25 -0
- package/dist/primitives/Pagination/Pagination.svelte +28 -13
- package/dist/primitives/Pagination/index.d.ts +44 -0
- package/dist/primitives/Popover/Popover.svelte +10 -0
- package/dist/primitives/Popover/index.d.ts +3 -3
- package/dist/primitives/RadioGroup/RadioGroup.svelte +3 -3
- package/dist/primitives/Select/Select.svelte +21 -11
- package/dist/primitives/Select/index.d.ts +7 -0
- package/dist/primitives/Select/select.variants.d.ts +17 -17
- package/dist/primitives/Select/select.variants.js +18 -0
- package/dist/primitives/Slider/Slider.svelte +4 -8
- package/dist/primitives/Textarea/Textarea.svelte +4 -5
- package/dist/primitives/Textarea/textarea.variants.d.ts +7 -7
- package/dist/primitives/Textarea/textarea.variants.js +5 -2
- package/dist/primitives/Toast/Toaster.svelte +42 -2
- package/dist/primitives/Toast/index.d.ts +30 -0
- package/dist/primitives/Toast/toast.store.svelte.d.ts +25 -1
- package/dist/primitives/Toast/toast.store.svelte.js +91 -1
- package/dist/primitives/Toast/toast.variants.d.ts +18 -0
- package/dist/primitives/Toast/toast.variants.js +15 -0
- package/dist/primitives/Toggle/Toggle.svelte +17 -3
- package/dist/primitives/Toggle/index.d.ts +8 -2
- package/dist/primitives/Toggle/toggle.variants.d.ts +7 -0
- package/dist/primitives/Toggle/toggle.variants.js +26 -0
- package/dist/primitives/Tooltip/Tooltip.svelte +47 -21
- package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +1 -1
- package/dist/primitives/Tooltip/index.d.ts +31 -3
- package/dist/primitives/Tooltip/tooltip.variants.d.ts +4 -4
- package/dist/primitives/Tooltip/tooltip.variants.js +2 -2
- package/dist/primitives/index.d.ts +3 -3
- package/dist/translations/de.d.ts +3 -0
- package/dist/translations/de.js +4 -1
- package/dist/translations/en.d.ts +3 -0
- package/dist/translations/en.js +4 -1
- package/dist/utils/guide.svelte.d.ts +8 -2
- package/dist/utils/guide.svelte.js +86 -5
- package/dist/utils/use-form-field.svelte.d.ts +9 -9
- package/dist/utils/use-form-field.svelte.js +12 -12
- package/package.json +3 -3
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { DialogIntent } from '../Dialog/index.js';
|
|
3
|
+
import type { DialogSlots } from '../Dialog/dialog.variants.js';
|
|
3
4
|
/**
|
|
4
5
|
* Intent applied to the confirm button. Reuses the standard intent palette
|
|
5
6
|
* via {@link DialogIntent} (with `neutral` mapped to `primary` for visual
|
|
@@ -88,5 +89,23 @@ export interface ConfirmDialogProps {
|
|
|
88
89
|
transitionEasing?: (t: number) => number;
|
|
89
90
|
/** Optional richer markup rendered below `description`. */
|
|
90
91
|
children?: Snippet;
|
|
92
|
+
/** Extra classes merged onto the dialog element. Forwarded to {@link DialogProps.class}. */
|
|
93
|
+
class?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Strip the underlying Dialog's default styles. Combine with `slotClasses`
|
|
96
|
+
* for a fully custom appearance. @default false
|
|
97
|
+
*/
|
|
98
|
+
unstyled?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Per-slot class overrides, forwarded to the underlying Dialog.
|
|
101
|
+
* Slots: dialog | backdrop | panel | content | header | title | body | footer.
|
|
102
|
+
*/
|
|
103
|
+
slotClasses?: Partial<Record<DialogSlots, string>>;
|
|
104
|
+
/**
|
|
105
|
+
* Apply a named preset registered via `<BlocksProvider presets={{ Dialog: {...} }}>`.
|
|
106
|
+
* Resolved against the `Dialog` component key — ConfirmDialog shares the
|
|
107
|
+
* Dialog preset space instead of introducing a parallel one.
|
|
108
|
+
*/
|
|
109
|
+
preset?: string;
|
|
91
110
|
}
|
|
92
111
|
export { default as ConfirmDialog } from './ConfirmDialog.svelte';
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
closeOnBackdropClick = true,
|
|
27
27
|
closeOnEscape = true,
|
|
28
28
|
hideCloseButton = false,
|
|
29
|
+
draggable = false,
|
|
29
30
|
transitionDuration,
|
|
30
31
|
transitionEasing,
|
|
31
32
|
onClose,
|
|
@@ -70,6 +71,64 @@
|
|
|
70
71
|
|
|
71
72
|
const structured = $derived(!!title);
|
|
72
73
|
|
|
74
|
+
// svelte-ignore state_referenced_locally
|
|
75
|
+
if (import.meta.env?.DEV && draggable && !title) {
|
|
76
|
+
console.warn(
|
|
77
|
+
'[Dialog] `draggable` needs a `title` — the header is the drag handle. Without a title no header renders, so dragging is a no-op.'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Drag offset from the centred position (px). Only ever non-zero while
|
|
82
|
+
// `draggable` and the user has grabbed the header. Reset on each open so a
|
|
83
|
+
// reopened dialog always starts centred.
|
|
84
|
+
let dragX = $state(0);
|
|
85
|
+
let dragY = $state(0);
|
|
86
|
+
|
|
87
|
+
// Attachment wiring the header as a drag handle. Kept off the markup as an
|
|
88
|
+
// `{@attach}` (not inline `onpointer*`) so the static `<header>` doesn't trip
|
|
89
|
+
// the a11y "non-interactive element with handlers needs a role" rule, and so
|
|
90
|
+
// the listeners tear down cleanly when `draggable` flips off. Pointer capture
|
|
91
|
+
// keeps the drag alive when the cursor outruns the header; header buttons
|
|
92
|
+
// (close) are exempted so they still click.
|
|
93
|
+
function draggableHandle(node: HTMLElement) {
|
|
94
|
+
node.style.touchAction = 'none';
|
|
95
|
+
node.style.userSelect = 'none';
|
|
96
|
+
node.style.cursor = 'move';
|
|
97
|
+
let dragging = false;
|
|
98
|
+
let startX = 0;
|
|
99
|
+
let startY = 0;
|
|
100
|
+
const onDown = (event: PointerEvent) => {
|
|
101
|
+
if ((event.target as HTMLElement).closest('button')) return;
|
|
102
|
+
dragging = true;
|
|
103
|
+
startX = event.clientX - dragX;
|
|
104
|
+
startY = event.clientY - dragY;
|
|
105
|
+
node.setPointerCapture(event.pointerId);
|
|
106
|
+
};
|
|
107
|
+
const onMove = (event: PointerEvent) => {
|
|
108
|
+
if (!dragging) return;
|
|
109
|
+
dragX = event.clientX - startX;
|
|
110
|
+
dragY = event.clientY - startY;
|
|
111
|
+
};
|
|
112
|
+
const onUp = (event: PointerEvent) => {
|
|
113
|
+
if (!dragging) return;
|
|
114
|
+
dragging = false;
|
|
115
|
+
node.releasePointerCapture(event.pointerId);
|
|
116
|
+
};
|
|
117
|
+
node.addEventListener('pointerdown', onDown);
|
|
118
|
+
node.addEventListener('pointermove', onMove);
|
|
119
|
+
node.addEventListener('pointerup', onUp);
|
|
120
|
+
return () => {
|
|
121
|
+
node.removeEventListener('pointerdown', onDown);
|
|
122
|
+
node.removeEventListener('pointermove', onMove);
|
|
123
|
+
node.removeEventListener('pointerup', onUp);
|
|
124
|
+
// Undo the inline styles too, so toggling `draggable` off at runtime
|
|
125
|
+
// doesn't leave the header stuck with `cursor: move` / unselectable text.
|
|
126
|
+
node.style.touchAction = '';
|
|
127
|
+
node.style.userSelect = '';
|
|
128
|
+
node.style.cursor = '';
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
73
132
|
const uid = $props.id();
|
|
74
133
|
const titleId = $derived(title ? `dialog-title-${uid}` : undefined);
|
|
75
134
|
const bodyId = `dialog-body-${uid}`;
|
|
@@ -124,6 +183,19 @@
|
|
|
124
183
|
previouslyFocused = null;
|
|
125
184
|
}
|
|
126
185
|
|
|
186
|
+
// Reset the drag offset whenever the dialog opens so a reopened dialog always
|
|
187
|
+
// starts centred — keyed on `open`, not the `open && !isVisible` transition.
|
|
188
|
+
// Reopening *during* the close outro (isVisible still true) must still recentre;
|
|
189
|
+
// the old transition-guarded reset silently skipped that case, restoring the
|
|
190
|
+
// stale drag position. `open` is constant during a drag, so this never fights
|
|
191
|
+
// the pointer handlers.
|
|
192
|
+
$effect(() => {
|
|
193
|
+
if (open) {
|
|
194
|
+
dragX = 0;
|
|
195
|
+
dragY = 0;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
127
199
|
$effect(() => {
|
|
128
200
|
if (open && !isVisible) {
|
|
129
201
|
isVisible = true;
|
|
@@ -191,6 +263,7 @@
|
|
|
191
263
|
: styles.panel({ class: [slotClasses?.panel, className] })}
|
|
192
264
|
data-intent={intent}
|
|
193
265
|
role="document"
|
|
266
|
+
style:translate={dragX !== 0 || dragY !== 0 ? `${dragX}px ${dragY}px` : undefined}
|
|
194
267
|
transition:scale={{
|
|
195
268
|
duration: motion.enterDuration,
|
|
196
269
|
easing: motion.easing,
|
|
@@ -204,6 +277,7 @@
|
|
|
204
277
|
class={unstyled
|
|
205
278
|
? (slotClasses?.header ?? '')
|
|
206
279
|
: styles.header({ class: slotClasses?.header })}
|
|
280
|
+
{@attach draggable ? draggableHandle : undefined}
|
|
207
281
|
>
|
|
208
282
|
<h2
|
|
209
283
|
class={unstyled
|
|
@@ -27,7 +27,7 @@ import type { DialogSlots, DialogVariants } from './dialog.variants.js';
|
|
|
27
27
|
* </Dialog>
|
|
28
28
|
* ```
|
|
29
29
|
*/
|
|
30
|
-
export interface DialogProps extends Omit<HTMLDialogAttributes, 'children' | 'open'> {
|
|
30
|
+
export interface DialogProps extends Omit<HTMLDialogAttributes, 'children' | 'open' | 'draggable'> {
|
|
31
31
|
/** Controls whether the dialog is visible. Supports bind:open. */
|
|
32
32
|
open?: boolean;
|
|
33
33
|
/** Dialog body content. When `title` is omitted the content fills the entire panel. */
|
|
@@ -52,6 +52,17 @@ export interface DialogProps extends Omit<HTMLDialogAttributes, 'children' | 'op
|
|
|
52
52
|
closeOnBackdropClick?: boolean;
|
|
53
53
|
/** Whether pressing Escape dismisses the dialog. @default true */
|
|
54
54
|
closeOnEscape?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Let the user reposition the dialog by dragging its header. Off by default.
|
|
57
|
+
* The structured header (rendered when `title` is set) becomes the drag
|
|
58
|
+
* handle; the close button and any header controls stay clickable, and the
|
|
59
|
+
* offset resets each time the dialog reopens. Most useful for a dialog the
|
|
60
|
+
* user may want to shove aside to see the content behind it. Requires a
|
|
61
|
+
* `title` — without a header there is nothing to grab. Overrides the native
|
|
62
|
+
* HTML `draggable` attribute, which has no meaningful use on a dialog.
|
|
63
|
+
* @default false
|
|
64
|
+
*/
|
|
65
|
+
draggable?: boolean;
|
|
55
66
|
/**
|
|
56
67
|
* Hides the built-in close button. Only takes effect when `title` is
|
|
57
68
|
* also set — the close button only renders in the structured (titled)
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
placement = 'right',
|
|
24
24
|
size = 'md',
|
|
25
25
|
intent = 'neutral',
|
|
26
|
+
accentEdge = false,
|
|
26
27
|
onClose,
|
|
27
28
|
hideCloseButton = false,
|
|
28
29
|
closeOnBackdropClick = true,
|
|
@@ -65,7 +66,7 @@
|
|
|
65
66
|
const bodyId = `drawer-body-${uid}`;
|
|
66
67
|
const overlayId = `drawer-${uid}`;
|
|
67
68
|
|
|
68
|
-
const variantProps: DrawerVariants = $derived({ placement, size, intent });
|
|
69
|
+
const variantProps: DrawerVariants = $derived({ placement, size, intent, accentEdge });
|
|
69
70
|
const styles = $derived(drawerVariants(variantProps));
|
|
70
71
|
const slotClasses = $derived(
|
|
71
72
|
resolveSlotClasses(blocksConfig, 'Drawer', preset, variantProps, slotClassesProp)
|
|
@@ -3,11 +3,13 @@ export declare const drawerVariants: ((props?: {
|
|
|
3
3
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
4
4
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
5
5
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
6
|
+
accentEdge?: boolean | undefined;
|
|
6
7
|
} | undefined) => {
|
|
7
8
|
dialog: (props?: ({
|
|
8
9
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
9
10
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
10
11
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
12
|
+
accentEdge?: boolean | undefined;
|
|
11
13
|
} & {
|
|
12
14
|
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
15
|
}) | undefined) => string;
|
|
@@ -15,6 +17,7 @@ export declare const drawerVariants: ((props?: {
|
|
|
15
17
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
16
18
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
17
19
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
20
|
+
accentEdge?: boolean | undefined;
|
|
18
21
|
} & {
|
|
19
22
|
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;
|
|
20
23
|
}) | undefined) => string;
|
|
@@ -22,6 +25,7 @@ export declare const drawerVariants: ((props?: {
|
|
|
22
25
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
23
26
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
24
27
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
28
|
+
accentEdge?: boolean | undefined;
|
|
25
29
|
} & {
|
|
26
30
|
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;
|
|
27
31
|
}) | undefined) => string;
|
|
@@ -29,6 +33,7 @@ export declare const drawerVariants: ((props?: {
|
|
|
29
33
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
30
34
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
31
35
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
36
|
+
accentEdge?: boolean | undefined;
|
|
32
37
|
} & {
|
|
33
38
|
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;
|
|
34
39
|
}) | undefined) => string;
|
|
@@ -36,6 +41,7 @@ export declare const drawerVariants: ((props?: {
|
|
|
36
41
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
37
42
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
38
43
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
44
|
+
accentEdge?: boolean | undefined;
|
|
39
45
|
} & {
|
|
40
46
|
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;
|
|
41
47
|
}) | undefined) => string;
|
|
@@ -43,6 +49,7 @@ export declare const drawerVariants: ((props?: {
|
|
|
43
49
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
44
50
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
45
51
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
52
|
+
accentEdge?: boolean | undefined;
|
|
46
53
|
} & {
|
|
47
54
|
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
55
|
}) | undefined) => string;
|
|
@@ -50,6 +57,7 @@ export declare const drawerVariants: ((props?: {
|
|
|
50
57
|
placement?: "top" | "right" | "bottom" | "left" | undefined;
|
|
51
58
|
size?: "sm" | "md" | "lg" | "xl" | "full" | undefined;
|
|
52
59
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
60
|
+
accentEdge?: boolean | undefined;
|
|
53
61
|
} & {
|
|
54
62
|
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;
|
|
55
63
|
}) | undefined) => string;
|
|
@@ -53,13 +53,24 @@ export const drawerVariants = tv({
|
|
|
53
53
|
xl: {},
|
|
54
54
|
full: {}
|
|
55
55
|
},
|
|
56
|
+
// Semantic purpose. By default the Drawer paints no accent (symmetry with
|
|
57
|
+
// Dialog — intent is surfaced only as `data-intent` for consumer hooks).
|
|
58
|
+
// Each value additionally parks its colour in `--drawer-accent`, consumed
|
|
59
|
+
// *only* when `accentEdge` is on (see the accentEdge×placement compounds).
|
|
56
60
|
intent: {
|
|
57
|
-
neutral: {},
|
|
58
|
-
primary: {},
|
|
59
|
-
secondary: {},
|
|
60
|
-
success: {},
|
|
61
|
-
warning: {},
|
|
62
|
-
danger: {}
|
|
61
|
+
neutral: { panel: '[--drawer-accent:var(--color-border-strong)]' },
|
|
62
|
+
primary: { panel: '[--drawer-accent:var(--color-primary)]' },
|
|
63
|
+
secondary: { panel: '[--drawer-accent:var(--color-secondary)]' },
|
|
64
|
+
success: { panel: '[--drawer-accent:var(--color-success)]' },
|
|
65
|
+
warning: { panel: '[--drawer-accent:var(--color-warning)]' },
|
|
66
|
+
danger: { panel: '[--drawer-accent:var(--color-danger)]' }
|
|
67
|
+
},
|
|
68
|
+
// Opt-in edge accent (DRW-1 completion). Off by default so the Drawer keeps
|
|
69
|
+
// Dialog symmetry; on, the docked (viewport-facing) edge is thickened to 2px
|
|
70
|
+
// and tinted in the intent colour via the placement compounds below.
|
|
71
|
+
accentEdge: {
|
|
72
|
+
true: {},
|
|
73
|
+
false: {}
|
|
63
74
|
}
|
|
64
75
|
},
|
|
65
76
|
compoundVariants: [
|
|
@@ -84,13 +95,37 @@ export const drawerVariants = tv({
|
|
|
84
95
|
{ placement: 'bottom', size: 'md', class: { panel: 'h-72' } },
|
|
85
96
|
{ placement: 'bottom', size: 'lg', class: { panel: 'h-96' } },
|
|
86
97
|
{ placement: 'bottom', size: 'xl', class: { panel: 'h-[32rem]' } },
|
|
87
|
-
{ placement: 'bottom', size: 'full', class: { panel: 'h-full' } }
|
|
88
|
-
//
|
|
89
|
-
//
|
|
98
|
+
{ placement: 'bottom', size: 'full', class: { panel: 'h-full' } },
|
|
99
|
+
// Edge accent (opt-in via `accentEdge`). The tinted 2px border lands on the
|
|
100
|
+
// *inner* edge — the one facing the viewport — so it reads as a coloured seam
|
|
101
|
+
// between the drawer and the page, never a floating outline. Placement picks
|
|
102
|
+
// the physical side; the colour comes from `--drawer-accent` (intent axis).
|
|
103
|
+
// Default (no accentEdge) leaves the panel exactly as before — Dialog symmetry.
|
|
104
|
+
{
|
|
105
|
+
accentEdge: true,
|
|
106
|
+
placement: 'left',
|
|
107
|
+
class: { panel: 'border-r-2 [border-right-color:var(--drawer-accent)]' }
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
accentEdge: true,
|
|
111
|
+
placement: 'right',
|
|
112
|
+
class: { panel: 'border-l-2 [border-left-color:var(--drawer-accent)]' }
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
accentEdge: true,
|
|
116
|
+
placement: 'top',
|
|
117
|
+
class: { panel: 'border-b-2 [border-bottom-color:var(--drawer-accent)]' }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
accentEdge: true,
|
|
121
|
+
placement: 'bottom',
|
|
122
|
+
class: { panel: 'border-t-2 [border-top-color:var(--drawer-accent)]' }
|
|
123
|
+
}
|
|
90
124
|
],
|
|
91
125
|
defaultVariants: {
|
|
92
126
|
placement: 'right',
|
|
93
127
|
size: 'md',
|
|
94
|
-
intent: 'neutral'
|
|
128
|
+
intent: 'neutral',
|
|
129
|
+
accentEdge: false
|
|
95
130
|
}
|
|
96
131
|
});
|
|
@@ -37,13 +37,22 @@ export interface DrawerProps extends Omit<HTMLDialogAttributes, 'children' | 'op
|
|
|
37
37
|
/** Width (for left/right) or height (for top/bottom) of the drawer panel. @default 'md' */
|
|
38
38
|
size?: DrawerVariants['size'];
|
|
39
39
|
/**
|
|
40
|
-
* Semantic purpose marker (mirrors Dialog).
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Semantic purpose marker (mirrors Dialog). By default the Drawer paints no
|
|
41
|
+
* accent border — the value is exposed on the panel as `data-intent="…"` so
|
|
42
|
+
* consumers can hook presets, CSS overrides, or icon/title color via their own
|
|
43
|
+
* snippets. Set {@link accentEdge} to also tint the docked edge in this colour.
|
|
44
44
|
* @default 'neutral'
|
|
45
45
|
*/
|
|
46
46
|
intent?: DrawerVariants['intent'];
|
|
47
|
+
/**
|
|
48
|
+
* Tint the panel's docked (viewport-facing) edge with a 2px accent border in
|
|
49
|
+
* the {@link intent} colour — `border-right` for `left`, `border-left` for
|
|
50
|
+
* `right`, `border-bottom` for `top`, `border-top` for `bottom`. Off by
|
|
51
|
+
* default, keeping symmetry with Dialog; opt in for a coloured seam that ties
|
|
52
|
+
* the drawer to a semantic purpose (e.g. a `danger` confirm drawer).
|
|
53
|
+
* @default false
|
|
54
|
+
*/
|
|
55
|
+
accentEdge?: DrawerVariants['accentEdge'];
|
|
47
56
|
/** Fires when the drawer is dismissed via Escape, backdrop click, or close button. */
|
|
48
57
|
onClose?: () => void;
|
|
49
58
|
/** Hides the built-in close button in the header. @default false */
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
let {
|
|
6
6
|
label,
|
|
7
|
-
|
|
7
|
+
helper,
|
|
8
8
|
error,
|
|
9
9
|
required = false,
|
|
10
10
|
disabled = false,
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
// `useFormField` / XC-2 for the contract.
|
|
21
21
|
const ff = useFormField(() => ({
|
|
22
22
|
fieldId: idProp ?? `field-${propsId}`,
|
|
23
|
-
|
|
23
|
+
helper,
|
|
24
24
|
error,
|
|
25
25
|
required,
|
|
26
26
|
disabled
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
>
|
|
60
60
|
{error}
|
|
61
61
|
</div>
|
|
62
|
-
{:else if ff.
|
|
62
|
+
{:else if ff.helperId}
|
|
63
63
|
<div
|
|
64
|
-
id={ff.
|
|
65
|
-
class={['text-text-tertiary text-xs', slotClasses.
|
|
64
|
+
id={ff.helperId}
|
|
65
|
+
class={['text-text-tertiary text-xs', slotClasses.helper].filter(Boolean).join(' ')}
|
|
66
66
|
>
|
|
67
|
-
{
|
|
67
|
+
{helper}
|
|
68
68
|
</div>
|
|
69
69
|
{/if}
|
|
70
70
|
</div>
|
|
@@ -17,7 +17,7 @@ import type { HTMLAttributes } from 'svelte/elements';
|
|
|
17
17
|
*
|
|
18
18
|
* @example
|
|
19
19
|
* ```svelte
|
|
20
|
-
* <FormField label="Document" required error={fileError}
|
|
20
|
+
* <FormField label="Document" required error={fileError} helper="PDF, JPG, PNG — max 10 MB">
|
|
21
21
|
* {#snippet children({ id, describedBy, invalid })}
|
|
22
22
|
* <FileUpload {id} aria-describedby={describedBy} aria-invalid={invalid} bind:files />
|
|
23
23
|
* {/snippet}
|
|
@@ -27,8 +27,12 @@ import type { HTMLAttributes } from 'svelte/elements';
|
|
|
27
27
|
export interface FormFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
28
28
|
/** Label rendered above the control. Auto-linked to the slot via the generated `id`. */
|
|
29
29
|
label?: string;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Helper text shown below the control. Hidden when `error` is present.
|
|
32
|
+
* Named `helper` to match the built-in helper prop of the form primitives
|
|
33
|
+
* (Input, Select, Toggle, …) — one vocabulary across the API seam.
|
|
34
|
+
*/
|
|
35
|
+
helper?: string;
|
|
32
36
|
/**
|
|
33
37
|
* Error message shown below the control. Replaces the helper text and
|
|
34
38
|
* propagates `invalid: true` to the slot for ARIA wiring.
|
|
@@ -54,8 +58,13 @@ export interface FormFieldProps extends Omit<HTMLAttributes<HTMLDivElement>, 'ch
|
|
|
54
58
|
class?: string;
|
|
55
59
|
/**
|
|
56
60
|
* Per-slot class overrides.
|
|
61
|
+
*
|
|
62
|
+
* FormField has no `tv()` config (it is a bare layout wrapper), so this
|
|
63
|
+
* key union is a hand-maintained literal instead of the usual
|
|
64
|
+
* `SlotNames<typeof xVariants>` derivation — keep it in sync with the
|
|
65
|
+
* elements rendered in `FormField.svelte`.
|
|
57
66
|
*/
|
|
58
|
-
slotClasses?: Partial<Record<'wrapper' | 'label' | 'message' | '
|
|
67
|
+
slotClasses?: Partial<Record<'wrapper' | 'label' | 'message' | 'helper', string>>;
|
|
59
68
|
/**
|
|
60
69
|
* Snippet receiving wiring metadata (`id`, `describedBy`, `invalid`,
|
|
61
70
|
* `required`, `disabled`). The wrapped control should spread or apply
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
persistDebounceMs = 300,
|
|
43
43
|
persistVersion = 1,
|
|
44
44
|
persistNamespace,
|
|
45
|
+
onkeydown: userOnKeydown,
|
|
45
46
|
...restProps
|
|
46
47
|
}: InputProps = $props();
|
|
47
48
|
|
|
@@ -119,7 +120,7 @@
|
|
|
119
120
|
const propsId = $props.id();
|
|
120
121
|
const ff = useFormField(() => ({
|
|
121
122
|
fieldId: `input-${propsId}`,
|
|
122
|
-
|
|
123
|
+
helper,
|
|
123
124
|
error,
|
|
124
125
|
required,
|
|
125
126
|
disabled
|
|
@@ -147,11 +148,14 @@
|
|
|
147
148
|
if (!disabled && onRightIconClick) onRightIconClick();
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
function handleKeydown(event: KeyboardEvent) {
|
|
151
|
+
function handleKeydown(event: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) {
|
|
151
152
|
if (event.key === 'Escape' && shouldShowClear) {
|
|
152
153
|
event.preventDefault();
|
|
153
154
|
handleClear();
|
|
154
155
|
}
|
|
156
|
+
// Forward the consumer's onkeydown (NumberInput's Arrow-step, etc.) — Input's
|
|
157
|
+
// own handler is hardcoded on the element, so without this it would swallow it.
|
|
158
|
+
userOnKeydown?.(event);
|
|
155
159
|
}
|
|
156
160
|
|
|
157
161
|
$effect(() => {
|
|
@@ -280,9 +284,9 @@
|
|
|
280
284
|
>
|
|
281
285
|
{error}
|
|
282
286
|
</div>
|
|
283
|
-
{:else if ff.
|
|
287
|
+
{:else if ff.helperId}
|
|
284
288
|
<div
|
|
285
|
-
id={ff.
|
|
289
|
+
id={ff.helperId}
|
|
286
290
|
class={unstyled
|
|
287
291
|
? (slotClasses?.message ?? '')
|
|
288
292
|
: styles.message({ class: slotClasses?.message })}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts" generics="TItem extends MenuItemType = MenuItemType">
|
|
2
|
+
import { tick } from 'svelte';
|
|
2
3
|
import { useBlocksI18n } from '../..';
|
|
3
4
|
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
4
5
|
import { Button, menuVariants, type MenuVariants } from '..';
|
|
@@ -27,11 +28,13 @@
|
|
|
27
28
|
disabled = false,
|
|
28
29
|
loading = false,
|
|
29
30
|
open = $bindable(false),
|
|
31
|
+
onOpenChange,
|
|
30
32
|
id: idProp,
|
|
31
33
|
placement = 'bottom-start',
|
|
32
34
|
syncWidth = true,
|
|
33
35
|
usePortal = true,
|
|
34
36
|
customTrigger,
|
|
37
|
+
contextTrigger,
|
|
35
38
|
customItem,
|
|
36
39
|
customHeader,
|
|
37
40
|
customFooter,
|
|
@@ -71,6 +74,31 @@
|
|
|
71
74
|
let openSubMenus = $state<Set<string>>(new Set());
|
|
72
75
|
const childrenMode = $derived(!!children);
|
|
73
76
|
|
|
77
|
+
// ── Context-menu (right-click) anchoring ───────────────────────────────
|
|
78
|
+
// A context menu has no trigger button — it opens at the cursor. Floating UI
|
|
79
|
+
// anchors to an element, not a point, so a 0×0 fixed-position element is
|
|
80
|
+
// parked at the click coordinates and handed to Popover as the trigger.
|
|
81
|
+
let cursorAnchor = $state<HTMLElement>();
|
|
82
|
+
let contextX = $state(0);
|
|
83
|
+
let contextY = $state(0);
|
|
84
|
+
|
|
85
|
+
async function handleContextMenu(event: MouseEvent) {
|
|
86
|
+
if (disabled || loading) return;
|
|
87
|
+
// Suppress the native browser menu and anchor ours at the cursor.
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
contextX = event.clientX;
|
|
90
|
+
contextY = event.clientY;
|
|
91
|
+
// Re-open at the new spot even if it was already open elsewhere: close and
|
|
92
|
+
// let Popover tear down (await tick) before reopening, so it re-reads the
|
|
93
|
+
// moved anchor rect. A synchronous setOpen(false)+setOpen(true) batches into
|
|
94
|
+
// no net change and would strand the menu at the previous cursor position.
|
|
95
|
+
if (open) {
|
|
96
|
+
setOpen(false);
|
|
97
|
+
await tick();
|
|
98
|
+
}
|
|
99
|
+
setOpen(true);
|
|
100
|
+
}
|
|
101
|
+
|
|
74
102
|
// Map of declarative MenuItems by id — populated via the context's
|
|
75
103
|
// `registerItem` / `unregisterItem` hooks. Used to debug + (in future)
|
|
76
104
|
// power type-ahead search; the keyboard model itself walks DOM-focusable
|
|
@@ -127,13 +155,25 @@
|
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
// ── Open / close lifecycle ─────────────────────────────────────────────
|
|
158
|
+
// Single mutation point for internally-driven open changes, so
|
|
159
|
+
// `onOpenChange` fires exactly once per transition. Popover-owned dismiss
|
|
160
|
+
// paths (outside click) mutate `open` via `bind:open` instead and report
|
|
161
|
+
// through the forwarded Popover `onOpenChange` — the Escape path can't
|
|
162
|
+
// double-fire because `handlePanelKeydown` calls `preventDefault()`,
|
|
163
|
+
// which Popover's document-level Escape listener honors.
|
|
164
|
+
function setOpen(next: boolean) {
|
|
165
|
+
if (open === next) return;
|
|
166
|
+
open = next;
|
|
167
|
+
onOpenChange?.(next);
|
|
168
|
+
}
|
|
169
|
+
|
|
130
170
|
function toggle() {
|
|
131
171
|
if (disabled || loading) return;
|
|
132
|
-
|
|
172
|
+
setOpen(!open);
|
|
133
173
|
}
|
|
134
174
|
|
|
135
175
|
function dismiss() {
|
|
136
|
-
|
|
176
|
+
setOpen(false);
|
|
137
177
|
triggerRef?.focus();
|
|
138
178
|
}
|
|
139
179
|
|
|
@@ -363,6 +403,26 @@
|
|
|
363
403
|
.join(' ')}
|
|
364
404
|
{...restProps}
|
|
365
405
|
>
|
|
406
|
+
{#if contextTrigger}
|
|
407
|
+
<!-- Right-click target. `display: contents` drops the wrapper from layout so
|
|
408
|
+
the consumer's own element controls sizing; the contextmenu event still
|
|
409
|
+
bubbles to the handler. -->
|
|
410
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
411
|
+
<div class="contents" oncontextmenu={handleContextMenu}>
|
|
412
|
+
{@render contextTrigger()}
|
|
413
|
+
</div>
|
|
414
|
+
<!-- 0×0 anchor parked at the cursor for Popover to position the menu against. -->
|
|
415
|
+
<div
|
|
416
|
+
bind:this={cursorAnchor}
|
|
417
|
+
aria-hidden="true"
|
|
418
|
+
style:position="fixed"
|
|
419
|
+
style:left="{contextX}px"
|
|
420
|
+
style:top="{contextY}px"
|
|
421
|
+
style:width="0"
|
|
422
|
+
style:height="0"
|
|
423
|
+
></div>
|
|
424
|
+
{/if}
|
|
425
|
+
|
|
366
426
|
{#snippet triggerContent()}
|
|
367
427
|
{#if customTrigger}
|
|
368
428
|
{@render customTrigger(toggle, open, dismiss)}
|
|
@@ -414,13 +474,15 @@
|
|
|
414
474
|
-->
|
|
415
475
|
<Popover
|
|
416
476
|
bind:open
|
|
477
|
+
onOpenChange={(o) => onOpenChange?.(o)}
|
|
417
478
|
placement={placement as import('../../utils/floating').Placement}
|
|
418
479
|
{usePortal}
|
|
419
480
|
autoTrigger={false}
|
|
420
481
|
unstyled
|
|
421
|
-
syncMinWidth={syncWidth}
|
|
482
|
+
syncMinWidth={contextTrigger ? false : syncWidth}
|
|
422
483
|
offsetDistance={effectiveTier === 'commit' ? 8 : 4}
|
|
423
|
-
trigger={triggerContent}
|
|
484
|
+
trigger={contextTrigger ? undefined : triggerContent}
|
|
485
|
+
triggerElement={contextTrigger ? cursorAnchor : undefined}
|
|
424
486
|
>
|
|
425
487
|
<div
|
|
426
488
|
bind:this={panelRef}
|
|
@@ -119,6 +119,12 @@ export interface MenuSpecificProps<TItem extends MenuItemType = MenuItemType> {
|
|
|
119
119
|
* @default false
|
|
120
120
|
*/
|
|
121
121
|
open?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Fires when the menu opens or closes from user interaction (trigger
|
|
124
|
+
* click, item activation, Escape, Tab-out, outside click). Receives the
|
|
125
|
+
* new open state. Not called when the consumer writes `bind:open` directly.
|
|
126
|
+
*/
|
|
127
|
+
onOpenChange?: (open: boolean) => void;
|
|
122
128
|
/**
|
|
123
129
|
* Button variant applied to the default trigger button.
|
|
124
130
|
* @default 'outlined'
|
|
@@ -252,6 +258,25 @@ export interface MenuCustomSlots<TItem extends MenuItemType = MenuItemType> {
|
|
|
252
258
|
* `aria-expanded={open}` + `aria-haspopup="menu"` for ARIA correctness.
|
|
253
259
|
*/
|
|
254
260
|
customTrigger?: Snippet<[() => void, boolean, () => void]>;
|
|
261
|
+
/**
|
|
262
|
+
* Turn the menu into a **context menu**: instead of a trigger button, the
|
|
263
|
+
* snippet you pass becomes a right-click target. A `contextmenu` (right-click
|
|
264
|
+
* or long-press) on it opens the menu at the cursor position — the native
|
|
265
|
+
* browser context menu is suppressed. Keyboard navigation, dismissal and
|
|
266
|
+
* item selection behave exactly as in the dropdown menu; on dismiss, focus
|
|
267
|
+
* returns to wherever it was. Mutually exclusive with `customTrigger`/the
|
|
268
|
+
* default trigger button (when set, no trigger button renders).
|
|
269
|
+
*
|
|
270
|
+
* @example
|
|
271
|
+
* ```svelte
|
|
272
|
+
* <Menu {items} contextTrigger>
|
|
273
|
+
* {#snippet contextTrigger()}
|
|
274
|
+
* <div class="rounded-modify border border-border-subtle p-8">Right-click me</div>
|
|
275
|
+
* {/snippet}
|
|
276
|
+
* </Menu>
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
contextTrigger?: Snippet;
|
|
255
280
|
/**
|
|
256
281
|
* Custom per-item content. **Render visible content only** — the outer
|
|
257
282
|
* `role="menuitem"` button is provided by Menu and handles the click /
|