tera-system-ui 0.2.2 → 0.2.4
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/bottom-sheet/BottomSheet.recipe.d.ts +20 -0
- package/dist/components/bottom-sheet/BottomSheet.svelte +113 -4
- package/dist/components/bottom-sheet/bottomSheetStore.svelte.d.ts +28 -1
- package/dist/components/bottom-sheet/bottomSheetStore.svelte.js +60 -39
- package/dist/components/combobox/Combobox.svelte +6 -0
- package/dist/components/date-range-picker/DateRangePicker.svelte +14 -1
- package/dist/components/dialog/Dialog.svelte +15 -2
- package/dist/components/drawer/Drawer.svelte +23 -1
- package/dist/components/dropdown-menu/components/DropdownMenu.svelte +7 -0
- package/dist/components/popover/Popover.svelte +8 -1
- package/dist/components/select/Select.svelte +14 -2
- package/dist/components/toast/ToastContainer.svelte +8 -1
- package/dist/components/tooltip/Tooltip.recipe.js +6 -1
- package/dist/hooks/index.d.ts +4 -0
- package/dist/hooks/index.js +3 -0
- package/dist/hooks/overlayLayer.svelte.d.ts +43 -0
- package/dist/hooks/overlayLayer.svelte.js +74 -0
- package/dist/hooks/overlayStack.svelte.d.ts +59 -0
- package/dist/hooks/overlayStack.svelte.js +107 -0
- package/dist/llms/bottom-sheet.md +2 -0
- package/dist/tokens/index.d.ts +35 -0
- package/dist/tokens/index.js +34 -0
- package/package.json +6 -1
- package/registry/bottom-sheet.json +50 -50
- package/registry/combobox.json +2 -2
- package/registry/date-range-picker.json +2 -2
- package/registry/dialog.json +1 -1
- package/registry/drawer.json +2 -2
- package/registry/dropdown-menu.json +2 -2
- package/registry/popover.json +2 -2
- package/registry/select.json +2 -2
- package/registry/toast.json +1 -1
- package/registry/tooltip.json +1 -1
|
@@ -3,6 +3,7 @@ import { selectRecipe } from './Select.recipe';
|
|
|
3
3
|
import { IconCheck, IconChevronDown } from '../../internal/icons';
|
|
4
4
|
import IconChevronsUp from '@tabler/icons-svelte/icons/chevrons-up';
|
|
5
5
|
import IconChevronsDown from '@tabler/icons-svelte/icons/chevrons-down';
|
|
6
|
+
import { overlayLayer, withZIndex } from '../../hooks';
|
|
6
7
|
let { type = 'single', options = [], groups, value = $bindable(), placeholder = 'Select…', variant, size, disabled, status, loop, allowDeselect, required, name, class: className, checkIcon, onchange, } = $props();
|
|
7
8
|
// All options flat — used for label lookup in multiple mode
|
|
8
9
|
const allOptions = $derived(groups ? groups.flatMap(g => g.options) : options);
|
|
@@ -18,7 +19,16 @@ const selectedChips = $derived(Array.isArray(value)
|
|
|
18
19
|
? value.map(v => ({ value: v, label: allOptions.find(o => o.value === v)?.label ?? v }))
|
|
19
20
|
: []);
|
|
20
21
|
const r = $derived(selectRecipe({ variant, size, disabled, status }));
|
|
22
|
+
// Internal only — Select exposes no `open` prop, but the shared stacking authority needs
|
|
23
|
+
// to know when the listbox is up so a Select inside a Dialog/BottomSheet lands above it
|
|
24
|
+
// instead of behind. `bind:open` on SelectPrimitive.Root is purely observational here.
|
|
25
|
+
let open = $state(false);
|
|
26
|
+
const layer = overlayLayer({ isOpen: () => open, kind: 'menu' });
|
|
27
|
+
const contentZIndex = $derived(layer.zIndex === undefined ? undefined : layer.zIndex + 1);
|
|
21
28
|
const minHeight = "21rem";
|
|
29
|
+
// The content already carries a `style` (max-height); z-index must be CONCATENATED onto
|
|
30
|
+
// it, not added as a second `style` attribute.
|
|
31
|
+
const contentStyle = $derived(withZIndex(`max-height: min(${minHeight}, var(--bits-select-content-available-height))`, contentZIndex));
|
|
22
32
|
</script>
|
|
23
33
|
|
|
24
34
|
{#snippet itemList()}
|
|
@@ -48,6 +58,7 @@ const minHeight = "21rem";
|
|
|
48
58
|
<SelectPrimitive.Root
|
|
49
59
|
type="multiple"
|
|
50
60
|
bind:value={value as string[] | undefined}
|
|
61
|
+
bind:open
|
|
51
62
|
onValueChange={(v) => onchange?.(v)}
|
|
52
63
|
{disabled}
|
|
53
64
|
{loop}
|
|
@@ -79,7 +90,7 @@ const minHeight = "21rem";
|
|
|
79
90
|
<SelectPrimitive.Content
|
|
80
91
|
sideOffset={4}
|
|
81
92
|
class={r.content()}
|
|
82
|
-
style=
|
|
93
|
+
style={contentStyle}
|
|
83
94
|
>
|
|
84
95
|
<SelectPrimitive.ScrollUpButton class="flex w-full items-center justify-center pt-1 text-text-tertiary cursor-default hover:text-text-primary transition-colors">
|
|
85
96
|
<IconChevronsUp class="size-3.5" />
|
|
@@ -96,6 +107,7 @@ const minHeight = "21rem";
|
|
|
96
107
|
<SelectPrimitive.Root
|
|
97
108
|
type="single"
|
|
98
109
|
bind:value={value as string | undefined}
|
|
110
|
+
bind:open
|
|
99
111
|
onValueChange={(v) => onchange?.(v)}
|
|
100
112
|
{disabled}
|
|
101
113
|
{loop}
|
|
@@ -116,7 +128,7 @@ const minHeight = "21rem";
|
|
|
116
128
|
<SelectPrimitive.Content
|
|
117
129
|
sideOffset={6}
|
|
118
130
|
class={r.content()}
|
|
119
|
-
style=
|
|
131
|
+
style={contentStyle}
|
|
120
132
|
>
|
|
121
133
|
<SelectPrimitive.ScrollUpButton class="flex w-full items-center justify-center pt-1 text-text-tertiary cursor-default hover:text-text-primary transition-colors">
|
|
122
134
|
<IconChevronsUp class="size-3.5" />
|
|
@@ -12,10 +12,17 @@ const iconSymbol = {
|
|
|
12
12
|
};
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
|
+
<!--
|
|
16
|
+
z-[8000] = the PINNED toast layer (`layer.toast` in tera-system-ui/tokens — keep in sync;
|
|
17
|
+
Tailwind can only emit a class it reads as literal text). Toasts don't register with the
|
|
18
|
+
overlay stack: a notification must sit above modals regardless of open order, so ordering
|
|
19
|
+
it by open time would be wrong. It was z-[60], which put it BEHIND an open BottomSheet
|
|
20
|
+
(z 1000/1001) — a toast fired over a sheet was simply invisible. It now floats above.
|
|
21
|
+
-->
|
|
15
22
|
<div
|
|
16
23
|
aria-live="polite"
|
|
17
24
|
aria-label="Notifications"
|
|
18
|
-
class="fixed bottom-4 right-4 z-[
|
|
25
|
+
class="fixed bottom-4 right-4 z-[8000] flex flex-col gap-2 max-sm:right-1/2 max-sm:translate-x-1/2"
|
|
19
26
|
>
|
|
20
27
|
{#each toast.items as item (item.id)}
|
|
21
28
|
<div
|
|
@@ -17,7 +17,12 @@ import { tv } from "tailwind-variants";
|
|
|
17
17
|
export const tooltipRecipe = tv({
|
|
18
18
|
slots: {
|
|
19
19
|
trigger: 'inline-flex',
|
|
20
|
-
|
|
20
|
+
// z-[9000] = the PINNED tooltip layer (`layer.tooltip` in tera-system-ui/tokens —
|
|
21
|
+
// keep in sync; Tailwind can only emit a class it reads as literal text). Tooltips
|
|
22
|
+
// don't register with the overlay stack: they're transient, pointer-events:none
|
|
23
|
+
// chrome that belongs above any modal regardless of open order, so ordering them by
|
|
24
|
+
// open time would be meaningless. 9000 stays clear of third-party peaks (~10000).
|
|
25
|
+
content: `z-[9000] max-w-[220px] px-2.5 py-1.5 text-xs leading-snug font-mono font-medium tracking-tight rounded-[var(--tera-tooltip-radius,var(--tera-radius-sm))]
|
|
21
26
|
bg-neutral-token-13 text-neutral-token-1 shadow-lg
|
|
22
27
|
animate-in fade-in zoom-in-95 duration-[var(--tera-duration-fast)] ease-ui
|
|
23
28
|
data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=closed]:zoom-out-95`,
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export { closeOnBackNavigation } from './closeOnBackNavigation.svelte';
|
|
2
|
+
export { overlayStack } from './overlayStack.svelte.js';
|
|
3
|
+
export type { OverlayKind, OverlayStackItem } from './overlayStack.svelte.js';
|
|
4
|
+
export { overlayLayer, withZIndex } from './overlayLayer.svelte.js';
|
|
5
|
+
export type { OverlayLayer } from './overlayLayer.svelte.js';
|
package/dist/hooks/index.js
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
1
|
export { closeOnBackNavigation } from './closeOnBackNavigation.svelte';
|
|
2
|
+
// The layering scale itself lives in the token contract: `tera-system-ui/tokens` → `layer`.
|
|
3
|
+
export { overlayStack } from './overlayStack.svelte.js';
|
|
4
|
+
export { overlayLayer, withZIndex } from './overlayLayer.svelte.js';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers a layered surface with the shared {@link overlayStack} and reports the z-index
|
|
3
|
+
* it should paint at.
|
|
4
|
+
*
|
|
5
|
+
* Call once per surface that OWNS A PORTAL, from a component init scope:
|
|
6
|
+
*
|
|
7
|
+
* const layer = overlayLayer({ isOpen: () => open, kind: 'dialog' });
|
|
8
|
+
* // ...then apply layer.zIndex inline (style:z-index, or withZIndex for a bits-ui prop)
|
|
9
|
+
*
|
|
10
|
+
* Wrappers that merely delegate to another Tera overlay (PopoverResponsive, and
|
|
11
|
+
* DateRangePicker's mobile branch, which both render a real `<Dialog>`) must NOT call this —
|
|
12
|
+
* the child already registers, and a wrapper entry would consume a stack slot while owning
|
|
13
|
+
* no element to receive z, shifting every later overlay's depth for nothing.
|
|
14
|
+
*
|
|
15
|
+
* Must live in a `.svelte.ts` module so it can use `$effect`.
|
|
16
|
+
*/
|
|
17
|
+
import { type OverlayKind } from './overlayStack.svelte.js';
|
|
18
|
+
export interface OverlayLayer {
|
|
19
|
+
/** The z to paint at, or `undefined` before this surface has ever opened (→ use the recipe's class). */
|
|
20
|
+
readonly zIndex: number | undefined;
|
|
21
|
+
/** Whether this surface is the topmost open overlay of ANY type. Drives Escape. */
|
|
22
|
+
readonly isTopmost: boolean;
|
|
23
|
+
}
|
|
24
|
+
export declare function overlayLayer(opts: {
|
|
25
|
+
/** Stable id. Defaults to an auto-generated one; pass it only if you already have one (BottomSheet). */
|
|
26
|
+
id?: string;
|
|
27
|
+
/** Current open state of the surface. */
|
|
28
|
+
isOpen: () => boolean;
|
|
29
|
+
/** Which family this surface belongs to. */
|
|
30
|
+
kind?: OverlayKind;
|
|
31
|
+
}): OverlayLayer;
|
|
32
|
+
/**
|
|
33
|
+
* Merge a runtime z-index into an existing `style` string.
|
|
34
|
+
*
|
|
35
|
+
* For bits-ui components, which take `style` as a PROP (so Svelte's `style:z-index`
|
|
36
|
+
* directive isn't available). Needed because several call sites spread `{...props}` last —
|
|
37
|
+
* a bare `style={...}` before the spread would be clobbered by an app-supplied one — and
|
|
38
|
+
* because Select's content already carries a `style` that must be concatenated, not
|
|
39
|
+
* duplicated. Apply AFTER the rest-spread.
|
|
40
|
+
*
|
|
41
|
+
* Returns `style` untouched when `z` is undefined (surface not open → recipe class stands).
|
|
42
|
+
*/
|
|
43
|
+
export declare function withZIndex(style: unknown, z: number | undefined): string | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers a layered surface with the shared {@link overlayStack} and reports the z-index
|
|
3
|
+
* it should paint at.
|
|
4
|
+
*
|
|
5
|
+
* Call once per surface that OWNS A PORTAL, from a component init scope:
|
|
6
|
+
*
|
|
7
|
+
* const layer = overlayLayer({ isOpen: () => open, kind: 'dialog' });
|
|
8
|
+
* // ...then apply layer.zIndex inline (style:z-index, or withZIndex for a bits-ui prop)
|
|
9
|
+
*
|
|
10
|
+
* Wrappers that merely delegate to another Tera overlay (PopoverResponsive, and
|
|
11
|
+
* DateRangePicker's mobile branch, which both render a real `<Dialog>`) must NOT call this —
|
|
12
|
+
* the child already registers, and a wrapper entry would consume a stack slot while owning
|
|
13
|
+
* no element to receive z, shifting every later overlay's depth for nothing.
|
|
14
|
+
*
|
|
15
|
+
* Must live in a `.svelte.ts` module so it can use `$effect`.
|
|
16
|
+
*/
|
|
17
|
+
import { untrack } from 'svelte';
|
|
18
|
+
import { overlayStack } from './overlayStack.svelte.js';
|
|
19
|
+
// Module scope, monotonic per page load. NOT `$props.id()`: that rune is component-only and
|
|
20
|
+
// is a hard compile error (`props_id_invalid_placement`) inside a `.svelte.ts` module.
|
|
21
|
+
// Mirrors the counter in closeOnBackNavigation.svelte.ts.
|
|
22
|
+
let counter = 0;
|
|
23
|
+
export function overlayLayer(opts) {
|
|
24
|
+
const id = opts.id ?? `tera-overlay-${++counter}`;
|
|
25
|
+
// Non-reactive (the `poppedByBack` pattern from closeOnBackNavigation): the z this
|
|
26
|
+
// surface was last assigned. A bits-ui exit animation keeps painting for ~200ms AFTER
|
|
27
|
+
// `open` flips false and the entry is gone; without this the element would snap back to
|
|
28
|
+
// its recipe fallback (z-50) mid-fade and flash behind whatever it was sitting above.
|
|
29
|
+
// Safe as a plain `let` because z is assigned once and never changes while open, so this
|
|
30
|
+
// can never disagree with a live entry — and it is always written before the store
|
|
31
|
+
// invalidation that makes `getZIndex` return undefined.
|
|
32
|
+
// Stays `undefined` until the first open, so a never-opened surface gets NO inline z and
|
|
33
|
+
// its recipe class stands (which is also what SSR renders — effects don't run there).
|
|
34
|
+
let lastZIndex;
|
|
35
|
+
// A plain `$effect`, not `$effect.pre`: bits-ui's Portal mounts inside runed's `watch()`,
|
|
36
|
+
// which is a POST effect, so portal DOM can never exist before this has run. There is no
|
|
37
|
+
// first-paint flash to guard against.
|
|
38
|
+
//
|
|
39
|
+
// `untrack` is load-bearing, not defensive. push() READS the stack (to find the current
|
|
40
|
+
// top) and WRITES it — so without untrack this effect depends on the very state it
|
|
41
|
+
// mutates and re-runs itself forever (`effect_update_depth_exceeded`), which is doubly
|
|
42
|
+
// wrong here because the re-run would also re-push and hand out a new z. The ONLY thing
|
|
43
|
+
// this effect may depend on is `isOpen()`.
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (!opts.isOpen())
|
|
46
|
+
return;
|
|
47
|
+
untrack(() => {
|
|
48
|
+
lastZIndex = overlayStack.push(id, opts.kind);
|
|
49
|
+
});
|
|
50
|
+
// Runs on close AND on unmount — no leaks.
|
|
51
|
+
return () => untrack(() => overlayStack.remove(id));
|
|
52
|
+
});
|
|
53
|
+
return {
|
|
54
|
+
get zIndex() { return overlayStack.getZIndex(id) ?? lastZIndex; },
|
|
55
|
+
get isTopmost() { return overlayStack.isTopmost(id); },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Merge a runtime z-index into an existing `style` string.
|
|
60
|
+
*
|
|
61
|
+
* For bits-ui components, which take `style` as a PROP (so Svelte's `style:z-index`
|
|
62
|
+
* directive isn't available). Needed because several call sites spread `{...props}` last —
|
|
63
|
+
* a bare `style={...}` before the spread would be clobbered by an app-supplied one — and
|
|
64
|
+
* because Select's content already carries a `style` that must be concatenated, not
|
|
65
|
+
* duplicated. Apply AFTER the rest-spread.
|
|
66
|
+
*
|
|
67
|
+
* Returns `style` untouched when `z` is undefined (surface not open → recipe class stands).
|
|
68
|
+
*/
|
|
69
|
+
export function withZIndex(style, z) {
|
|
70
|
+
const base = typeof style === 'string' ? style.trim().replace(/;$/, '') : undefined;
|
|
71
|
+
if (z === undefined)
|
|
72
|
+
return base || undefined;
|
|
73
|
+
return base ? `${base};z-index:${z}` : `z-index:${z}`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay stacking authority — Svelte 5 runes singleton.
|
|
3
|
+
*
|
|
4
|
+
* THE single source of truth for how layered surfaces stack. Every overlay that owns a
|
|
5
|
+
* portal (dialog, drawer, bottom-sheet, popover, dropdown-menu, select, combobox,
|
|
6
|
+
* date-range-picker) pushes itself here on open and is removed on close/destroy; the z it
|
|
7
|
+
* gets back is applied as an INLINE z-index. That makes stacking deterministic and
|
|
8
|
+
* "last-opened is on top" for arbitrary nesting and mixed types — a dialog opened from
|
|
9
|
+
* inside a bottom sheet lands above it, which is the bug this exists to kill.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT LIVES IN `hooks/` AND NOT `components/`: `scripts/tera-ui.mjs` COPIES component
|
|
12
|
+
* deps into a consuming app but only ever REWRITES `$lib/hooks` to the package path. A
|
|
13
|
+
* copied stacking store would mean two singletons, two stacks, and the bug straight back.
|
|
14
|
+
* This module must be shared, never copied.
|
|
15
|
+
*
|
|
16
|
+
* ## The scale
|
|
17
|
+
*
|
|
18
|
+
* | Layer | z |
|
|
19
|
+
* |--------------------------|------------------------|
|
|
20
|
+
* | interactive overlays | 1000 + n*10 (≤ 7900) |
|
|
21
|
+
* | toast (pinned) | 8000 |
|
|
22
|
+
* | tooltip (pinned) | 9000 |
|
|
23
|
+
* | third-party (MathLive VK)| ~10000 — headroom kept |
|
|
24
|
+
*
|
|
25
|
+
* Base 1000 clears typical app chrome and is byte-identical to the sheet formula this
|
|
26
|
+
* generalizes. Toast and tooltip are PINNED and never register: they are non-competing
|
|
27
|
+
* chrome that must always sit above the stack, so open-order is meaningless for them.
|
|
28
|
+
* Tiers are otherwise deliberately collapsed — among portal-owning overlays, open-order
|
|
29
|
+
* alone already yields the intuitive answer for every nesting case, so a tier base would
|
|
30
|
+
* only add machinery that never changes a result.
|
|
31
|
+
*
|
|
32
|
+
* ## z is assigned ONCE at push and never reindexed
|
|
33
|
+
*
|
|
34
|
+
* `z = max(base, topZ + step)`. Reindexing survivors on removal (what the sheet store used
|
|
35
|
+
* to do) collides with exit animations: a surface that is mid-fade still paints at the z it
|
|
36
|
+
* was given, and live entries shifting underneath it can tie or overtake it. Assign-once is
|
|
37
|
+
* self-bounding anyway — closing the top frees that z for the next push — so the ratchet
|
|
38
|
+
* can't run away.
|
|
39
|
+
*
|
|
40
|
+
* SSR-safe: no `window`/`document` at module load. Surfaces register on mount only.
|
|
41
|
+
*/
|
|
42
|
+
/** Which family a surface belongs to. Only used to project sheet-scoped views today. */
|
|
43
|
+
export type OverlayKind = 'bottom-sheet' | 'dialog' | 'drawer' | 'popover' | 'menu' | 'other';
|
|
44
|
+
export interface OverlayStackItem {
|
|
45
|
+
id: string;
|
|
46
|
+
zIndex: number;
|
|
47
|
+
kind: OverlayKind;
|
|
48
|
+
}
|
|
49
|
+
/** The shared singleton. */
|
|
50
|
+
export declare const overlayStack: {
|
|
51
|
+
/** The live stack, bottom to top. Readonly — mutate via push/remove. */
|
|
52
|
+
readonly stack: OverlayStackItem[];
|
|
53
|
+
readonly hasOpenOverlays: boolean;
|
|
54
|
+
push: (id: string, kind?: OverlayKind) => number;
|
|
55
|
+
remove: (id: string) => void;
|
|
56
|
+
getZIndex: (id: string) => number | undefined;
|
|
57
|
+
has: (id: string) => boolean;
|
|
58
|
+
isTopmost: (id: string) => boolean;
|
|
59
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay stacking authority — Svelte 5 runes singleton.
|
|
3
|
+
*
|
|
4
|
+
* THE single source of truth for how layered surfaces stack. Every overlay that owns a
|
|
5
|
+
* portal (dialog, drawer, bottom-sheet, popover, dropdown-menu, select, combobox,
|
|
6
|
+
* date-range-picker) pushes itself here on open and is removed on close/destroy; the z it
|
|
7
|
+
* gets back is applied as an INLINE z-index. That makes stacking deterministic and
|
|
8
|
+
* "last-opened is on top" for arbitrary nesting and mixed types — a dialog opened from
|
|
9
|
+
* inside a bottom sheet lands above it, which is the bug this exists to kill.
|
|
10
|
+
*
|
|
11
|
+
* WHY IT LIVES IN `hooks/` AND NOT `components/`: `scripts/tera-ui.mjs` COPIES component
|
|
12
|
+
* deps into a consuming app but only ever REWRITES `$lib/hooks` to the package path. A
|
|
13
|
+
* copied stacking store would mean two singletons, two stacks, and the bug straight back.
|
|
14
|
+
* This module must be shared, never copied.
|
|
15
|
+
*
|
|
16
|
+
* ## The scale
|
|
17
|
+
*
|
|
18
|
+
* | Layer | z |
|
|
19
|
+
* |--------------------------|------------------------|
|
|
20
|
+
* | interactive overlays | 1000 + n*10 (≤ 7900) |
|
|
21
|
+
* | toast (pinned) | 8000 |
|
|
22
|
+
* | tooltip (pinned) | 9000 |
|
|
23
|
+
* | third-party (MathLive VK)| ~10000 — headroom kept |
|
|
24
|
+
*
|
|
25
|
+
* Base 1000 clears typical app chrome and is byte-identical to the sheet formula this
|
|
26
|
+
* generalizes. Toast and tooltip are PINNED and never register: they are non-competing
|
|
27
|
+
* chrome that must always sit above the stack, so open-order is meaningless for them.
|
|
28
|
+
* Tiers are otherwise deliberately collapsed — among portal-owning overlays, open-order
|
|
29
|
+
* alone already yields the intuitive answer for every nesting case, so a tier base would
|
|
30
|
+
* only add machinery that never changes a result.
|
|
31
|
+
*
|
|
32
|
+
* ## z is assigned ONCE at push and never reindexed
|
|
33
|
+
*
|
|
34
|
+
* `z = max(base, topZ + step)`. Reindexing survivors on removal (what the sheet store used
|
|
35
|
+
* to do) collides with exit animations: a surface that is mid-fade still paints at the z it
|
|
36
|
+
* was given, and live entries shifting underneath it can tie or overtake it. Assign-once is
|
|
37
|
+
* self-bounding anyway — closing the top frees that z for the next push — so the ratchet
|
|
38
|
+
* can't run away.
|
|
39
|
+
*
|
|
40
|
+
* SSR-safe: no `window`/`document` at module load. Surfaces register on mount only.
|
|
41
|
+
*/
|
|
42
|
+
import { layer } from '../tokens/index.js';
|
|
43
|
+
function createOverlayStack() {
|
|
44
|
+
// Ordered by push time. Because every push takes `topZ + step`, array order is always
|
|
45
|
+
// ascending z — so "last element" IS "topmost", with no re-sorting needed.
|
|
46
|
+
let stack = $state([]);
|
|
47
|
+
/** Highest z currently held, or `base - step` when empty (so the next push lands on base). */
|
|
48
|
+
function topZIndex() {
|
|
49
|
+
let max = layer.base - layer.step;
|
|
50
|
+
for (const item of stack) {
|
|
51
|
+
if (item.zIndex > max)
|
|
52
|
+
max = item.zIndex;
|
|
53
|
+
}
|
|
54
|
+
return max;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add a surface to the top of the stack and return the z it owns for its lifetime.
|
|
58
|
+
* Idempotent: pushing an id that is already present returns its existing z rather than
|
|
59
|
+
* jumping it to the top.
|
|
60
|
+
*/
|
|
61
|
+
function push(id, kind = 'other') {
|
|
62
|
+
const existing = stack.find(item => item.id === id);
|
|
63
|
+
if (existing)
|
|
64
|
+
return existing.zIndex;
|
|
65
|
+
const raw = Math.max(layer.base, topZIndex() + layer.step);
|
|
66
|
+
const zIndex = Math.min(layer.ceiling, raw);
|
|
67
|
+
if (raw > layer.ceiling) {
|
|
68
|
+
// Needs ~690 simultaneously-open overlays to reach. If it ever fires, ordering
|
|
69
|
+
// degrades to DOM order at the top of the stack rather than silently inverting.
|
|
70
|
+
console.warn(`[tera-ui] overlay stack saturated at z-index ${layer.ceiling}; ` +
|
|
71
|
+
`stacking order above this point falls back to DOM order.`);
|
|
72
|
+
}
|
|
73
|
+
stack = [...stack, { id, zIndex, kind }];
|
|
74
|
+
return zIndex;
|
|
75
|
+
}
|
|
76
|
+
/** Remove a surface. Survivors KEEP their z — see the assign-once note above. */
|
|
77
|
+
function remove(id) {
|
|
78
|
+
stack = stack.filter(item => item.id !== id);
|
|
79
|
+
}
|
|
80
|
+
/** The z a surface owns, or `undefined` if it is not open (→ caller falls back to its recipe class). */
|
|
81
|
+
function getZIndex(id) {
|
|
82
|
+
return stack.find(item => item.id === id)?.zIndex;
|
|
83
|
+
}
|
|
84
|
+
/** Whether a surface is open. */
|
|
85
|
+
function has(id) {
|
|
86
|
+
return stack.some(item => item.id === id);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Whether a surface is the topmost open overlay OF ANY TYPE. This is what should drive
|
|
90
|
+
* Escape / close-top, so a sheet and a dialog can't both claim the keypress.
|
|
91
|
+
*/
|
|
92
|
+
function isTopmost(id) {
|
|
93
|
+
return stack[stack.length - 1]?.id === id;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
/** The live stack, bottom to top. Readonly — mutate via push/remove. */
|
|
97
|
+
get stack() { return stack; },
|
|
98
|
+
get hasOpenOverlays() { return stack.length > 0; },
|
|
99
|
+
push,
|
|
100
|
+
remove,
|
|
101
|
+
getZIndex,
|
|
102
|
+
has,
|
|
103
|
+
isTopmost,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The shared singleton. */
|
|
107
|
+
export const overlayStack = createOverlayStack();
|
|
@@ -28,6 +28,8 @@ BottomSheet recipe — the canonical styled layer for the draggable sheet. An iO
|
|
|
28
28
|
dragToClose boolean optional Allow dragging down past the lowest snap point to close.
|
|
29
29
|
closeOnBackdrop boolean optional Close when the backdrop is tapped.
|
|
30
30
|
showCloseButton boolean optional Show a close (X) button in the header (requires the `header` snippet).
|
|
31
|
+
syncThemeColor boolean optional Keep the browser UI tint (iOS Safari status bar / Android address bar) matching the page while the sheet is open. The full-screen backdrop darkens the area behind the status bar; iOS Safari, when the page declares no `<meta name="theme-color">`, re-samples that area and flips the status bar dark — losing the immersive look. When true (the default) the sheet, only if the page has no `theme-color` of its own, adds one pinned to the page background for its open lifetime and removes it on close. A page that sets its own tint is left untouched.
|
|
32
|
+
preventPullToRefresh boolean optional Prevent the browser's native pull-to-refresh / overscroll-reload from hijacking a downward drag that starts on an interactive element inside the sheet. The two-phase gesture only claims the touch (`preventDefault`) once the drag commits past its activation threshold, so a *slow* pull can let the browser start pull-to-refresh first (a quick flick clears the threshold immediately and does not). When true (the default) the sheet pins `overscroll-behavior-y: contain` on the document root for its open lifetime — reference-counted across stacked sheets, the page's original value restored on close. Reliable on Android and iOS 16+. Set false if the host app manages page overscroll itself.
|
|
31
33
|
onClose () => void optional Called after the sheet finishes closing.
|
|
32
34
|
onOpenChange (open: boolean) => void optional Called whenever the open state changes.
|
|
33
35
|
children Snippet optional Main body content.
|
package/dist/tokens/index.d.ts
CHANGED
|
@@ -72,6 +72,40 @@ export declare const token: {
|
|
|
72
72
|
readonly display: string;
|
|
73
73
|
};
|
|
74
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* Layering scale — the shared z-index contract every layered surface resolves against.
|
|
77
|
+
*
|
|
78
|
+
* There is exactly ONE stacking authority (`overlayStack`, see `tera-system-ui/hooks`).
|
|
79
|
+
* Overlays that own a portal push themselves onto it on open and are assigned
|
|
80
|
+
* `base + n*step` (clamped to `ceiling`) as an INLINE z-index, so "last-opened is on top"
|
|
81
|
+
* holds for arbitrary nesting and mixed types. `toast` and `tooltip` are PINNED above the
|
|
82
|
+
* stack — they're non-competing chrome, so open-order is meaningless for them.
|
|
83
|
+
*
|
|
84
|
+
* overlays 1000, 1010, 1020 … ≤ 7900 (backdrop = z, content = z + 1)
|
|
85
|
+
* toast 8000
|
|
86
|
+
* tooltip 9000
|
|
87
|
+
* ~10000 third-party peaks (e.g. a MathLive virtual keyboard) — headroom kept, so an
|
|
88
|
+
* input that owns its own keyboard still layers above our overlays.
|
|
89
|
+
*
|
|
90
|
+
* `base` is high enough to clear typical app chrome. Plain numbers, not CSS vars: z is
|
|
91
|
+
* assigned at runtime from a live stack, not by the cascade.
|
|
92
|
+
*
|
|
93
|
+
* NOTE: the pinned layers are also written as LITERAL Tailwind classes in
|
|
94
|
+
* `Tooltip.recipe.ts` (`z-[9000]`) and `ToastContainer.svelte` (`z-[8000]`) — Tailwind only
|
|
95
|
+
* emits utilities it can read as text, so it cannot see these constants. Keep them in sync.
|
|
96
|
+
*/
|
|
97
|
+
export declare const layer: {
|
|
98
|
+
/** First interactive overlay. */
|
|
99
|
+
readonly base: 1000;
|
|
100
|
+
/** Gap between stacked overlays — leaves room for backdrop `z` + content `z+1`. */
|
|
101
|
+
readonly step: 10;
|
|
102
|
+
/** Ratchet clamp; stays below `toast` so a saturated stack can never bury chrome. */
|
|
103
|
+
readonly ceiling: 7900;
|
|
104
|
+
/** Pinned: notifications sit above modals regardless of open order. */
|
|
105
|
+
readonly toast: 8000;
|
|
106
|
+
/** Pinned: transient, pointer-events:none, always on top. */
|
|
107
|
+
readonly tooltip: 9000;
|
|
108
|
+
};
|
|
75
109
|
/**
|
|
76
110
|
* Per-component override hooks — the raw CSS custom-property names. Set these
|
|
77
111
|
* (not the values above) to restyle a single component's internals. Each
|
|
@@ -166,3 +200,4 @@ export declare const componentVars: {
|
|
|
166
200
|
};
|
|
167
201
|
export type Token = typeof token;
|
|
168
202
|
export type ComponentVars = typeof componentVars;
|
|
203
|
+
export type Layer = typeof layer;
|
package/dist/tokens/index.js
CHANGED
|
@@ -79,6 +79,40 @@ export const token = {
|
|
|
79
79
|
display: cssVar("--font-display"),
|
|
80
80
|
},
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* Layering scale — the shared z-index contract every layered surface resolves against.
|
|
84
|
+
*
|
|
85
|
+
* There is exactly ONE stacking authority (`overlayStack`, see `tera-system-ui/hooks`).
|
|
86
|
+
* Overlays that own a portal push themselves onto it on open and are assigned
|
|
87
|
+
* `base + n*step` (clamped to `ceiling`) as an INLINE z-index, so "last-opened is on top"
|
|
88
|
+
* holds for arbitrary nesting and mixed types. `toast` and `tooltip` are PINNED above the
|
|
89
|
+
* stack — they're non-competing chrome, so open-order is meaningless for them.
|
|
90
|
+
*
|
|
91
|
+
* overlays 1000, 1010, 1020 … ≤ 7900 (backdrop = z, content = z + 1)
|
|
92
|
+
* toast 8000
|
|
93
|
+
* tooltip 9000
|
|
94
|
+
* ~10000 third-party peaks (e.g. a MathLive virtual keyboard) — headroom kept, so an
|
|
95
|
+
* input that owns its own keyboard still layers above our overlays.
|
|
96
|
+
*
|
|
97
|
+
* `base` is high enough to clear typical app chrome. Plain numbers, not CSS vars: z is
|
|
98
|
+
* assigned at runtime from a live stack, not by the cascade.
|
|
99
|
+
*
|
|
100
|
+
* NOTE: the pinned layers are also written as LITERAL Tailwind classes in
|
|
101
|
+
* `Tooltip.recipe.ts` (`z-[9000]`) and `ToastContainer.svelte` (`z-[8000]`) — Tailwind only
|
|
102
|
+
* emits utilities it can read as text, so it cannot see these constants. Keep them in sync.
|
|
103
|
+
*/
|
|
104
|
+
export const layer = {
|
|
105
|
+
/** First interactive overlay. */
|
|
106
|
+
base: 1000,
|
|
107
|
+
/** Gap between stacked overlays — leaves room for backdrop `z` + content `z+1`. */
|
|
108
|
+
step: 10,
|
|
109
|
+
/** Ratchet clamp; stays below `toast` so a saturated stack can never bury chrome. */
|
|
110
|
+
ceiling: 7900,
|
|
111
|
+
/** Pinned: notifications sit above modals regardless of open order. */
|
|
112
|
+
toast: 8000,
|
|
113
|
+
/** Pinned: transient, pointer-events:none, always on top. */
|
|
114
|
+
tooltip: 9000,
|
|
115
|
+
};
|
|
82
116
|
/**
|
|
83
117
|
* Per-component override hooks — the raw CSS custom-property names. Set these
|
|
84
118
|
* (not the values above) to restyle a single component's internals. Each
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tera-system-ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "vite dev",
|
|
6
6
|
"build": "npm run customPrepublish && npm run generate-index && npm run generate-registry && npm run generate-llms && vite build && npm run package && npm run copy-docs && npm run copy-llms && npm run postpublish",
|
|
@@ -58,6 +58,11 @@
|
|
|
58
58
|
"types": "./dist/utils/index.d.ts",
|
|
59
59
|
"default": "./dist/utils/index.js"
|
|
60
60
|
},
|
|
61
|
+
"./hooks": {
|
|
62
|
+
"types": "./dist/hooks/index.d.ts",
|
|
63
|
+
"svelte": "./dist/hooks/index.js",
|
|
64
|
+
"default": "./dist/hooks/index.js"
|
|
65
|
+
},
|
|
61
66
|
"./recipes": {
|
|
62
67
|
"types": "./dist/recipes/index.d.ts",
|
|
63
68
|
"svelte": "./dist/recipes/index.js",
|