@urbicon-ui/blocks 6.21.1 → 6.21.3
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/Avatar/avatar.variants.d.ts +6 -6
- package/dist/primitives/Badge/Badge.svelte +18 -8
- 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/Button/button.variants.d.ts +4 -4
- package/dist/primitives/ButtonGroup/index.d.ts +1 -1
- package/dist/primitives/Card/card.variants.d.ts +5 -5
- 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/dialog.variants.d.ts +9 -9
- 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/Input/input.variants.d.ts +9 -9
- 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 +34 -3
- 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/Skeleton/skeleton.variants.d.ts +3 -3
- package/dist/primitives/Slider/Slider.svelte +4 -8
- package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
- package/dist/primitives/Textarea/Textarea.svelte +13 -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/Toolbar/toolbar.variants.d.ts +6 -6
- 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/style/semantic.css +55 -29
- 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/figma-token-export.d.ts +13 -0
- package/dist/utils/figma-token-export.js +99 -26
- 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
|
@@ -2,7 +2,7 @@ import { type SlotNames, type VariantProps } from '../../utils/variants.js';
|
|
|
2
2
|
export declare const inputVariants: ((props?: {
|
|
3
3
|
tier?: "commit" | "modify" | undefined;
|
|
4
4
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
5
|
-
size?: "sm" | "md" | "lg" | "
|
|
5
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
6
6
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
7
7
|
disabled?: boolean | undefined;
|
|
8
8
|
readonly?: boolean | undefined;
|
|
@@ -16,7 +16,7 @@ export declare const inputVariants: ((props?: {
|
|
|
16
16
|
wrapper: (props?: ({
|
|
17
17
|
tier?: "commit" | "modify" | undefined;
|
|
18
18
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
19
|
-
size?: "sm" | "md" | "lg" | "
|
|
19
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
20
20
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
21
21
|
disabled?: boolean | undefined;
|
|
22
22
|
readonly?: boolean | undefined;
|
|
@@ -32,7 +32,7 @@ export declare const inputVariants: ((props?: {
|
|
|
32
32
|
container: (props?: ({
|
|
33
33
|
tier?: "commit" | "modify" | undefined;
|
|
34
34
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
35
|
-
size?: "sm" | "md" | "lg" | "
|
|
35
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
36
36
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
37
37
|
disabled?: boolean | undefined;
|
|
38
38
|
readonly?: boolean | undefined;
|
|
@@ -48,7 +48,7 @@ export declare const inputVariants: ((props?: {
|
|
|
48
48
|
base: (props?: ({
|
|
49
49
|
tier?: "commit" | "modify" | undefined;
|
|
50
50
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
51
|
-
size?: "sm" | "md" | "lg" | "
|
|
51
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
52
52
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
53
53
|
disabled?: boolean | undefined;
|
|
54
54
|
readonly?: boolean | undefined;
|
|
@@ -64,7 +64,7 @@ export declare const inputVariants: ((props?: {
|
|
|
64
64
|
label: (props?: ({
|
|
65
65
|
tier?: "commit" | "modify" | undefined;
|
|
66
66
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
67
|
-
size?: "sm" | "md" | "lg" | "
|
|
67
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
68
68
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
69
69
|
disabled?: boolean | undefined;
|
|
70
70
|
readonly?: boolean | undefined;
|
|
@@ -80,7 +80,7 @@ export declare const inputVariants: ((props?: {
|
|
|
80
80
|
message: (props?: ({
|
|
81
81
|
tier?: "commit" | "modify" | undefined;
|
|
82
82
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
83
|
-
size?: "sm" | "md" | "lg" | "
|
|
83
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
84
84
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
85
85
|
disabled?: boolean | undefined;
|
|
86
86
|
readonly?: boolean | undefined;
|
|
@@ -96,7 +96,7 @@ export declare const inputVariants: ((props?: {
|
|
|
96
96
|
iconContainer: (props?: ({
|
|
97
97
|
tier?: "commit" | "modify" | undefined;
|
|
98
98
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
99
|
-
size?: "sm" | "md" | "lg" | "
|
|
99
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
100
100
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
101
101
|
disabled?: boolean | undefined;
|
|
102
102
|
readonly?: boolean | undefined;
|
|
@@ -112,7 +112,7 @@ export declare const inputVariants: ((props?: {
|
|
|
112
112
|
iconButton: (props?: ({
|
|
113
113
|
tier?: "commit" | "modify" | undefined;
|
|
114
114
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
115
|
-
size?: "sm" | "md" | "lg" | "
|
|
115
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
116
116
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
117
117
|
disabled?: boolean | undefined;
|
|
118
118
|
readonly?: boolean | undefined;
|
|
@@ -128,7 +128,7 @@ export declare const inputVariants: ((props?: {
|
|
|
128
128
|
iconDecoration: (props?: ({
|
|
129
129
|
tier?: "commit" | "modify" | undefined;
|
|
130
130
|
variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
|
|
131
|
-
size?: "sm" | "md" | "lg" | "
|
|
131
|
+
size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
|
|
132
132
|
intent?: "default" | "success" | "warning" | "danger" | undefined;
|
|
133
133
|
disabled?: boolean | undefined;
|
|
134
134
|
readonly?: boolean | undefined;
|
|
@@ -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 /
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
nextIcon,
|
|
35
35
|
firstIcon,
|
|
36
36
|
lastIcon,
|
|
37
|
+
renderItem,
|
|
37
38
|
itemsPerPage = 10,
|
|
38
39
|
totalItems,
|
|
39
40
|
startItem,
|
|
@@ -323,19 +324,33 @@
|
|
|
323
324
|
|
|
324
325
|
{#if showNumbers}
|
|
325
326
|
{#each visiblePageNumbers as page (page)}
|
|
326
|
-
|
|
327
|
-
{
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
327
|
+
{#if renderItem}
|
|
328
|
+
{@render renderItem({
|
|
329
|
+
page,
|
|
330
|
+
active: page === currentPage,
|
|
331
|
+
disabled: disabled || loading,
|
|
332
|
+
size,
|
|
333
|
+
variant,
|
|
334
|
+
intent,
|
|
335
|
+
tier,
|
|
336
|
+
mint,
|
|
337
|
+
select: goToPage(page)
|
|
338
|
+
})}
|
|
339
|
+
{:else}
|
|
340
|
+
<PaginationItem
|
|
341
|
+
{size}
|
|
342
|
+
{variant}
|
|
343
|
+
{intent}
|
|
344
|
+
{tier}
|
|
345
|
+
{page}
|
|
346
|
+
active={page === currentPage}
|
|
347
|
+
disabled={disabled || loading}
|
|
348
|
+
onPageClick={goToPage(page)}
|
|
349
|
+
{mint}
|
|
350
|
+
>
|
|
351
|
+
{page}
|
|
352
|
+
</PaginationItem>
|
|
353
|
+
{/if}
|
|
339
354
|
{/each}
|
|
340
355
|
{/if}
|
|
341
356
|
|
|
@@ -10,6 +10,33 @@ export interface PaginationPageItem {
|
|
|
10
10
|
active?: boolean;
|
|
11
11
|
disabled?: boolean;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Context handed to the `renderItem` snippet for a single numbered page button.
|
|
15
|
+
* Bundles the page number, its active/disabled state, the style props forwarded
|
|
16
|
+
* from the Pagination (so a custom item stays visually consistent), and a
|
|
17
|
+
* `select` callback that changes the page (guarded against disabled / no-op /
|
|
18
|
+
* out-of-range internally).
|
|
19
|
+
*/
|
|
20
|
+
export interface PaginationItemContext {
|
|
21
|
+
/** The 1-based page number this item represents. */
|
|
22
|
+
page: number;
|
|
23
|
+
/** Whether this item is the currently active page. */
|
|
24
|
+
active: boolean;
|
|
25
|
+
/** Whether the item is inert (component `disabled` or `loading`). */
|
|
26
|
+
disabled: boolean;
|
|
27
|
+
/** Button size forwarded from the Pagination props. */
|
|
28
|
+
size: 'sm' | 'md' | 'lg';
|
|
29
|
+
/** Button variant forwarded from the Pagination props. */
|
|
30
|
+
variant: 'outlined' | 'filled' | 'ghost';
|
|
31
|
+
/** Semantic intent forwarded from the Pagination props. */
|
|
32
|
+
intent: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'neutral';
|
|
33
|
+
/** Semantic radius tier forwarded from the Pagination props. */
|
|
34
|
+
tier?: InteractiveTier;
|
|
35
|
+
/** Micro-interaction preset forwarded from the Pagination props. */
|
|
36
|
+
mint: MintProp;
|
|
37
|
+
/** Navigate to this page. No-op when disabled, already active, or out of range. */
|
|
38
|
+
select: () => void;
|
|
39
|
+
}
|
|
13
40
|
/**
|
|
14
41
|
* @description Navigation control for paged data sets.
|
|
15
42
|
* Supports multiple layouts, intents, button variants, and configurable ellipsis behaviour.
|
|
@@ -69,6 +96,23 @@ export interface PaginationProps extends Omit<PaginationVariants, 'disabled' | '
|
|
|
69
96
|
firstIcon?: Snippet;
|
|
70
97
|
/** Custom icon rendered inside the "Last" button. */
|
|
71
98
|
lastIcon?: Snippet;
|
|
99
|
+
/**
|
|
100
|
+
* Render each numbered page button yourself. Receives a {@link PaginationItemContext}
|
|
101
|
+
* with the page number, its active/disabled state, the forwarded style props
|
|
102
|
+
* (size, variant, intent, tier, mint) and a `select` callback. Only affects the
|
|
103
|
+
* numbered page buttons in the default layout — prev/next/first/last keep their
|
|
104
|
+
* own icon snippets, and the ellipsis is unaffected.
|
|
105
|
+
*
|
|
106
|
+
* @example
|
|
107
|
+
* ```svelte
|
|
108
|
+
* <Pagination {currentPage} {totalPages} {onPageChange}>
|
|
109
|
+
* {#snippet renderItem({ page, active, disabled, select })}
|
|
110
|
+
* <button class:active onclick={select} {disabled}>{page}</button>
|
|
111
|
+
* {/snippet}
|
|
112
|
+
* </Pagination>
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
renderItem?: Snippet<[PaginationItemContext]>;
|
|
72
116
|
/** Items shown per page. Used by the table layout to compute "Showing X to Y of Z". */
|
|
73
117
|
itemsPerPage?: number;
|
|
74
118
|
/** Total number of items across all pages. Used by the table layout info text. */
|
|
@@ -184,7 +184,7 @@
|
|
|
184
184
|
}
|
|
185
185
|
|
|
186
186
|
if (!wasTrigger) {
|
|
187
|
-
|
|
187
|
+
focusTrigger();
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
}
|
|
@@ -209,11 +209,17 @@
|
|
|
209
209
|
// intentionally consume Escape.
|
|
210
210
|
function handleEscape(e: KeyboardEvent) {
|
|
211
211
|
if (e.key !== 'Escape' || e.defaultPrevented) return;
|
|
212
|
+
// Re-check `open` (like the auto-mode toggle handler does): the
|
|
213
|
+
// listener teardown is deferred to the next effect flush, so a
|
|
214
|
+
// consumer handler earlier in this same dispatch may already have
|
|
215
|
+
// closed via `bind:open` — without this guard we'd report a second,
|
|
216
|
+
// transition-less onOpenChange(false) + onEscape and steal focus.
|
|
217
|
+
if (!open) return;
|
|
212
218
|
e.preventDefault();
|
|
213
219
|
open = false;
|
|
214
220
|
onOpenChange?.(false);
|
|
215
221
|
onEscapeProp?.();
|
|
216
|
-
|
|
222
|
+
focusTrigger();
|
|
217
223
|
}
|
|
218
224
|
document.addEventListener('keydown', handleEscape);
|
|
219
225
|
return () => document.removeEventListener('keydown', handleEscape);
|
|
@@ -231,6 +237,10 @@
|
|
|
231
237
|
if (!target) return;
|
|
232
238
|
if (popoverElement?.contains(target)) return;
|
|
233
239
|
if (effectiveTriggerElement?.contains(target)) return;
|
|
240
|
+
// Same deferred-teardown re-check as handleEscape above: a consumer
|
|
241
|
+
// capture-phase pointerdown handler may have closed via `bind:open`
|
|
242
|
+
// within this dispatch — don't report a second close.
|
|
243
|
+
if (!open) return;
|
|
234
244
|
open = false;
|
|
235
245
|
onOpenChange?.(false);
|
|
236
246
|
onClickOutsideProp?.();
|
|
@@ -266,8 +276,29 @@
|
|
|
266
276
|
|
|
267
277
|
// ── Trigger handlers ───────────────────────────────────────
|
|
268
278
|
|
|
279
|
+
// Restore focus to the trigger after a dismiss. The snippet trigger is
|
|
280
|
+
// wrapped in a plain (non-focusable) div — `focus()` on it is a spec no-op —
|
|
281
|
+
// so target the interactive descendant instead (same query as the
|
|
282
|
+
// aria-expanded effect). An external `triggerElement` is the consumer's
|
|
283
|
+
// real control: focus it directly.
|
|
284
|
+
function focusTrigger() {
|
|
285
|
+
const target =
|
|
286
|
+
triggerElement ??
|
|
287
|
+
internalTriggerElement?.querySelector<HTMLElement>(
|
|
288
|
+
'button, a[href], [role="button"], [tabindex]'
|
|
289
|
+
) ??
|
|
290
|
+
internalTriggerElement;
|
|
291
|
+
target?.focus();
|
|
292
|
+
}
|
|
293
|
+
|
|
269
294
|
function handleTriggerPointerDown() {
|
|
270
|
-
|
|
295
|
+
// Arm the "this pointerdown already dismissed it" guard only in auto
|
|
296
|
+
// mode, where the browser's light dismiss really closes the popover
|
|
297
|
+
// between pointerdown and click (the guard stops that click from
|
|
298
|
+
// re-opening it). In manual mode nothing light-dismisses — the click
|
|
299
|
+
// itself must toggle-close, so arming the guard there left the trigger
|
|
300
|
+
// unable to close its own popover.
|
|
301
|
+
if (open && popoverMode === 'auto') dismissedByTrigger = true;
|
|
271
302
|
}
|
|
272
303
|
|
|
273
304
|
function handleTriggerClick(event: MouseEvent) {
|
|
@@ -4,8 +4,8 @@ import type { Placement } from '../../utils/floating.js';
|
|
|
4
4
|
import type { PopoverVariants } from './popover.variants.js';
|
|
5
5
|
/**
|
|
6
6
|
* @description Floating panel anchored to a trigger element. Uses the native Popover API
|
|
7
|
-
* for top-layer rendering, light dismiss, and Escape handling.
|
|
8
|
-
*
|
|
7
|
+
* for top-layer rendering, light dismiss, and Escape handling. The library's built-in
|
|
8
|
+
* positioning engine provides automatic flip, shift, and optional width syncing.
|
|
9
9
|
*
|
|
10
10
|
* @tag overlay
|
|
11
11
|
* @related Tooltip
|
|
@@ -45,7 +45,7 @@ export interface PopoverProps extends PopoverVariants, Omit<HTMLAttributes<HTMLD
|
|
|
45
45
|
trigger?: Snippet;
|
|
46
46
|
/** External trigger element ref. Use instead of the `trigger` snippet when the trigger lives outside the Popover tree. Supports `bind:triggerElement`. */
|
|
47
47
|
triggerElement?: HTMLElement;
|
|
48
|
-
/** Where the popover appears relative to the trigger. All
|
|
48
|
+
/** Where the popover appears relative to the trigger. All standard `Placement` values (side plus optional `-start`/`-end` alignment) are supported. */
|
|
49
49
|
placement?: Placement;
|
|
50
50
|
/** Gap in px between the trigger edge and the popover. */
|
|
51
51
|
offsetDistance?: number;
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
// ARIA wiring is shared with every form primitive — see XC-2.
|
|
47
47
|
const ff = useFormField(() => ({
|
|
48
48
|
fieldId: groupId,
|
|
49
|
-
|
|
49
|
+
helper,
|
|
50
50
|
error,
|
|
51
51
|
required,
|
|
52
52
|
disabled
|
|
@@ -174,9 +174,9 @@
|
|
|
174
174
|
>
|
|
175
175
|
{error}
|
|
176
176
|
</div>
|
|
177
|
-
{:else if ff.
|
|
177
|
+
{:else if ff.helperId}
|
|
178
178
|
<div
|
|
179
|
-
id={ff.
|
|
179
|
+
id={ff.helperId}
|
|
180
180
|
class={unstyled
|
|
181
181
|
? (slotClasses?.message ?? '')
|
|
182
182
|
: styles.message({ class: slotClasses?.message })}
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
mint = 'none',
|
|
39
39
|
onValueChange,
|
|
40
40
|
open = $bindable(false),
|
|
41
|
+
onOpenChange,
|
|
41
42
|
usePortal = true,
|
|
42
43
|
syncWidth = true,
|
|
43
44
|
customTrigger,
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
const labelId = $derived(label ? `${uid}-label` : undefined);
|
|
102
103
|
const ff = useFormField(() => ({
|
|
103
104
|
fieldId: uid,
|
|
104
|
-
|
|
105
|
+
helper,
|
|
105
106
|
error,
|
|
106
107
|
required,
|
|
107
108
|
disabled
|
|
@@ -235,12 +236,21 @@
|
|
|
235
236
|
syncWidth: () => syncWidth
|
|
236
237
|
});
|
|
237
238
|
|
|
239
|
+
// Single mutation point for internally-driven open changes, so
|
|
240
|
+
// `onOpenChange` fires exactly once per transition (and never when the
|
|
241
|
+
// consumer writes `bind:open` directly).
|
|
242
|
+
function setOpen(next: boolean) {
|
|
243
|
+
if (open === next) return;
|
|
244
|
+
open = next;
|
|
245
|
+
onOpenChange?.(next);
|
|
246
|
+
}
|
|
247
|
+
|
|
238
248
|
function toggle() {
|
|
239
249
|
if (disabled) return;
|
|
240
250
|
// Pointer-driven open (trigger onclick). Mark modality so the open effect
|
|
241
251
|
// doesn't pre-highlight the first row for a mouse/touch user.
|
|
242
252
|
if (!open) openedViaKeyboard = false;
|
|
243
|
-
|
|
253
|
+
setOpen(!open);
|
|
244
254
|
if (!open) activeIndex = -1;
|
|
245
255
|
}
|
|
246
256
|
|
|
@@ -271,7 +281,7 @@
|
|
|
271
281
|
dispatchValueChange?.(nextValue);
|
|
272
282
|
}
|
|
273
283
|
if (effectiveCloseOnSelect) {
|
|
274
|
-
|
|
284
|
+
setOpen(false);
|
|
275
285
|
activeIndex = -1;
|
|
276
286
|
focusTrigger();
|
|
277
287
|
}
|
|
@@ -297,7 +307,7 @@
|
|
|
297
307
|
// explicitly intent to focus elsewhere)
|
|
298
308
|
function dismissByEscape() {
|
|
299
309
|
if (!closeOnEscape) return false;
|
|
300
|
-
|
|
310
|
+
setOpen(false);
|
|
301
311
|
activeIndex = -1;
|
|
302
312
|
onEscape?.();
|
|
303
313
|
return true;
|
|
@@ -319,7 +329,7 @@
|
|
|
319
329
|
event.preventDefault();
|
|
320
330
|
if (!open) {
|
|
321
331
|
openedViaKeyboard = true;
|
|
322
|
-
|
|
332
|
+
setOpen(true);
|
|
323
333
|
} else {
|
|
324
334
|
activeIndex = activeIndex < enabledOptions.length - 1 ? activeIndex + 1 : 0;
|
|
325
335
|
}
|
|
@@ -328,7 +338,7 @@
|
|
|
328
338
|
event.preventDefault();
|
|
329
339
|
if (!open) {
|
|
330
340
|
openedViaKeyboard = true;
|
|
331
|
-
|
|
341
|
+
setOpen(true);
|
|
332
342
|
} else {
|
|
333
343
|
activeIndex = activeIndex > 0 ? activeIndex - 1 : enabledOptions.length - 1;
|
|
334
344
|
}
|
|
@@ -338,7 +348,7 @@
|
|
|
338
348
|
event.preventDefault();
|
|
339
349
|
if (!open) {
|
|
340
350
|
openedViaKeyboard = true;
|
|
341
|
-
|
|
351
|
+
setOpen(true);
|
|
342
352
|
} else if (activeIndex >= 0 && activeIndex < enabledOptions.length) {
|
|
343
353
|
selectOption(enabledOptions[activeIndex]);
|
|
344
354
|
}
|
|
@@ -353,7 +363,7 @@
|
|
|
353
363
|
// Tab leaves the widget — close (focus moves on via the default tab),
|
|
354
364
|
// independent of closeOnEscape/closeOnClickOutside.
|
|
355
365
|
if (open) {
|
|
356
|
-
|
|
366
|
+
setOpen(false);
|
|
357
367
|
activeIndex = -1;
|
|
358
368
|
}
|
|
359
369
|
break;
|
|
@@ -382,7 +392,7 @@
|
|
|
382
392
|
!listboxRef.contains(target)
|
|
383
393
|
) {
|
|
384
394
|
if (!closeOnClickOutside) return;
|
|
385
|
-
|
|
395
|
+
setOpen(false);
|
|
386
396
|
activeIndex = -1;
|
|
387
397
|
onClickOutside?.();
|
|
388
398
|
}
|
|
@@ -725,9 +735,9 @@
|
|
|
725
735
|
>
|
|
726
736
|
{error}
|
|
727
737
|
</div>
|
|
728
|
-
{:else if ff.
|
|
738
|
+
{:else if ff.helperId}
|
|
729
739
|
<div
|
|
730
|
-
id={ff.
|
|
740
|
+
id={ff.helperId}
|
|
731
741
|
class={unstyled
|
|
732
742
|
? (slotClasses?.message ?? '')
|
|
733
743
|
: styles.message({ class: slotClasses?.message })}
|
|
@@ -164,6 +164,13 @@ interface SelectBaseProps<T extends SelectValue = string> extends Omit<SelectVar
|
|
|
164
164
|
* the Select wrapper). @default false
|
|
165
165
|
*/
|
|
166
166
|
open?: boolean;
|
|
167
|
+
/**
|
|
168
|
+
* Fires when the listbox opens or closes from user interaction (trigger
|
|
169
|
+
* click, keyboard, selection, Escape, Tab-out, outside click). Receives the
|
|
170
|
+
* new open state — use it e.g. to lazy-load options on first open. Not
|
|
171
|
+
* called when the consumer writes `bind:open` directly.
|
|
172
|
+
*/
|
|
173
|
+
onOpenChange?: (open: boolean) => void;
|
|
167
174
|
/**
|
|
168
175
|
* When true, the listbox is rendered into the browser top layer via the native
|
|
169
176
|
* `popover` API, so it cannot be clipped by `overflow: auto` ancestors. When
|