tera-system-ui 0.2.3 → 0.2.5

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 (32) hide show
  1. package/dist/components/bottom-sheet/BottomSheet.recipe.d.ts +9 -0
  2. package/dist/components/bottom-sheet/BottomSheet.svelte +36 -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/tokens/index.d.ts +35 -0
  21. package/dist/tokens/index.js +34 -0
  22. package/package.json +6 -1
  23. package/registry/bottom-sheet.json +4 -4
  24. package/registry/combobox.json +2 -2
  25. package/registry/date-range-picker.json +2 -2
  26. package/registry/dialog.json +1 -1
  27. package/registry/drawer.json +2 -2
  28. package/registry/dropdown-menu.json +2 -2
  29. package/registry/popover.json +2 -2
  30. package/registry/select.json +2 -2
  31. package/registry/toast.json +1 -1
  32. package/registry/tooltip.json +1 -1
@@ -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();
@@ -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.3",
3
+ "version": "0.2.5",
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",