@urbicon-ui/blocks 6.3.7 → 6.3.9
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/primitives/Breadcrumb/Breadcrumb.svelte +2 -1
- package/dist/primitives/Breadcrumb/breadcrumb.variants.d.ts +17 -0
- package/dist/primitives/Breadcrumb/breadcrumb.variants.js +22 -2
- package/dist/primitives/Breadcrumb/index.d.ts +7 -0
- package/dist/primitives/Combobox/Combobox.svelte +6 -3
- package/dist/primitives/Menu/menu.variants.js +6 -1
- package/dist/primitives/Popover/Popover.svelte +23 -100
- package/dist/primitives/Popover/popover.variants.js +7 -1
- package/dist/primitives/Select/Select.svelte +2 -2
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/use-floating-panel.svelte.d.ts +44 -0
- package/dist/utils/use-floating-panel.svelte.js +109 -0
- package/package.json +3 -3
- package/dist/utils/use-floating-listbox.svelte.d.ts +0 -17
- package/dist/utils/use-floating-listbox.svelte.js +0 -87
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
let {
|
|
8
8
|
items,
|
|
9
9
|
size = 'md',
|
|
10
|
+
wrap = true,
|
|
10
11
|
separator,
|
|
11
12
|
maxItems,
|
|
12
13
|
itemsBeforeCollapse = 1,
|
|
@@ -23,7 +24,7 @@
|
|
|
23
24
|
const blocksConfig = getBlocksConfig();
|
|
24
25
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
25
26
|
|
|
26
|
-
const variantProps: BreadcrumbVariants = $derived({ size });
|
|
27
|
+
const variantProps: BreadcrumbVariants = $derived({ size, wrap });
|
|
27
28
|
|
|
28
29
|
const styles = $derived(breadcrumbVariants(variantProps));
|
|
29
30
|
|
|
@@ -14,45 +14,62 @@ export declare const breadcrumbVariants: (props?: import("../../utils/variants.j
|
|
|
14
14
|
separator: string;
|
|
15
15
|
};
|
|
16
16
|
};
|
|
17
|
+
wrap: {
|
|
18
|
+
true: {
|
|
19
|
+
list: string;
|
|
20
|
+
};
|
|
21
|
+
false: {
|
|
22
|
+
nav: string;
|
|
23
|
+
list: string;
|
|
24
|
+
currentPage: string;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
17
27
|
}> | undefined) => {
|
|
18
28
|
nav: (props?: ({
|
|
19
29
|
size?: "sm" | "md" | "lg" | undefined;
|
|
30
|
+
wrap?: boolean | undefined;
|
|
20
31
|
} & {
|
|
21
32
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
22
33
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
23
34
|
}) | undefined) => string;
|
|
24
35
|
list: (props?: ({
|
|
25
36
|
size?: "sm" | "md" | "lg" | undefined;
|
|
37
|
+
wrap?: boolean | undefined;
|
|
26
38
|
} & {
|
|
27
39
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
28
40
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
29
41
|
}) | undefined) => string;
|
|
30
42
|
item: (props?: ({
|
|
31
43
|
size?: "sm" | "md" | "lg" | undefined;
|
|
44
|
+
wrap?: boolean | undefined;
|
|
32
45
|
} & {
|
|
33
46
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
34
47
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
35
48
|
}) | undefined) => string;
|
|
36
49
|
link: (props?: ({
|
|
37
50
|
size?: "sm" | "md" | "lg" | undefined;
|
|
51
|
+
wrap?: boolean | undefined;
|
|
38
52
|
} & {
|
|
39
53
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
40
54
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
41
55
|
}) | undefined) => string;
|
|
42
56
|
currentPage: (props?: ({
|
|
43
57
|
size?: "sm" | "md" | "lg" | undefined;
|
|
58
|
+
wrap?: boolean | undefined;
|
|
44
59
|
} & {
|
|
45
60
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
46
61
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
47
62
|
}) | undefined) => string;
|
|
48
63
|
ellipsis: (props?: ({
|
|
49
64
|
size?: "sm" | "md" | "lg" | undefined;
|
|
65
|
+
wrap?: boolean | undefined;
|
|
50
66
|
} & {
|
|
51
67
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
52
68
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
53
69
|
}) | undefined) => string;
|
|
54
70
|
separator: (props?: ({
|
|
55
71
|
size?: "sm" | "md" | "lg" | undefined;
|
|
72
|
+
wrap?: boolean | undefined;
|
|
56
73
|
} & {
|
|
57
74
|
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
58
75
|
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
@@ -2,7 +2,7 @@ import { tv } from '../../utils/variants.js';
|
|
|
2
2
|
export const breadcrumbVariants = tv({
|
|
3
3
|
slots: {
|
|
4
4
|
nav: 'flex items-center',
|
|
5
|
-
list: 'flex items-center
|
|
5
|
+
list: 'flex items-center',
|
|
6
6
|
item: 'inline-flex items-center',
|
|
7
7
|
link: [
|
|
8
8
|
'text-text-secondary hover:text-text-primary',
|
|
@@ -35,9 +35,29 @@ export const breadcrumbVariants = tv({
|
|
|
35
35
|
list: 'text-base gap-1',
|
|
36
36
|
separator: 'mx-2'
|
|
37
37
|
}
|
|
38
|
+
},
|
|
39
|
+
// Overflow strategy when the trail runs out of horizontal room.
|
|
40
|
+
// `wrap` (default): the trail wraps to multiple lines — the standard
|
|
41
|
+
// free-flowing breadcrumb. `false`: the trail stays on one line and the
|
|
42
|
+
// current page (always the last item) truncates while the ancestor links
|
|
43
|
+
// hold their width — for tight single-line bars (sticky headers, toolbars)
|
|
44
|
+
// where the page name is the part worth shortening. Pair with `maxItems`
|
|
45
|
+
// to fold the middle into a "…" before the current page has to truncate.
|
|
46
|
+
wrap: {
|
|
47
|
+
true: {
|
|
48
|
+
list: 'flex-wrap'
|
|
49
|
+
},
|
|
50
|
+
false: {
|
|
51
|
+
nav: 'min-w-0',
|
|
52
|
+
list: 'flex-nowrap min-w-0 [&>li:not(:last-child)]:shrink-0 [&>li:last-child]:min-w-0 [&>li:last-child]:shrink',
|
|
53
|
+
// Let the current page consume the available width and truncate, rather
|
|
54
|
+
// than capping at the wrap-mode max-width.
|
|
55
|
+
currentPage: 'max-w-none'
|
|
56
|
+
}
|
|
38
57
|
}
|
|
39
58
|
},
|
|
40
59
|
defaultVariants: {
|
|
41
|
-
size: 'md'
|
|
60
|
+
size: 'md',
|
|
61
|
+
wrap: true
|
|
42
62
|
}
|
|
43
63
|
});
|
|
@@ -52,6 +52,13 @@ export interface BreadcrumbProps extends BreadcrumbVariants, Omit<HTMLAttributes
|
|
|
52
52
|
items: BreadcrumbItem[];
|
|
53
53
|
/** Size @default 'md' */
|
|
54
54
|
size?: 'sm' | 'md' | 'lg';
|
|
55
|
+
/**
|
|
56
|
+
* Let the trail wrap onto multiple lines (`true`, default) or keep it on a
|
|
57
|
+
* single line where the current page truncates and the ancestor links hold
|
|
58
|
+
* their width (`false`). Use `false` for tight single-line bars such as a
|
|
59
|
+
* sticky header or toolbar. @default true
|
|
60
|
+
*/
|
|
61
|
+
wrap?: boolean;
|
|
55
62
|
/** Custom separator snippet (default: "/") */
|
|
56
63
|
separator?: Snippet;
|
|
57
64
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import CloseIconDefault from '../../icons/CloseIcon.svelte';
|
|
8
8
|
import ChevronDownIconDefault from '../../icons/ChevronDownIcon.svelte';
|
|
9
9
|
import CheckIconDefault from '../../icons/CheckIcon.svelte';
|
|
10
|
-
import { useFormField, getTierContext,
|
|
10
|
+
import { useFormField, getTierContext, useFloatingPanel } from '../../utils';
|
|
11
11
|
import type { ComboboxProps, ComboboxOption } from './index';
|
|
12
12
|
|
|
13
13
|
const bt = useBlocksI18n();
|
|
@@ -263,10 +263,13 @@
|
|
|
263
263
|
}
|
|
264
264
|
});
|
|
265
265
|
|
|
266
|
-
|
|
266
|
+
useFloatingPanel({
|
|
267
267
|
reference: () => inputEl,
|
|
268
268
|
floating: () => listboxEl,
|
|
269
|
-
open: () => open
|
|
269
|
+
open: () => open,
|
|
270
|
+
// The listbox always tracks the input width exactly; the generic panel
|
|
271
|
+
// helper defaults to no width-sync, so the listbox pattern opts in.
|
|
272
|
+
syncWidth: () => true
|
|
270
273
|
});
|
|
271
274
|
|
|
272
275
|
const listboxId = $derived(`${id}-listbox`);
|
|
@@ -16,7 +16,12 @@ export const menuVariants = tv({
|
|
|
16
16
|
// modify trigger stays on the subtler container border.
|
|
17
17
|
content: [
|
|
18
18
|
'box-border border shadow-[var(--blocks-shadow-md)] p-1',
|
|
19
|
-
|
|
19
|
+
// 20rem (max-h-80) is the static design cap; the wrapping Popover's
|
|
20
|
+
// useFloatingPanel sets `--blocks-overlay-available-height` on the panel,
|
|
21
|
+
// which cascades to this content surface so the menu shrinks above the
|
|
22
|
+
// iOS keyboard and recovers when it closes. Falls back to the design cap
|
|
23
|
+
// when the var is unset (SSR / no JS).
|
|
24
|
+
'max-h-[min(20rem,var(--blocks-overlay-available-height,100dvh))] overflow-y-auto',
|
|
20
25
|
'bg-surface-elevated'
|
|
21
26
|
],
|
|
22
27
|
header: ['sticky top-0 z-10 bg-surface-elevated', 'px-2 py-1 border-b border-border-hairline'],
|
|
@@ -1,14 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { untrack } from 'svelte';
|
|
3
3
|
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
4
|
-
import {
|
|
5
|
-
autoUpdate,
|
|
6
|
-
computePosition,
|
|
7
|
-
flip,
|
|
8
|
-
size as floatingSize,
|
|
9
|
-
offset,
|
|
10
|
-
shift
|
|
11
|
-
} from '../../utils/floating';
|
|
4
|
+
import { useFloatingPanel } from '../../utils';
|
|
12
5
|
import { popoverVariants } from './popover.variants';
|
|
13
6
|
import type { PopoverProps } from './index';
|
|
14
7
|
|
|
@@ -56,7 +49,6 @@
|
|
|
56
49
|
|
|
57
50
|
let internalTriggerElement = $state<HTMLElement | null>(null);
|
|
58
51
|
let popoverElement = $state<HTMLElement | null>(null);
|
|
59
|
-
let cleanupFloatingUI = $state<(() => void) | undefined>();
|
|
60
52
|
|
|
61
53
|
const effectiveTriggerElement = $derived(triggerElement || internalTriggerElement);
|
|
62
54
|
|
|
@@ -245,94 +237,24 @@
|
|
|
245
237
|
});
|
|
246
238
|
|
|
247
239
|
// ── Floating UI positioning + native show/hide ─────────────
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
popoverElement.showPopover();
|
|
267
|
-
} catch (err) {
|
|
268
|
-
// Real causes: legacy browser without Popover API (Safari < 17,
|
|
269
|
-
// Chrome < 114, Firefox < 125), element detached, or `popover`
|
|
270
|
-
// attribute overridden by a consumer via restProps.
|
|
271
|
-
console.warn('[Popover] showPopover failed', err);
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const updatePosition = () => {
|
|
276
|
-
if (!effectiveTriggerElement || !popoverElement) return;
|
|
277
|
-
|
|
278
|
-
return computePosition(effectiveTriggerElement, popoverElement, {
|
|
279
|
-
placement,
|
|
280
|
-
// `fixed` is the right strategy when the panel sits in the popover
|
|
281
|
-
// top layer (viewport-coordinated); `absolute` matches the in-flow
|
|
282
|
-
// mode where the panel is positioned relative to the trigger's
|
|
283
|
-
// offset parent.
|
|
284
|
-
strategy: usePortal ? 'fixed' : 'absolute',
|
|
285
|
-
middleware: [
|
|
286
|
-
offset(offsetDistance),
|
|
287
|
-
flip(),
|
|
288
|
-
shift({ padding: shiftPadding }),
|
|
289
|
-
floatingSize({
|
|
290
|
-
apply({ rects }) {
|
|
291
|
-
if (!popoverElement) return;
|
|
292
|
-
// `syncWidth` clamps the panel to the trigger width exactly
|
|
293
|
-
// (Select/Combobox semantics — items longer than the field
|
|
294
|
-
// truncate or wrap). `syncMinWidth` is the looser Menu-style
|
|
295
|
-
// variant: panel ≥ trigger.width, but content is free to grow
|
|
296
|
-
// it. Hard sync wins if both are set.
|
|
297
|
-
popoverElement.style.width = syncWidth ? `${rects.reference.width}px` : '';
|
|
298
|
-
popoverElement.style.minWidth =
|
|
299
|
-
!syncWidth && syncMinWidth ? `${rects.reference.width}px` : '';
|
|
300
|
-
}
|
|
301
|
-
})
|
|
302
|
-
]
|
|
303
|
-
})
|
|
304
|
-
.then(({ x, y }) => {
|
|
305
|
-
if (!popoverElement) return;
|
|
306
|
-
// `autoUpdate` may fire after the trigger has detached, in which
|
|
307
|
-
// case Floating UI returns NaN. Bail rather than writing `NaNpx`.
|
|
308
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
309
|
-
Object.assign(popoverElement.style, {
|
|
310
|
-
left: `${x}px`,
|
|
311
|
-
top: `${y}px`
|
|
312
|
-
});
|
|
313
|
-
})
|
|
314
|
-
.catch((err) => {
|
|
315
|
-
// Middleware can throw synchronously (e.g. geometry read on a
|
|
316
|
-
// detached node). Without a catch this becomes an unhandled
|
|
317
|
-
// rejection with no Popover context.
|
|
318
|
-
console.warn('[Popover] computePosition failed', err);
|
|
319
|
-
});
|
|
320
|
-
};
|
|
321
|
-
|
|
322
|
-
updatePosition();
|
|
323
|
-
cleanupFloatingUI = autoUpdate(effectiveTriggerElement, popoverElement, updatePosition);
|
|
324
|
-
|
|
325
|
-
return () => {
|
|
326
|
-
cleanupFloatingUI?.();
|
|
327
|
-
cleanupFloatingUI = undefined;
|
|
328
|
-
if (popoverElement?.matches(':popover-open')) {
|
|
329
|
-
try {
|
|
330
|
-
popoverElement.hidePopover();
|
|
331
|
-
} catch (err) {
|
|
332
|
-
console.warn('[Popover] hidePopover failed', err);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
};
|
|
240
|
+
//
|
|
241
|
+
// Delegated to the shared `useFloatingPanel` helper — the same positioning
|
|
242
|
+
// codepath Select/Combobox use — so the iOS/visualViewport handling and the
|
|
243
|
+
// keyboard-aware height cap (`--blocks-overlay-available-height`, consumed by
|
|
244
|
+
// popoverVariants/menuVariants) land here too instead of being a second,
|
|
245
|
+
// drifting copy. The helper drives `showPopover()`/`hidePopover()` purely
|
|
246
|
+
// from `open`; the `popover="auto"`/`"manual"` dismiss wiring above is
|
|
247
|
+
// independent of it.
|
|
248
|
+
useFloatingPanel({
|
|
249
|
+
reference: () => effectiveTriggerElement,
|
|
250
|
+
floating: () => popoverElement,
|
|
251
|
+
open: () => open,
|
|
252
|
+
portal: () => usePortal,
|
|
253
|
+
placement: () => placement,
|
|
254
|
+
offsetDistance: () => offsetDistance,
|
|
255
|
+
shiftPadding: () => shiftPadding,
|
|
256
|
+
syncWidth: () => syncWidth,
|
|
257
|
+
syncMinWidth: () => syncMinWidth
|
|
336
258
|
});
|
|
337
259
|
|
|
338
260
|
// ── Trigger handlers ───────────────────────────────────────
|
|
@@ -390,9 +312,10 @@
|
|
|
390
312
|
`position: absolute`, anchored to the trigger's offset parent. The
|
|
391
313
|
`popover` attribute is suppressed so the browser does not promote the
|
|
392
314
|
element to the top layer; Floating UI still drives `left` / `top` via
|
|
393
|
-
|
|
394
|
-
shows / hides the element. Used by Menu /
|
|
395
|
-
inside another popover (avoids nested-top-layer
|
|
315
|
+
`useFloatingPanel` (which skips show/hide when not portalled), and the
|
|
316
|
+
`open`/`!open` toggle just shows / hides the element. Used by Menu /
|
|
317
|
+
Select consumers that live inside another popover (avoids nested-top-layer
|
|
318
|
+
focus & z-index quirks).
|
|
396
319
|
-->
|
|
397
320
|
<div
|
|
398
321
|
bind:this={popoverElement}
|
|
@@ -4,7 +4,13 @@ export const popoverVariants = tv({
|
|
|
4
4
|
base: [
|
|
5
5
|
'bg-surface-elevated border border-border-hairline rounded-contain',
|
|
6
6
|
'shadow-[var(--blocks-shadow-md)] backdrop-blur-sm',
|
|
7
|
-
|
|
7
|
+
// `calc(100dvh-4rem)` is the static design cap; Floating UI's `size`
|
|
8
|
+
// middleware (via useFloatingPanel) narrows it to the room actually left
|
|
9
|
+
// between the anchor and the visual viewport edge through
|
|
10
|
+
// `--blocks-overlay-available-height`, so the panel shrinks above the iOS
|
|
11
|
+
// keyboard and recovers when it closes. The var falls back to 100dvh when
|
|
12
|
+
// unset (SSR / no JS), leaving the design cap in charge.
|
|
13
|
+
'overflow-y-auto max-h-[min(calc(100dvh-4rem),var(--blocks-overlay-available-height,100dvh))]'
|
|
8
14
|
],
|
|
9
15
|
variants: {
|
|
10
16
|
size: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts" generics="T extends string | number | boolean = string">
|
|
2
2
|
import { useBlocksI18n, mintRegistry } from '../..';
|
|
3
|
-
import { useFormField, getTierContext,
|
|
3
|
+
import { useFormField, getTierContext, useFloatingPanel } from '../../utils';
|
|
4
4
|
import { getBlocksConfig, resolveSlotClasses } from '../../provider';
|
|
5
5
|
import { resolveIcon } from '../../icons';
|
|
6
6
|
import ChevronDownIconDefault from '../../icons/ChevronDownIcon.svelte';
|
|
@@ -228,7 +228,7 @@
|
|
|
228
228
|
activeIndex = next;
|
|
229
229
|
});
|
|
230
230
|
|
|
231
|
-
|
|
231
|
+
useFloatingPanel({
|
|
232
232
|
reference: () => triggerRef,
|
|
233
233
|
floating: () => listboxRef,
|
|
234
234
|
open: () => open,
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -11,6 +11,6 @@ export { type EasingFn, getOverlayMotion, OVERLAY_MOTION_DEFAULTS, type OverlayM
|
|
|
11
11
|
export * from './persistent-state.svelte.js';
|
|
12
12
|
export { getTierContext, type InteractiveTier, setTierContext, type TierContext } from './tier-context.js';
|
|
13
13
|
export * from './types.js';
|
|
14
|
-
export { type
|
|
14
|
+
export { type FloatingPanelOptions, useFloatingPanel } from './use-floating-panel.svelte.js';
|
|
15
15
|
export { computeFormFieldAria, type UseFormFieldInputs, type UseFormFieldReturn, useFormField } from './use-form-field.svelte.js';
|
|
16
16
|
export { cx, type TVProps, tv, type VariantProps } from './variants.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -11,6 +11,6 @@ export { getOverlayMotion, OVERLAY_MOTION_DEFAULTS } from './overlay-tokens.js';
|
|
|
11
11
|
export * from './persistent-state.svelte.js';
|
|
12
12
|
export { getTierContext, setTierContext } from './tier-context.js';
|
|
13
13
|
export * from './types.js';
|
|
14
|
-
export {
|
|
14
|
+
export { useFloatingPanel } from './use-floating-panel.svelte.js';
|
|
15
15
|
export { computeFormFieldAria, useFormField } from './use-form-field.svelte.js';
|
|
16
16
|
export { cx, tv } from './variants.js';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Placement } from './floating.js';
|
|
2
|
+
export interface FloatingPanelOptions {
|
|
3
|
+
/** The anchor the panel positions against. */
|
|
4
|
+
reference: () => HTMLElement | null | undefined;
|
|
5
|
+
/** The floating panel element. */
|
|
6
|
+
floating: () => HTMLElement | null | undefined;
|
|
7
|
+
/** Whether the panel is currently open. */
|
|
8
|
+
open: () => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Render in the browser top layer via the native popover API (default).
|
|
11
|
+
* `false` keeps the panel in normal DOM flow (`position: absolute`) for
|
|
12
|
+
* nested-overlay scenarios — the caller drives visibility via `display`.
|
|
13
|
+
*/
|
|
14
|
+
portal?: () => boolean;
|
|
15
|
+
/** Preferred placement. @default 'bottom-start' */
|
|
16
|
+
placement?: () => Placement;
|
|
17
|
+
/** Main-axis gap between anchor and panel, in px. @default 4 */
|
|
18
|
+
offsetDistance?: () => number;
|
|
19
|
+
/** Viewport padding kept by the shift middleware, in px. @default 8 */
|
|
20
|
+
shiftPadding?: () => number;
|
|
21
|
+
/** Clamp the panel width to exactly the anchor width. @default false */
|
|
22
|
+
syncWidth?: () => boolean;
|
|
23
|
+
/** Clamp the panel's *min*-width to the anchor width (it may still grow). @default false */
|
|
24
|
+
syncMinWidth?: () => boolean;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Manages native popover state + Floating UI positioning for anchored overlay
|
|
28
|
+
* panels (Select, Combobox, Popover, and through it Menu). Handles show/hide,
|
|
29
|
+
* the autoUpdate lifecycle, position computation, a keyboard-aware height cap,
|
|
30
|
+
* and cleanup — one positioning codepath shared across every overlay so an
|
|
31
|
+
* iOS/visualViewport or flip/shift fix lands everywhere at once.
|
|
32
|
+
*
|
|
33
|
+
* `popover` (top layer) keeps the panel above any `overflow` clipping from
|
|
34
|
+
* parent containers. `popover="manual"` (set by the caller in markup) leaves
|
|
35
|
+
* the caller's own dismiss handlers in control; this helper only drives
|
|
36
|
+
* `showPopover()`/`hidePopover()` and never reads the dismiss mode.
|
|
37
|
+
*
|
|
38
|
+
* The Floating-UI `size` middleware feeds the room actually left between the
|
|
39
|
+
* anchor and the (visual) viewport edge into `--blocks-overlay-available-height`.
|
|
40
|
+
* Variants cap height via `max-h-[min(<design-cap>,var(--…,100dvh))]`, so the
|
|
41
|
+
* static design cap stays in CSS and the panel only ever shrinks to fit — and
|
|
42
|
+
* recovers once room is restored (e.g. the iOS keyboard closes).
|
|
43
|
+
*/
|
|
44
|
+
export declare function useFloatingPanel(opts: FloatingPanelOptions): void;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { autoUpdate, computePosition, flip, size as floatingSize, offset, shift } from './floating.js';
|
|
2
|
+
/**
|
|
3
|
+
* Manages native popover state + Floating UI positioning for anchored overlay
|
|
4
|
+
* panels (Select, Combobox, Popover, and through it Menu). Handles show/hide,
|
|
5
|
+
* the autoUpdate lifecycle, position computation, a keyboard-aware height cap,
|
|
6
|
+
* and cleanup — one positioning codepath shared across every overlay so an
|
|
7
|
+
* iOS/visualViewport or flip/shift fix lands everywhere at once.
|
|
8
|
+
*
|
|
9
|
+
* `popover` (top layer) keeps the panel above any `overflow` clipping from
|
|
10
|
+
* parent containers. `popover="manual"` (set by the caller in markup) leaves
|
|
11
|
+
* the caller's own dismiss handlers in control; this helper only drives
|
|
12
|
+
* `showPopover()`/`hidePopover()` and never reads the dismiss mode.
|
|
13
|
+
*
|
|
14
|
+
* The Floating-UI `size` middleware feeds the room actually left between the
|
|
15
|
+
* anchor and the (visual) viewport edge into `--blocks-overlay-available-height`.
|
|
16
|
+
* Variants cap height via `max-h-[min(<design-cap>,var(--…,100dvh))]`, so the
|
|
17
|
+
* static design cap stays in CSS and the panel only ever shrinks to fit — and
|
|
18
|
+
* recovers once room is restored (e.g. the iOS keyboard closes).
|
|
19
|
+
*/
|
|
20
|
+
export function useFloatingPanel(opts) {
|
|
21
|
+
let cleanupPosition;
|
|
22
|
+
$effect(() => {
|
|
23
|
+
const ref = opts.reference();
|
|
24
|
+
const floating = opts.floating();
|
|
25
|
+
const isOpen = opts.open();
|
|
26
|
+
const usePortal = opts.portal?.() ?? true;
|
|
27
|
+
const placement = opts.placement?.() ?? 'bottom-start';
|
|
28
|
+
const offsetDistance = opts.offsetDistance?.() ?? 4;
|
|
29
|
+
const shiftPadding = opts.shiftPadding?.() ?? 8;
|
|
30
|
+
const syncWidth = opts.syncWidth?.() ?? false;
|
|
31
|
+
const syncMinWidth = opts.syncMinWidth?.() ?? false;
|
|
32
|
+
// No panel element → nothing to show, hide, or position.
|
|
33
|
+
if (!floating)
|
|
34
|
+
return;
|
|
35
|
+
// Closed — OR open but the anchor has gone (e.g. a conditionally-rendered
|
|
36
|
+
// trigger unmounted while the panel was up). Either way tear down and hide
|
|
37
|
+
// so a portalled panel can't linger, unanchored, in the top layer. The
|
|
38
|
+
// hide path deliberately needs only `floating`, not `ref`.
|
|
39
|
+
if (!isOpen || !ref) {
|
|
40
|
+
cleanupPosition?.();
|
|
41
|
+
cleanupPosition = undefined;
|
|
42
|
+
// Drop the keyboard-aware height clamp so the next open re-measures from
|
|
43
|
+
// the full design cap instead of inheriting a stale value.
|
|
44
|
+
floating.style.removeProperty('--blocks-overlay-available-height');
|
|
45
|
+
if (usePortal && floating.matches(':popover-open')) {
|
|
46
|
+
try {
|
|
47
|
+
floating.hidePopover();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* already hidden */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (usePortal && !floating.matches(':popover-open')) {
|
|
56
|
+
try {
|
|
57
|
+
floating.showPopover();
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* already shown or not supported */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
cleanupPosition?.();
|
|
64
|
+
cleanupPosition = autoUpdate(ref, floating, () => {
|
|
65
|
+
computePosition(ref, floating, {
|
|
66
|
+
placement,
|
|
67
|
+
strategy: usePortal ? 'fixed' : 'absolute',
|
|
68
|
+
middleware: [
|
|
69
|
+
offset(offsetDistance),
|
|
70
|
+
flip(),
|
|
71
|
+
shift({ padding: shiftPadding }),
|
|
72
|
+
floatingSize({
|
|
73
|
+
apply({ availableHeight, rects }) {
|
|
74
|
+
// Cap the panel to the room actually left between the anchor and
|
|
75
|
+
// the (visual) viewport edge — decisive on iOS, where the
|
|
76
|
+
// keyboard shrinks the visualViewport. Exposed as a CSS var that
|
|
77
|
+
// the variant feeds into `max-h-[min(<cap>,var(--…))]`, so the
|
|
78
|
+
// design token stays the upper bound while this tracks the live
|
|
79
|
+
// available height (and recovers when room is restored).
|
|
80
|
+
floating.style.setProperty('--blocks-overlay-available-height', `${Math.max(0, Math.round(availableHeight))}px`);
|
|
81
|
+
// `syncWidth` clamps the panel to the anchor width exactly
|
|
82
|
+
// (Select/Combobox semantics — items wider than the field
|
|
83
|
+
// truncate or wrap). `syncMinWidth` is the looser Menu-style
|
|
84
|
+
// variant: panel ≥ anchor width, but content is free to grow it.
|
|
85
|
+
// Hard sync wins if both are set; otherwise width resets so a
|
|
86
|
+
// toggled-off sync cannot leave a stale inline width behind.
|
|
87
|
+
floating.style.width = syncWidth ? `${rects.reference.width}px` : '';
|
|
88
|
+
floating.style.minWidth =
|
|
89
|
+
!syncWidth && syncMinWidth ? `${rects.reference.width}px` : '';
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
]
|
|
93
|
+
})
|
|
94
|
+
.then(({ x, y }) => {
|
|
95
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
96
|
+
return;
|
|
97
|
+
Object.assign(floating.style, {
|
|
98
|
+
left: `${x}px`,
|
|
99
|
+
top: `${y}px`
|
|
100
|
+
});
|
|
101
|
+
})
|
|
102
|
+
.catch(() => { });
|
|
103
|
+
});
|
|
104
|
+
return () => {
|
|
105
|
+
cleanupPosition?.();
|
|
106
|
+
cleanupPosition = undefined;
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.9",
|
|
4
4
|
"description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"@sveltejs/package": "^2.5.8",
|
|
92
92
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
93
93
|
"@tailwindcss/vite": "^4.3.1",
|
|
94
|
-
"@urbicon-ui/i18n": "6.3.
|
|
95
|
-
"@urbicon-ui/shared-types": "6.3.
|
|
94
|
+
"@urbicon-ui/i18n": "6.3.9",
|
|
95
|
+
"@urbicon-ui/shared-types": "6.3.9",
|
|
96
96
|
"prettier": "^3.8.4",
|
|
97
97
|
"prettier-plugin-svelte": "^4.1.1",
|
|
98
98
|
"prettier-plugin-tailwindcss": "^0.8.0",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface FloatingListboxOptions {
|
|
2
|
-
reference: () => HTMLElement | undefined;
|
|
3
|
-
floating: () => HTMLElement | undefined;
|
|
4
|
-
open: () => boolean;
|
|
5
|
-
portal?: () => boolean;
|
|
6
|
-
syncWidth?: () => boolean;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Manages native popover state + Floating UI positioning for listbox-pattern
|
|
10
|
-
* components (Select, Combobox). Handles show/hide, autoUpdate lifecycle,
|
|
11
|
-
* position computation, and cleanup.
|
|
12
|
-
*
|
|
13
|
-
* `popover="manual"` puts the listbox in the browser top layer so overflow
|
|
14
|
-
* clipping from parent containers cannot hide it. Manual mode (not auto)
|
|
15
|
-
* keeps the component's own dismiss handlers in control.
|
|
16
|
-
*/
|
|
17
|
-
export declare function useFloatingListbox(opts: FloatingListboxOptions): void;
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { autoUpdate, computePosition, flip, size as floatingSize, offset, shift } from './floating.js';
|
|
2
|
-
/**
|
|
3
|
-
* Manages native popover state + Floating UI positioning for listbox-pattern
|
|
4
|
-
* components (Select, Combobox). Handles show/hide, autoUpdate lifecycle,
|
|
5
|
-
* position computation, and cleanup.
|
|
6
|
-
*
|
|
7
|
-
* `popover="manual"` puts the listbox in the browser top layer so overflow
|
|
8
|
-
* clipping from parent containers cannot hide it. Manual mode (not auto)
|
|
9
|
-
* keeps the component's own dismiss handlers in control.
|
|
10
|
-
*/
|
|
11
|
-
export function useFloatingListbox(opts) {
|
|
12
|
-
let cleanupPosition;
|
|
13
|
-
$effect(() => {
|
|
14
|
-
const ref = opts.reference();
|
|
15
|
-
const floating = opts.floating();
|
|
16
|
-
if (!ref || !floating)
|
|
17
|
-
return;
|
|
18
|
-
const isOpen = opts.open();
|
|
19
|
-
const usePortal = opts.portal?.() ?? true;
|
|
20
|
-
const syncWidth = opts.syncWidth?.() ?? true;
|
|
21
|
-
if (!isOpen) {
|
|
22
|
-
cleanupPosition?.();
|
|
23
|
-
cleanupPosition = undefined;
|
|
24
|
-
// Drop the keyboard-aware height clamp so the next open re-measures from
|
|
25
|
-
// the full design cap instead of inheriting a stale value.
|
|
26
|
-
floating.style.removeProperty('--blocks-overlay-available-height');
|
|
27
|
-
if (usePortal && floating.matches(':popover-open')) {
|
|
28
|
-
try {
|
|
29
|
-
floating.hidePopover();
|
|
30
|
-
}
|
|
31
|
-
catch {
|
|
32
|
-
/* already hidden */
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
if (usePortal && !floating.matches(':popover-open')) {
|
|
38
|
-
try {
|
|
39
|
-
floating.showPopover();
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
/* already shown or not supported */
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
cleanupPosition?.();
|
|
46
|
-
cleanupPosition = autoUpdate(ref, floating, () => {
|
|
47
|
-
if (!ref || !floating)
|
|
48
|
-
return;
|
|
49
|
-
computePosition(ref, floating, {
|
|
50
|
-
placement: 'bottom-start',
|
|
51
|
-
strategy: usePortal ? 'fixed' : 'absolute',
|
|
52
|
-
middleware: [
|
|
53
|
-
offset(4),
|
|
54
|
-
flip(),
|
|
55
|
-
shift({ padding: 8 }),
|
|
56
|
-
floatingSize({
|
|
57
|
-
apply({ availableHeight, rects }) {
|
|
58
|
-
// Cap the panel to the room actually left between the anchor and
|
|
59
|
-
// the (visual) viewport edge — decisive on iOS, where the
|
|
60
|
-
// keyboard shrinks the visualViewport. Exposed as a CSS var that
|
|
61
|
-
// the variant feeds into `max-h-[min(15rem,var(--…))]`, so the
|
|
62
|
-
// 15rem design token stays the upper bound while this tracks the
|
|
63
|
-
// live available height (and recovers when room is restored).
|
|
64
|
-
floating.style.setProperty('--blocks-overlay-available-height', `${Math.max(0, Math.round(availableHeight))}px`);
|
|
65
|
-
if (syncWidth) {
|
|
66
|
-
floating.style.width = `${rects.reference.width}px`;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
]
|
|
71
|
-
})
|
|
72
|
-
.then(({ x, y }) => {
|
|
73
|
-
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
74
|
-
return;
|
|
75
|
-
Object.assign(floating.style, {
|
|
76
|
-
left: `${x}px`,
|
|
77
|
-
top: `${y}px`
|
|
78
|
-
});
|
|
79
|
-
})
|
|
80
|
-
.catch(() => { });
|
|
81
|
-
});
|
|
82
|
-
return () => {
|
|
83
|
-
cleanupPosition?.();
|
|
84
|
-
cleanupPosition = undefined;
|
|
85
|
-
};
|
|
86
|
-
});
|
|
87
|
-
}
|