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.
Files changed (33) hide show
  1. package/dist/components/bottom-sheet/BottomSheet.recipe.d.ts +20 -0
  2. package/dist/components/bottom-sheet/BottomSheet.svelte +113 -4
  3. package/dist/components/bottom-sheet/bottomSheetStore.svelte.d.ts +28 -1
  4. package/dist/components/bottom-sheet/bottomSheetStore.svelte.js +60 -39
  5. package/dist/components/combobox/Combobox.svelte +6 -0
  6. package/dist/components/date-range-picker/DateRangePicker.svelte +14 -1
  7. package/dist/components/dialog/Dialog.svelte +15 -2
  8. package/dist/components/drawer/Drawer.svelte +23 -1
  9. package/dist/components/dropdown-menu/components/DropdownMenu.svelte +7 -0
  10. package/dist/components/popover/Popover.svelte +8 -1
  11. package/dist/components/select/Select.svelte +14 -2
  12. package/dist/components/toast/ToastContainer.svelte +8 -1
  13. package/dist/components/tooltip/Tooltip.recipe.js +6 -1
  14. package/dist/hooks/index.d.ts +4 -0
  15. package/dist/hooks/index.js +3 -0
  16. package/dist/hooks/overlayLayer.svelte.d.ts +43 -0
  17. package/dist/hooks/overlayLayer.svelte.js +74 -0
  18. package/dist/hooks/overlayStack.svelte.d.ts +59 -0
  19. package/dist/hooks/overlayStack.svelte.js +107 -0
  20. package/dist/llms/bottom-sheet.md +2 -0
  21. package/dist/tokens/index.d.ts +35 -0
  22. package/dist/tokens/index.js +34 -0
  23. package/package.json +6 -1
  24. package/registry/bottom-sheet.json +50 -50
  25. package/registry/combobox.json +2 -2
  26. package/registry/date-range-picker.json +2 -2
  27. package/registry/dialog.json +1 -1
  28. package/registry/drawer.json +2 -2
  29. package/registry/dropdown-menu.json +2 -2
  30. package/registry/popover.json +2 -2
  31. package/registry/select.json +2 -2
  32. package/registry/toast.json +1 -1
  33. 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="max-height: min({minHeight}, var(--bits-select-content-available-height))"
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="max-height: min({minHeight}, var(--bits-select-content-available-height))"
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-[60] flex flex-col gap-2 max-sm:right-1/2 max-sm:translate-x-1/2"
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
- content: `z-50 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))]
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`,
@@ -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';
@@ -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.
@@ -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;
@@ -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.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",