@warkypublic/svelix 0.1.11 → 0.1.12

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.
@@ -3,6 +3,7 @@
3
3
  import type { BetterMenuInstanceItem, BetterMenuStoreState } from './types';
4
4
  import type { BetterMenuStore } from './store';
5
5
  import BetterMenuAsyncButton from './BetterMenuAsyncButton.svelte';
6
+ import { registerOverlay } from '../OverlayStack';
6
7
  import Portal from '../Portal/Portal.svelte';
7
8
 
8
9
  interface Props {
@@ -11,14 +12,39 @@
11
12
  const { disablePortal = false }: Props = $props();
12
13
 
13
14
  const store = getContext<BetterMenuStore>('betterMenuStore');
14
- let state = $state<BetterMenuStoreState>({ menus: [], providerID: '' });
15
+ let menuState = $state<BetterMenuStoreState>({ menus: [], providerID: '' });
16
+ const hasVisibleMenus = $derived(menuState.menus.some((menu) => menu.visible));
17
+ let backdropZ = $state(1090);
18
+ let menuZ = $state(1100);
19
+ let submenuZ = $state(1110);
15
20
 
16
21
  $effect(() => {
17
22
  return store.subscribe((s) => {
18
- state = s;
23
+ menuState = s;
19
24
  });
20
25
  });
21
26
 
27
+ $effect(() => {
28
+ if (!hasVisibleMenus) {
29
+ return;
30
+ }
31
+
32
+ const overlay = registerOverlay({
33
+ kind: 'menu',
34
+ mount: disablePortal ? 'inline' : 'portal',
35
+ });
36
+ backdropZ = overlay.layer.backdrop;
37
+ menuZ = overlay.layer.content;
38
+ submenuZ = overlay.layer.floating;
39
+
40
+ return () => {
41
+ overlay.unregister();
42
+ backdropZ = 1090;
43
+ menuZ = 1100;
44
+ submenuZ = 1110;
45
+ };
46
+ });
47
+
22
48
  function hideMenu(id: string) {
23
49
  store.setInstanceState(id, 'visible', false);
24
50
  }
@@ -38,7 +64,8 @@
38
64
  <span class="text-xs">▶</span>
39
65
  </button>
40
66
  <div
41
- class="absolute left-full top-0 hidden group-hover:block card bg-surface-100-900 shadow-xl min-w-48 py-1 z-50"
67
+ class="absolute left-full top-0 hidden group-hover:block card bg-surface-100-900 shadow-xl min-w-48 py-1"
68
+ style:z-index={submenuZ}
42
69
  >
43
70
  {#each item.items as subItem, i (`sub_${i}`)}
44
71
  {@render menuItem(subItem, onClose)}
@@ -53,18 +80,19 @@
53
80
  {/snippet}
54
81
 
55
82
  <Portal disabled={disablePortal}>
56
- {#each state.menus as menu, menuIndex (menu.id)}
83
+ {#each menuState.menus as menu, menuIndex (menu.id)}
57
84
  {#if menu.visible}
58
85
  <!-- svelte-ignore a11y_click_events_have_key_events -->
59
86
  <!-- svelte-ignore a11y_no_static_element_interactions -->
60
- <div class="fixed inset-0 z-40" onclick={() => hideMenu(menu.id)}></div>
87
+ <div class="fixed inset-0" onclick={() => hideMenu(menu.id)} style:z-index={backdropZ}></div>
61
88
 
62
89
  <div
63
- class="fixed z-50 card bg-surface-100-900 shadow-xl min-w-48 py-1"
64
- id={`bmm_portal_${state.providerID}_${menuIndex}`}
90
+ class="fixed card bg-surface-100-900 shadow-xl min-w-48 py-1"
91
+ id={`bmm_portal_${menuState.providerID}_${menuIndex}`}
65
92
  style:left="{menu.x}px"
66
93
  style:top="{menu.y}px"
67
- style:width={state.width ? `${state.width}px` : undefined}
94
+ style:width={menuState.width ? `${menuState.width}px` : undefined}
95
+ style:z-index={menuZ}
68
96
  >
69
97
  {#each menu.items ?? [] as item, itemIndex (`${menu.id}_item_${item.id ?? itemIndex}`)}
70
98
  {@render menuItem(item, () => hideMenu(menu.id))}
@@ -5,6 +5,10 @@
5
5
  createVirtualizer,
6
6
  type SvelteVirtualizer,
7
7
  } from "@tanstack/svelte-virtual";
8
+ import {
9
+ registerOverlay,
10
+ } from "../OverlayStack";
11
+ import Portal from "../Portal/Portal.svelte";
8
12
  import type { BoxerItem, BoxerProps } from "./types";
9
13
  import { createBoxerStore } from "./store";
10
14
  import BoxerTarget from "./BoxerTarget.svelte";
@@ -14,6 +18,7 @@
14
18
  clearable = true,
15
19
  data = [],
16
20
  dataSource: dataSourceProp = undefined,
21
+ disablePortal = false,
17
22
  disabled,
18
23
  error,
19
24
  label,
@@ -78,6 +83,13 @@
78
83
  // Virtualizer
79
84
  let parentEl = $state<HTMLDivElement | undefined>(undefined);
80
85
  let targetRef = $state<{ focus: () => void } | undefined>(undefined);
86
+ let anchorEl = $state<HTMLDivElement | undefined>(undefined);
87
+ let dropdownEl = $state<HTMLDivElement | undefined>(undefined);
88
+ let popupTop = $state(0);
89
+ let popupLeft = $state(0);
90
+ let popupWidth = $state(0);
91
+ let backdropZ = $state(1090);
92
+ let dropdownZ = $state(1100);
81
93
  // Plain variable — NOT $state to avoid deep proxy on the complex virtualizer object.
82
94
  let rawVirtualizer: SvelteVirtualizer<
83
95
  HTMLDivElement,
@@ -306,6 +318,22 @@
306
318
  }
307
319
  }
308
320
 
321
+ function updatePopoverPosition() {
322
+ if (!anchorEl) return;
323
+ const rect = anchorEl.getBoundingClientRect();
324
+ const viewportWidth = typeof window === "undefined" ? rect.width : window.innerWidth;
325
+ const horizontalPadding = 8;
326
+ const nextWidth = Math.max(220, rect.width);
327
+ const maxLeft = viewportWidth - nextWidth - horizontalPadding;
328
+
329
+ popupTop = rect.bottom + 4;
330
+ popupWidth = nextWidth;
331
+ popupLeft = Math.min(
332
+ Math.max(rect.left, horizontalPadding),
333
+ Math.max(horizontalPadding, maxLeft),
334
+ );
335
+ }
336
+
309
337
  // Public API via bind:this
310
338
  export function clear() {
311
339
  onClear();
@@ -326,14 +354,59 @@
326
354
  value = val;
327
355
  onChange?.(val);
328
356
  }
357
+
358
+ $effect(() => {
359
+ if (!$store.opened || disablePortal || disabled) {
360
+ return;
361
+ }
362
+
363
+ updatePopoverPosition();
364
+
365
+ if (typeof window === "undefined") {
366
+ return;
367
+ }
368
+
369
+ const onWindowChange = () => {
370
+ updatePopoverPosition();
371
+ };
372
+
373
+ window.addEventListener("resize", onWindowChange);
374
+ window.addEventListener("scroll", onWindowChange, true);
375
+
376
+ return () => {
377
+ window.removeEventListener("resize", onWindowChange);
378
+ window.removeEventListener("scroll", onWindowChange, true);
379
+ };
380
+ });
381
+
382
+ $effect(() => {
383
+ if (!$store.opened || disabled) {
384
+ return;
385
+ }
386
+
387
+ const overlay = registerOverlay({
388
+ kind: "popover",
389
+ mount: disablePortal ? "inline" : "portal",
390
+ });
391
+ backdropZ = overlay.layer.backdrop;
392
+ dropdownZ = overlay.layer.content;
393
+
394
+ return () => {
395
+ overlay.unregister();
396
+ backdropZ = 1090;
397
+ dropdownZ = 1100;
398
+ };
399
+ });
329
400
  </script>
330
401
 
331
402
  <div
332
403
  class="relative w-full"
404
+ bind:this={anchorEl}
333
405
  onfocusout={(e) => {
334
406
  const currentTarget = e.currentTarget as HTMLElement;
335
407
  const relatedTarget = e.relatedTarget as Node | null;
336
- if (!currentTarget.contains(relatedTarget)) {
408
+ const insideDropdown = !!(relatedTarget && dropdownEl?.contains(relatedTarget));
409
+ if (!currentTarget.contains(relatedTarget) && !insideDropdown) {
337
410
  if (!value && !multiSelect) {
338
411
  store.setSearch("");
339
412
  store.setInput("");
@@ -362,71 +435,81 @@
362
435
  }}
363
436
  />
364
437
 
365
- {#if $store.opened}
366
- <!-- svelte-ignore a11y_click_events_have_key_events -->
367
- <!-- svelte-ignore a11y_no_static_element_interactions -->
368
- <div
369
- class="fixed inset-0 z-30"
370
- onclick={() => store.setOpened(false)}
371
- ></div>
372
-
373
- <div
374
- class="absolute left-0 right-0 top-full mt-1 z-40 card bg-surface-50-950 shadow-lg border border-surface-300-700 overflow-hidden"
375
- role="listbox"
376
- aria-label={label ?? "Options"}
377
- >
378
- {#if $store.boxerData.length > 0}
379
- <div
380
- bind:this={parentEl}
381
- class="overflow-auto"
382
- style:max-height="{mah}px"
383
- onscroll={(e) =>
384
- store.fetchMoreOnBottomReached(e.currentTarget as HTMLDivElement)}
385
- >
386
- <div style:height="{totalSize}px" style:position="relative">
387
- <div
388
- style:left="0"
389
- style:position="absolute"
390
- style:top="0"
391
- style:transform="translateY({virtualItems[0]?.start ?? 0}px)"
392
- style:width="100%"
393
- >
394
- {#each virtualItems as vRow (vRow.key)}
395
- {@const item = $store.boxerData[vRow.index]}
396
- {@const isSelected = multiSelect
397
- ? Array.isArray(value) && value.includes(item?.value)
398
- : value === item?.value}
399
- <div
400
- class="px-3 py-2 cursor-pointer text-sm hover:bg-surface-100 dark:hover:bg-surface-700 flex items-center gap-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500"
401
- class:bg-primary-100={isSelected}
402
- class:dark:bg-primary-900={isSelected}
403
- data-index={vRow.index}
404
- role="option"
405
- aria-selected={isSelected}
406
- tabindex="-1"
407
- onclick={() => onOptionSubmit(vRow.index)}
408
- onkeydown={(e) => onDropdownItemKeydown(e, vRow.index)}
409
- >
410
- {#if multiSelect}
411
- <input
412
- checked={isSelected}
413
- class="checkbox"
414
- readonly
415
- tabindex={-1}
416
- type="checkbox"
417
- />
418
- {/if}
419
- <span>{item?.label}</span>
420
- </div>
421
- {/each}
438
+ <Portal disabled={disablePortal}>
439
+ {#if $store.opened}
440
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
441
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
442
+ <div
443
+ class="fixed inset-0"
444
+ onclick={() => store.setOpened(false)}
445
+ style:z-index={backdropZ}
446
+ ></div>
447
+
448
+ <div
449
+ bind:this={dropdownEl}
450
+ class={`${disablePortal
451
+ ? "absolute left-0 right-0 top-full mt-1"
452
+ : "fixed"} card bg-surface-50-950 shadow-lg border border-surface-300-700 overflow-hidden`}
453
+ role="listbox"
454
+ aria-label={label ?? "Options"}
455
+ style:z-index={dropdownZ}
456
+ style:top={!disablePortal ? `${popupTop}px` : undefined}
457
+ style:left={!disablePortal ? `${popupLeft}px` : undefined}
458
+ style:width={!disablePortal ? `${popupWidth}px` : undefined}
459
+ >
460
+ {#if $store.boxerData.length > 0}
461
+ <div
462
+ bind:this={parentEl}
463
+ class="overflow-auto"
464
+ style:max-height="{mah}px"
465
+ onscroll={(e) =>
466
+ store.fetchMoreOnBottomReached(e.currentTarget as HTMLDivElement)}
467
+ >
468
+ <div style:height="{totalSize}px" style:position="relative">
469
+ <div
470
+ style:left="0"
471
+ style:position="absolute"
472
+ style:top="0"
473
+ style:transform="translateY({virtualItems[0]?.start ?? 0}px)"
474
+ style:width="100%"
475
+ >
476
+ {#each virtualItems as vRow (vRow.key)}
477
+ {@const item = $store.boxerData[vRow.index]}
478
+ {@const isSelected = multiSelect
479
+ ? Array.isArray(value) && value.includes(item?.value)
480
+ : value === item?.value}
481
+ <div
482
+ class="px-3 py-2 cursor-pointer text-sm hover:bg-surface-100 dark:hover:bg-surface-700 flex items-center gap-2 focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-primary-500"
483
+ class:bg-primary-100={isSelected}
484
+ class:dark:bg-primary-900={isSelected}
485
+ data-index={vRow.index}
486
+ role="option"
487
+ aria-selected={isSelected}
488
+ tabindex="-1"
489
+ onclick={() => onOptionSubmit(vRow.index)}
490
+ onkeydown={(e) => onDropdownItemKeydown(e, vRow.index)}
491
+ >
492
+ {#if multiSelect}
493
+ <input
494
+ checked={isSelected}
495
+ class="checkbox"
496
+ readonly
497
+ tabindex={-1}
498
+ type="checkbox"
499
+ />
500
+ {/if}
501
+ <span>{item?.label}</span>
502
+ </div>
503
+ {/each}
504
+ </div>
422
505
  </div>
423
506
  </div>
424
- </div>
425
- {:else}
426
- <div class="px-3 py-2 text-sm text-surface-500">Nothing found</div>
427
- {/if}
428
- </div>
429
- {/if}
507
+ {:else}
508
+ <div class="px-3 py-2 text-sm text-surface-500">Nothing found</div>
509
+ {/if}
510
+ </div>
511
+ {/if}
512
+ </Portal>
430
513
 
431
514
  {#if rightSection}
432
515
  <div class="absolute right-8 top-1/2 -translate-y-1/2">
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
- import Portal from "../Portal/Portal.svelte";
4
3
 
5
4
  interface Props {
6
5
  clearable?: boolean;
@@ -45,6 +45,7 @@ export interface BoxerProps {
45
45
  clearable?: boolean;
46
46
  data?: Array<BoxerItem>;
47
47
  dataSource?: BoxerDataSource;
48
+ disablePortal?: boolean;
48
49
  disabled?: boolean;
49
50
  error?: string;
50
51
  id?: string;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  import { Dialog, Portal } from '@skeletonlabs/skeleton-svelte';
4
+ import { registerOverlay } from '../OverlayStack';
4
5
  import Former from './Former.svelte';
5
6
  import type { FormerProps, FormRequestType } from './types';
6
7
 
@@ -56,6 +57,25 @@
56
57
  'transition transition-discrete opacity-0 starting:data-[state=open]:opacity-0 data-[state=open]:opacity-100';
57
58
  const animContent =
58
59
  'transition transition-discrete opacity-0 translate-x-full starting:data-[state=open]:opacity-0 starting:data-[state=open]:translate-x-full data-[state=open]:opacity-100 data-[state=open]:translate-x-0';
60
+
61
+ let backdropZ = $state(1000);
62
+ let contentZ = $state(1010);
63
+
64
+ $effect(() => {
65
+ if (!opened) {
66
+ return;
67
+ }
68
+
69
+ const overlay = registerOverlay({ kind: 'drawer', mount: 'portal' });
70
+ backdropZ = overlay.layer.backdrop;
71
+ contentZ = overlay.layer.content;
72
+
73
+ return () => {
74
+ overlay.unregister();
75
+ backdropZ = 1000;
76
+ contentZ = 1010;
77
+ };
78
+ });
59
79
  </script>
60
80
 
61
81
  <Dialog
@@ -69,8 +89,11 @@
69
89
  modal
70
90
  >
71
91
  <Portal>
72
- <Dialog.Backdrop class="fixed inset-0 z-[1000] bg-black/50 backdrop-blur-sm {animBackdrop}" />
73
- <Dialog.Positioner class="fixed inset-0 z-[1010] flex justify-end">
92
+ <Dialog.Backdrop
93
+ class="fixed inset-0 bg-black/50 backdrop-blur-sm {animBackdrop}"
94
+ style={`z-index:${backdropZ};`}
95
+ />
96
+ <Dialog.Positioner class="fixed inset-0 flex justify-end" style={`z-index:${contentZ};`}>
74
97
  <Dialog.Content
75
98
  class="card bg-surface-100-900 shadow-2xl h-full flex flex-col overflow-hidden rounded-none rounded-l-container-token {animContent}"
76
99
  style="width: {width}"
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
3
  import FormerDrawer from './FormerDrawer.svelte';
4
4
  import type { FormRequestType } from './types';
5
5
  import TextInputCtrl from '../FormerControllers/TextInputCtrl.svelte';
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  /* eslint-disable @typescript-eslint/no-explicit-any */
3
3
  import { Dialog, Portal } from '@skeletonlabs/skeleton-svelte';
4
+ import { registerOverlay } from '../OverlayStack';
4
5
  import Former from './Former.svelte';
5
6
  import type { FormerProps, FormRequestType } from './types';
6
7
 
@@ -50,6 +51,25 @@
50
51
  'transition transition-discrete opacity-0 starting:data-[state=open]:opacity-0 data-[state=open]:opacity-100';
51
52
  const animContent =
52
53
  'transition transition-discrete opacity-0 scale-95 starting:data-[state=open]:opacity-0 starting:data-[state=open]:scale-95 data-[state=open]:opacity-100 data-[state=open]:scale-100';
54
+
55
+ let backdropZ = $state(1000);
56
+ let contentZ = $state(1010);
57
+
58
+ $effect(() => {
59
+ if (!opened) {
60
+ return;
61
+ }
62
+
63
+ const overlay = registerOverlay({ kind: 'dialog', mount: 'portal' });
64
+ backdropZ = overlay.layer.backdrop;
65
+ contentZ = overlay.layer.content;
66
+
67
+ return () => {
68
+ overlay.unregister();
69
+ backdropZ = 1000;
70
+ contentZ = 1010;
71
+ };
72
+ });
53
73
  </script>
54
74
 
55
75
  <Dialog
@@ -63,8 +83,11 @@
63
83
  modal
64
84
  >
65
85
  <Portal>
66
- <Dialog.Backdrop class="fixed inset-0 z-[1000] bg-black/50 backdrop-blur-sm {animBackdrop}" />
67
- <Dialog.Positioner class="fixed inset-0 z-[1010] flex items-center justify-center p-4">
86
+ <Dialog.Backdrop
87
+ class="fixed inset-0 bg-black/50 backdrop-blur-sm {animBackdrop}"
88
+ style={`z-index:${backdropZ};`}
89
+ />
90
+ <Dialog.Positioner class="fixed inset-0 flex items-center justify-center p-4" style={`z-index:${contentZ};`}>
68
91
  <Dialog.Content
69
92
  class="card bg-surface-100-900 shadow-2xl w-full max-w-2xl max-h-[90vh] flex flex-col overflow-hidden {animContent}"
70
93
  >
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
3
  import FormerModal from './FormerModal.svelte';
4
4
  import type { FormRequestType } from './types';
5
5
  import TextInputCtrl from '../FormerControllers/TextInputCtrl.svelte';
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
3
  import Portal from '../../Portal/Portal.svelte';
4
+ import { registerOverlay } from '../../OverlayStack';
4
5
  import type { CtrlBaseProps } from '../types';
5
6
  import DateTimeCtrlPickerPanel from './DateTimeCtrlPickerPanel.svelte';
6
7
  import {
@@ -140,6 +141,7 @@
140
141
  let popupTop = $state(0);
141
142
  let popupLeft = $state(0);
142
143
  let popupWidth = $state(0);
144
+ let pickerZIndex = $state(1100);
143
145
 
144
146
  const calendarMonthLabel = $derived(getYearMonthLabel(calendarYear, calendarMonth));
145
147
  const calendarCells = $derived(getCalendarCells(calendarYear, calendarMonth));
@@ -424,6 +426,23 @@
424
426
  window.removeEventListener('scroll', onWindowChange, true);
425
427
  };
426
428
  });
429
+
430
+ $effect(() => {
431
+ if (!withPicker || !pickerOpen || disabled) {
432
+ return;
433
+ }
434
+
435
+ const overlay = registerOverlay({
436
+ kind: 'popover',
437
+ mount: disablePortal ? 'inline' : 'portal',
438
+ });
439
+ pickerZIndex = overlay.layer.content;
440
+
441
+ return () => {
442
+ overlay.unregister();
443
+ pickerZIndex = 1100;
444
+ };
445
+ });
427
446
  </script>
428
447
 
429
448
  <div class="flex flex-col gap-1" title={tooltip}>
@@ -535,12 +554,13 @@
535
554
  {#if withPicker && pickerOpen && !disabled && disablePortal}
536
555
  <div
537
556
  id={pickerDialogId}
538
- class={`absolute z-20 mt-2 w-full min-w-[17.5rem] max-w-[35rem] rounded border border-surface-300-700 bg-surface-50-950 p-3 shadow-xl ${panelClass}`.trim()}
557
+ class={`absolute mt-2 w-full min-w-[17.5rem] max-w-[35rem] rounded border border-surface-300-700 bg-surface-50-950 p-3 shadow-xl ${panelClass}`.trim()}
539
558
  role="dialog"
540
559
  aria-modal="false"
541
560
  aria-label={dialogAriaLabel}
542
561
  tabindex="-1"
543
562
  onkeydown={handlePickerKeydown}
563
+ style:z-index={pickerZIndex}
544
564
  >
545
565
  <DateTimeCtrlPickerPanel
546
566
  {showDatePicker}
@@ -579,12 +599,13 @@
579
599
  {#if withPicker && pickerOpen && !disabled && !disablePortal}
580
600
  <div
581
601
  id={pickerDialogId}
582
- class={`fixed z-50 max-h-[min(80vh,38rem)] overflow-auto rounded border border-surface-300-700 bg-surface-50-950 p-3 shadow-xl ${panelClass}`.trim()}
602
+ class={`fixed max-h-[min(80vh,38rem)] overflow-auto rounded border border-surface-300-700 bg-surface-50-950 p-3 shadow-xl ${panelClass}`.trim()}
583
603
  role="dialog"
584
604
  aria-modal="false"
585
605
  aria-label={dialogAriaLabel}
586
606
  tabindex="-1"
587
607
  onkeydown={handlePickerKeydown}
608
+ style:z-index={pickerZIndex}
588
609
  style:top={`${popupTop}px`}
589
610
  style:left={`${popupLeft}px`}
590
611
  style:width={`${popupWidth}px`}
@@ -0,0 +1 @@
1
+ export * from './overlayStack';
@@ -0,0 +1 @@
1
+ export * from './overlayStack';
@@ -0,0 +1,29 @@
1
+ export type OverlayKind = 'dialog' | 'drawer' | 'popover' | 'menu' | 'contextmenu';
2
+ export type OverlayMount = 'portal' | 'inline';
3
+ export interface OverlayEntry {
4
+ id: string;
5
+ kind: OverlayKind;
6
+ mount: OverlayMount;
7
+ openedAt: number;
8
+ }
9
+ export interface OverlayStackSnapshot {
10
+ entries: OverlayEntry[];
11
+ }
12
+ export interface OverlayLayer {
13
+ backdrop: number;
14
+ content: number;
15
+ floating: number;
16
+ top: number;
17
+ }
18
+ export declare const overlayStack: {
19
+ subscribe: (this: void, run: import("svelte/store").Subscriber<OverlayStackSnapshot>, invalidate?: () => void) => import("svelte/store").Unsubscriber;
20
+ };
21
+ export declare function registerOverlay(params: {
22
+ kind: OverlayKind;
23
+ mount?: OverlayMount;
24
+ }): {
25
+ id: string;
26
+ layer: OverlayLayer;
27
+ unregister: () => void;
28
+ };
29
+ export declare function resolveOverlayLayer(state: OverlayStackSnapshot, id: string): OverlayLayer;
@@ -0,0 +1,49 @@
1
+ import { writable } from 'svelte/store';
2
+ const BASE_Z_INDEX = 1000;
3
+ const OVERLAY_STRIDE = 20;
4
+ const overlayStackInternal = writable({ entries: [] });
5
+ let openedAtCounter = 0;
6
+ let overlayIdCounter = 0;
7
+ export const overlayStack = {
8
+ subscribe: overlayStackInternal.subscribe,
9
+ };
10
+ function resolveOverlayLayerFromOrder(order) {
11
+ const base = BASE_Z_INDEX + order * OVERLAY_STRIDE;
12
+ return {
13
+ backdrop: base,
14
+ content: base + 10,
15
+ floating: base + 12,
16
+ top: base + 14,
17
+ };
18
+ }
19
+ export function registerOverlay(params) {
20
+ const id = `overlay_${++overlayIdCounter}`;
21
+ const openedAt = ++openedAtCounter;
22
+ const entry = {
23
+ id,
24
+ kind: params.kind,
25
+ mount: params.mount ?? 'portal',
26
+ openedAt,
27
+ };
28
+ overlayStackInternal.update((state) => ({
29
+ ...state,
30
+ entries: [...state.entries, entry],
31
+ }));
32
+ return {
33
+ id,
34
+ layer: resolveOverlayLayerFromOrder(openedAt),
35
+ unregister: () => {
36
+ overlayStackInternal.update((state) => ({
37
+ ...state,
38
+ entries: state.entries.filter((item) => item.id !== id),
39
+ }));
40
+ },
41
+ };
42
+ }
43
+ export function resolveOverlayLayer(state, id) {
44
+ const entry = state.entries.find((item) => item.id === id);
45
+ if (!entry) {
46
+ return resolveOverlayLayerFromOrder(state.entries.length + 1);
47
+ }
48
+ return resolveOverlayLayerFromOrder(entry.openedAt);
49
+ }
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import Portal from '../Portal/Portal.svelte';
3
+ import { registerOverlay } from '../OverlayStack';
2
4
  import type { VTreeContextMenuItem, VTreeRecord } from './types.js';
3
5
 
4
6
  export interface Props<T extends VTreeRecord = VTreeRecord> {
@@ -10,31 +12,53 @@
10
12
  }
11
13
 
12
14
  const { open, x, y, items, onselectitem }: Props = $props();
15
+
16
+ let backdropZ = $state(1090);
17
+ let menuZ = $state(1100);
18
+
19
+ $effect(() => {
20
+ if (!open) {
21
+ return;
22
+ }
23
+
24
+ const overlay = registerOverlay({ kind: 'contextmenu', mount: 'portal' });
25
+ backdropZ = overlay.layer.backdrop;
26
+ menuZ = overlay.layer.content;
27
+
28
+ return () => {
29
+ overlay.unregister();
30
+ backdropZ = 1090;
31
+ menuZ = 1100;
32
+ };
33
+ });
13
34
  </script>
14
35
 
15
- {#if open}
16
- <button
17
- class="fixed inset-0 z-50"
18
- type="button"
19
- aria-label="Close context menu"
20
- onclick={() => onselectitem?.({ id: '__close__', label: '' })}
21
- ></button>
22
- <div
23
- class="fixed z-[60] min-w-44 overflow-hidden rounded border border-surface-300-700 bg-surface-100-900 shadow-xl"
24
- style={`left:${x}px; top:${y}px;`}
25
- role="menu"
26
- aria-label="Row context menu"
27
- >
28
- {#each items as item (item.id)}
29
- <button
30
- class="block w-full px-3 py-2 text-left text-sm hover:bg-surface-200-800 disabled:cursor-not-allowed disabled:opacity-50"
31
- disabled={item.disabled}
32
- type="button"
33
- role="menuitem"
34
- onclick={() => onselectitem?.(item)}
35
- >
36
- {item.label}
37
- </button>
38
- {/each}
39
- </div>
40
- {/if}
36
+ <Portal>
37
+ {#if open}
38
+ <button
39
+ class="fixed inset-0"
40
+ type="button"
41
+ aria-label="Close context menu"
42
+ onclick={() => onselectitem?.({ id: '__close__', label: '' })}
43
+ style:z-index={backdropZ}
44
+ ></button>
45
+ <div
46
+ class="fixed min-w-44 overflow-hidden rounded border border-surface-300-700 bg-surface-100-900 shadow-xl"
47
+ style={`left:${x}px; top:${y}px; z-index:${menuZ};`}
48
+ role="menu"
49
+ aria-label="Row context menu"
50
+ >
51
+ {#each items as item (item.id)}
52
+ <button
53
+ class="block w-full px-3 py-2 text-left text-sm hover:bg-surface-200-800 disabled:cursor-not-allowed disabled:opacity-50"
54
+ disabled={item.disabled}
55
+ type="button"
56
+ role="menuitem"
57
+ onclick={() => onselectitem?.(item)}
58
+ >
59
+ {item.label}
60
+ </button>
61
+ {/each}
62
+ </div>
63
+ {/if}
64
+ </Portal>
@@ -6,6 +6,7 @@ export * from "./Former/index";
6
6
  export * from "./Boxer/index";
7
7
  export * from "./Gridler/index";
8
8
  export * from "./Portal/index";
9
+ export * from "./OverlayStack/index";
9
10
  export * from "./Svark/index";
10
11
  export * from "./GlobalStateStore/index";
11
12
  export * from "./Screenshot/index";
@@ -6,6 +6,7 @@ export * from "./Former/index";
6
6
  export * from "./Boxer/index";
7
7
  export * from "./Gridler/index";
8
8
  export * from "./Portal/index";
9
+ export * from "./OverlayStack/index";
9
10
  export * from "./Svark/index";
10
11
  export * from "./GlobalStateStore/index";
11
12
  export * from "./Screenshot/index";
@@ -27,24 +27,24 @@
27
27
  --anchor-text-decoration-active: none;
28
28
  --anchor-text-decoration-focus: none;
29
29
  --spacing: 0.22rem;
30
- --radius-base: 0.125rem;
31
- --radius-container: 0.125rem;
30
+ --radius-base: 0.375rem;
31
+ --radius-container: 0.75rem;
32
32
  --default-border-width: 2px;
33
- --default-divide-width: 1px;
34
- --default-ring-width: 2px;
33
+ --default-divide-width: 2px;
34
+ --default-ring-width: 1px;
35
35
  --body-background-color: var(--color-surface-50);
36
36
  --body-background-color-dark: var(--color-surface-950);
37
37
  --color-primary-50: oklch(91.66% 0.04 257.51deg);
38
- --color-primary-100: oklch(84.1% 0.08 254.61deg);
39
- --color-primary-200: oklch(76.5% 0.11 254.28deg);
40
- --color-primary-300: oklch(69.52% 0.15 254.36deg);
41
- --color-primary-400: oklch(62.85% 0.19 255.71deg);
42
- --color-primary-500: oklch(57.32% 0.21 258.29deg);
43
- --color-primary-600: oklch(51.62% 0.19 258.15deg);
44
- --color-primary-700: oklch(46.06% 0.17 257.78deg);
45
- --color-primary-800: oklch(40.05% 0.14 257.62deg);
46
- --color-primary-900: oklch(34.15% 0.11 257.14deg);
47
- --color-primary-950: oklch(27.73% 0.08 257.49deg);
38
+ --color-primary-100: oklch(84.19% 0.07 255.4deg);
39
+ --color-primary-200: oklch(76.89% 0.11 254.81deg);
40
+ --color-primary-300: oklch(69.67% 0.15 255.38deg);
41
+ --color-primary-400: oklch(63.17% 0.18 256.17deg);
42
+ --color-primary-500: oklch(57.44% 0.21 258.27deg);
43
+ --color-primary-600: oklch(51.82% 0.19 258.2deg);
44
+ --color-primary-700: oklch(45.98% 0.16 258deg);
45
+ --color-primary-800: oklch(40.28% 0.14 257.7deg);
46
+ --color-primary-900: oklch(34.07% 0.11 257.5deg);
47
+ --color-primary-950: oklch(27.72% 0.08 257.98deg);
48
48
  --color-primary-contrast-dark: var(--color-primary-900);
49
49
  --color-primary-contrast-light: var(--color-primary-50);
50
50
  --color-primary-contrast-50: var(--color-primary-contrast-dark);
@@ -58,24 +58,24 @@
58
58
  --color-primary-contrast-800: var(--color-primary-contrast-light);
59
59
  --color-primary-contrast-900: var(--color-primary-contrast-light);
60
60
  --color-primary-contrast-950: var(--color-primary-contrast-light);
61
- --color-secondary-50: oklch(86.66% 0.05 300.15deg);
62
- --color-secondary-100: oklch(78.51% 0.09 303.57deg);
63
- --color-secondary-200: oklch(70.44% 0.13 304.44deg);
64
- --color-secondary-300: oklch(62.83% 0.17 303.81deg);
65
- --color-secondary-400: oklch(55.48% 0.2 302.75deg);
66
- --color-secondary-500: oklch(49.07% 0.23 300.46deg);
67
- --color-secondary-600: oklch(45.39% 0.21 299.6deg);
68
- --color-secondary-700: oklch(41.75% 0.19 298.26deg);
69
- --color-secondary-800: oklch(37.84% 0.17 296.27deg);
70
- --color-secondary-900: oklch(34.08% 0.15 293.97deg);
71
- --color-secondary-950: oklch(30.18% 0.13 291.16deg);
61
+ --color-secondary-50: oklch(86.62% 0.05 300.58deg);
62
+ --color-secondary-100: oklch(79.55% 0.05 350.56deg);
63
+ --color-secondary-200: oklch(72.85% 0.07 25.52deg);
64
+ --color-secondary-300: oklch(66.69% 0.11 39.77deg);
65
+ --color-secondary-400: oklch(61.19% 0.15 43.62deg);
66
+ --color-secondary-500: oklch(56.57% 0.17 40.75deg);
67
+ --color-secondary-600: oklch(50.53% 0.15 38.69deg);
68
+ --color-secondary-700: oklch(44.68% 0.12 29.27deg);
69
+ --color-secondary-800: oklch(38.89% 0.1 3.71deg);
70
+ --color-secondary-900: oklch(33.93% 0.1 324.36deg);
71
+ --color-secondary-950: oklch(30.27% 0.13 290.83deg);
72
72
  --color-secondary-contrast-dark: var(--color-secondary-950);
73
73
  --color-secondary-contrast-light: var(--color-secondary-50);
74
74
  --color-secondary-contrast-50: var(--color-secondary-contrast-dark);
75
75
  --color-secondary-contrast-100: var(--color-secondary-contrast-dark);
76
76
  --color-secondary-contrast-200: var(--color-secondary-contrast-dark);
77
77
  --color-secondary-contrast-300: var(--color-secondary-contrast-dark);
78
- --color-secondary-contrast-400: var(--color-secondary-contrast-light);
78
+ --color-secondary-contrast-400: var(--color-secondary-contrast-dark);
79
79
  --color-secondary-contrast-500: var(--color-secondary-contrast-light);
80
80
  --color-secondary-contrast-600: var(--color-secondary-contrast-light);
81
81
  --color-secondary-contrast-700: var(--color-secondary-contrast-light);
@@ -85,38 +85,38 @@
85
85
  --color-tertiary-50: oklch(90.73% 0.08 328.92deg);
86
86
  --color-tertiary-100: oklch(82.91% 0.13 339.68deg);
87
87
  --color-tertiary-200: oklch(76% 0.18 345.55deg);
88
- --color-tertiary-300: oklch(70.27% 0.23 350.68deg);
89
- --color-tertiary-400: oklch(66.48% 0.25 355.85deg);
90
- --color-tertiary-500: oklch(64.54% 0.26 2.48deg);
91
- --color-tertiary-600: oklch(59.37% 0.24 1.7deg);
92
- --color-tertiary-700: oklch(53.9% 0.22 0.5deg);
93
- --color-tertiary-800: oklch(48.45% 0.2 359.66deg);
94
- --color-tertiary-900: oklch(42.69% 0.17 357.71deg);
95
- --color-tertiary-950: oklch(36.93% 0.15 355.34deg);
88
+ --color-tertiary-300: oklch(70.37% 0.22 350.76deg);
89
+ --color-tertiary-400: oklch(66.52% 0.25 355.9deg);
90
+ --color-tertiary-500: oklch(64.55% 0.26 2.5deg);
91
+ --color-tertiary-600: oklch(59.39% 0.24 1.72deg);
92
+ --color-tertiary-700: oklch(53.93% 0.22 0.54deg);
93
+ --color-tertiary-800: oklch(48.51% 0.2 359.74deg);
94
+ --color-tertiary-900: oklch(42.76% 0.17 357.83deg);
95
+ --color-tertiary-950: oklch(37.03% 0.15 355.51deg);
96
96
  --color-tertiary-contrast-dark: var(--color-tertiary-950);
97
97
  --color-tertiary-contrast-light: var(--color-tertiary-50);
98
98
  --color-tertiary-contrast-50: var(--color-tertiary-contrast-dark);
99
99
  --color-tertiary-contrast-100: var(--color-tertiary-contrast-dark);
100
100
  --color-tertiary-contrast-200: var(--color-tertiary-contrast-dark);
101
- --color-tertiary-contrast-300: var(--color-tertiary-contrast-light);
102
- --color-tertiary-contrast-400: var(--color-tertiary-contrast-light);
103
- --color-tertiary-contrast-500: var(--color-tertiary-contrast-light);
101
+ --color-tertiary-contrast-300: var(--color-tertiary-contrast-dark);
102
+ --color-tertiary-contrast-400: var(--color-tertiary-contrast-dark);
103
+ --color-tertiary-contrast-500: var(--color-tertiary-contrast-dark);
104
104
  --color-tertiary-contrast-600: var(--color-tertiary-contrast-light);
105
105
  --color-tertiary-contrast-700: var(--color-tertiary-contrast-light);
106
106
  --color-tertiary-contrast-800: var(--color-tertiary-contrast-light);
107
107
  --color-tertiary-contrast-900: var(--color-tertiary-contrast-light);
108
108
  --color-tertiary-contrast-950: var(--color-tertiary-contrast-light);
109
- --color-success-50: oklch(94.05% 0.09 178.66deg);
110
- --color-success-100: oklch(91.62% 0.1 178.6deg);
111
- --color-success-200: oklch(89.44% 0.11 177.16deg);
112
- --color-success-300: oklch(87.13% 0.12 176.9deg);
113
- --color-success-400: oklch(85.09% 0.13 175.45deg);
114
- --color-success-500: oklch(82.91% 0.13 174.95deg);
115
- --color-success-600: oklch(72.85% 0.12 175.7deg);
116
- --color-success-700: oklch(62.4% 0.1 175.99deg);
117
- --color-success-800: oklch(51.26% 0.08 178.28deg);
118
- --color-success-900: oklch(39.72% 0.06 179.74deg);
119
- --color-success-950: oklch(27.27% 0.04 185.29deg);
109
+ --color-success-50: oklch(93.9% 0.09 179.09deg);
110
+ --color-success-100: oklch(91.57% 0.1 178.72deg);
111
+ --color-success-200: oklch(89.21% 0.11 177.71deg);
112
+ --color-success-300: oklch(87.2% 0.12 176.72deg);
113
+ --color-success-400: oklch(84.95% 0.12 175.68deg);
114
+ --color-success-500: oklch(82.81% 0.13 175.08deg);
115
+ --color-success-600: oklch(72.74% 0.11 175.88deg);
116
+ --color-success-700: oklch(62.29% 0.1 176.23deg);
117
+ --color-success-800: oklch(51.16% 0.08 178.61deg);
118
+ --color-success-900: oklch(39.61% 0.06 180.35deg);
119
+ --color-success-950: oklch(27.17% 0.04 186.97deg);
120
120
  --color-success-contrast-dark: var(--color-success-950);
121
121
  --color-success-contrast-light: var(--color-success-50);
122
122
  --color-success-contrast-50: var(--color-success-contrast-dark);
@@ -125,22 +125,22 @@
125
125
  --color-success-contrast-300: var(--color-success-contrast-dark);
126
126
  --color-success-contrast-400: var(--color-success-contrast-dark);
127
127
  --color-success-contrast-500: var(--color-success-contrast-dark);
128
- --color-success-contrast-600: var(--color-success-contrast-light);
129
- --color-success-contrast-700: var(--color-success-contrast-light);
128
+ --color-success-contrast-600: var(--color-success-contrast-dark);
129
+ --color-success-contrast-700: var(--color-success-contrast-dark);
130
130
  --color-success-contrast-800: var(--color-success-contrast-light);
131
131
  --color-success-contrast-900: var(--color-success-contrast-light);
132
132
  --color-success-contrast-950: var(--color-success-contrast-light);
133
- --color-warning-50: oklch(95.67% 0.05 84.56deg);
134
- --color-warning-100: oklch(92.83% 0.06 82.16deg);
135
- --color-warning-200: oklch(90.12% 0.08 80.33deg);
136
- --color-warning-300: oklch(87.59% 0.1 80.01deg);
137
- --color-warning-400: oklch(85.03% 0.12 78.35deg);
138
- --color-warning-500: oklch(82.46% 0.14 76.71deg);
139
- --color-warning-600: oklch(76.34% 0.13 72.25deg);
140
- --color-warning-700: oklch(70.34% 0.13 68.09deg);
133
+ --color-warning-50: oklch(95.58% 0.05 86.88deg);
134
+ --color-warning-100: oklch(92.81% 0.07 83.57deg);
135
+ --color-warning-200: oklch(90.05% 0.09 81.8deg);
136
+ --color-warning-300: oklch(87.42% 0.11 80.05deg);
137
+ --color-warning-400: oklch(84.79% 0.12 78.56deg);
138
+ --color-warning-500: oklch(82.33% 0.14 76.57deg);
139
+ --color-warning-600: oklch(76.31% 0.14 72.87deg);
140
+ --color-warning-700: oklch(70.33% 0.13 68.3deg);
141
141
  --color-warning-800: oklch(63.99% 0.13 63.18deg);
142
- --color-warning-900: oklch(57.91% 0.13 57.97deg);
143
- --color-warning-950: oklch(51.69% 0.13 51.44deg);
142
+ --color-warning-900: oklch(57.92% 0.13 57.63deg);
143
+ --color-warning-950: oklch(51.71% 0.13 51.88deg);
144
144
  --color-warning-contrast-dark: var(--color-warning-950);
145
145
  --color-warning-contrast-light: var(--color-warning-50);
146
146
  --color-warning-contrast-50: var(--color-warning-contrast-dark);
@@ -149,22 +149,22 @@
149
149
  --color-warning-contrast-300: var(--color-warning-contrast-dark);
150
150
  --color-warning-contrast-400: var(--color-warning-contrast-dark);
151
151
  --color-warning-contrast-500: var(--color-warning-contrast-dark);
152
- --color-warning-contrast-600: var(--color-warning-contrast-light);
152
+ --color-warning-contrast-600: var(--color-warning-contrast-dark);
153
153
  --color-warning-contrast-700: var(--color-warning-contrast-light);
154
154
  --color-warning-contrast-800: var(--color-warning-contrast-light);
155
155
  --color-warning-contrast-900: var(--color-warning-contrast-light);
156
156
  --color-warning-contrast-950: var(--color-warning-contrast-light);
157
157
  --color-error-50: oklch(89.99% 0.04 14.04deg);
158
- --color-error-100: oklch(85.74% 0.07 43.8deg);
159
- --color-error-200: oklch(81.53% 0.11 52.73deg);
160
- --color-error-300: oklch(77.99% 0.14 55.49deg);
161
- --color-error-400: oklch(74.61% 0.17 54.32deg);
162
- --color-error-500: oklch(72.01% 0.19 49.76deg);
163
- --color-error-600: oklch(69% 0.19 47.98deg);
164
- --color-error-700: oklch(65.83% 0.18 46.33deg);
165
- --color-error-800: oklch(62.8% 0.18 44.45deg);
166
- --color-error-900: oklch(59.61% 0.18 42.69deg);
167
- --color-error-950: oklch(56.57% 0.17 40.75deg);
158
+ --color-error-100: oklch(85.76% 0.07 42.85deg);
159
+ --color-error-200: oklch(81.56% 0.11 51.68deg);
160
+ --color-error-300: oklch(78.03% 0.14 54.55deg);
161
+ --color-error-400: oklch(74.63% 0.17 53.69deg);
162
+ --color-error-500: oklch(72.02% 0.19 49.51deg);
163
+ --color-error-600: oklch(68.87% 0.19 47.84deg);
164
+ --color-error-700: oklch(65.84% 0.18 46.46deg);
165
+ --color-error-800: oklch(62.8% 0.18 44.35deg);
166
+ --color-error-900: oklch(59.74% 0.17 42.77deg);
167
+ --color-error-950: oklch(56.52% 0.17 40.7deg);
168
168
  --color-error-contrast-dark: var(--color-error-950);
169
169
  --color-error-contrast-light: var(--color-error-50);
170
170
  --color-error-contrast-50: var(--color-error-contrast-dark);
@@ -195,7 +195,7 @@
195
195
  --color-surface-contrast-100: var(--color-surface-contrast-dark);
196
196
  --color-surface-contrast-200: var(--color-surface-contrast-dark);
197
197
  --color-surface-contrast-300: var(--color-surface-contrast-dark);
198
- --color-surface-contrast-400: var(--color-surface-contrast-light);
198
+ --color-surface-contrast-400: var(--color-surface-contrast-dark);
199
199
  --color-surface-contrast-500: var(--color-surface-contrast-light);
200
200
  --color-surface-contrast-600: var(--color-surface-contrast-light);
201
201
  --color-surface-contrast-700: var(--color-surface-contrast-light);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@warkypublic/svelix",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Svelte 5 component library with Skeleton UI and Tailwind CSS",
5
5
  "license": "Apache-2.0",
6
6
  "exports": {