sheer-ui 0.4.0 → 0.4.3

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 (96) hide show
  1. package/package.json +1 -1
  2. package/src/lib/components/accordion/components/accordion-item.svelte +3 -3
  3. package/src/lib/components/accordion/components/accordion.svelte +29 -13
  4. package/src/lib/components/accordion/types.ts +3 -3
  5. package/src/lib/components/avatar/avatar.svelte.ts +34 -37
  6. package/src/lib/components/avatar/components/avatar-fallback.svelte +9 -6
  7. package/src/lib/components/avatar/components/avatar-image.svelte +18 -9
  8. package/src/lib/components/avatar/components/avatar.svelte +20 -16
  9. package/src/lib/components/checkbox/checkbox.svelte.ts +45 -55
  10. package/src/lib/components/checkbox/components/checkbox-group-label.svelte +9 -6
  11. package/src/lib/components/checkbox/components/checkbox-group.svelte +25 -14
  12. package/src/lib/components/checkbox/components/checkbox.svelte +39 -21
  13. package/src/lib/components/combobox/components/combobox.svelte +2 -2
  14. package/src/lib/components/combobox/select/components/select.svelte +2 -2
  15. package/src/lib/components/context-menu/components/context-menu-trigger.svelte +1 -1
  16. package/src/lib/components/date-field/date-field.svelte.ts +2 -3
  17. package/src/lib/components/dialog/components/dialog-close.svelte +13 -8
  18. package/src/lib/components/dialog/components/dialog-content-headless.svelte +14 -11
  19. package/src/lib/components/dialog/components/dialog-description.svelte +9 -6
  20. package/src/lib/components/dialog/components/dialog-overlay.svelte +10 -7
  21. package/src/lib/components/dialog/components/dialog-title.svelte +12 -7
  22. package/src/lib/components/dialog/components/dialog-trigger.svelte +12 -7
  23. package/src/lib/components/dialog/components/dialog.svelte +10 -4
  24. package/src/lib/components/dialog/components/modal-surface.svelte +11 -8
  25. package/src/lib/components/dialog/dialog.svelte.ts +73 -61
  26. package/src/lib/components/drawer/drawer.svelte +8 -3
  27. package/src/lib/components/link-preview/components/link-preview-content.svelte +15 -8
  28. package/src/lib/components/link-preview/components/link-preview-trigger.svelte +9 -6
  29. package/src/lib/components/link-preview/components/link-preview.svelte +19 -11
  30. package/src/lib/components/link-preview/link-preview.svelte.ts +42 -43
  31. package/src/lib/components/menu/components/menu-checkbox-group.svelte +6 -4
  32. package/src/lib/components/menu/components/menu-checkbox-item.svelte +11 -7
  33. package/src/lib/components/menu/menu.svelte.ts +29 -19
  34. package/src/lib/components/menubar/menubar.svelte.ts +7 -6
  35. package/src/lib/components/meter/components/meter.svelte +18 -9
  36. package/src/lib/components/meter/meter.svelte.ts +15 -18
  37. package/src/lib/components/navigation-menu/components/navigation-menu-content.svelte +1 -1
  38. package/src/lib/components/navigation-menu/components/navigation-menu-indicator.svelte +2 -2
  39. package/src/lib/components/navigation-menu/components/navigation-menu-trigger.svelte +1 -1
  40. package/src/lib/components/navigation-menu/navigation-menu.svelte.ts +57 -60
  41. package/src/lib/components/pagination/components/pagination-next-button.svelte +12 -7
  42. package/src/lib/components/pagination/components/pagination-page.svelte +15 -8
  43. package/src/lib/components/pagination/components/pagination-prev-button.svelte +12 -7
  44. package/src/lib/components/pagination/components/pagination.svelte +31 -18
  45. package/src/lib/components/pagination/pagination.svelte.ts +48 -64
  46. package/src/lib/components/pin-input/pin-input.svelte.ts +7 -8
  47. package/src/lib/components/pin-input/usePasswordManager.svelte.ts +2 -2
  48. package/src/lib/components/popover/components/popover-close.svelte +9 -6
  49. package/src/lib/components/popover/components/popover-content.svelte +12 -7
  50. package/src/lib/components/popover/components/popover-trigger.svelte +21 -10
  51. package/src/lib/components/popover/components/popover.svelte +9 -8
  52. package/src/lib/components/popover/popover.svelte.ts +68 -74
  53. package/src/lib/components/progress/components/progress.svelte +18 -9
  54. package/src/lib/components/progress/progress.svelte.ts +14 -17
  55. package/src/lib/components/scroll-area/scroll-area.svelte.ts +9 -10
  56. package/src/lib/components/separator/components/separator.svelte +15 -8
  57. package/src/lib/components/separator/separator.svelte.ts +12 -15
  58. package/src/lib/components/sidebar/sidebar-mobile-surface.svelte +8 -4
  59. package/src/lib/components/slider/components/slider.svelte +2 -1
  60. package/src/lib/components/tabs/components/tabs-content.svelte +12 -7
  61. package/src/lib/components/tabs/components/tabs-list.svelte +9 -6
  62. package/src/lib/components/tabs/components/tabs-trigger.svelte +15 -8
  63. package/src/lib/components/tabs/components/tabs.svelte +28 -17
  64. package/src/lib/components/tabs/tabs.svelte.ts +51 -67
  65. package/src/lib/components/time-field/time-field.svelte.ts +2 -2
  66. package/src/lib/components/toggle/components/toggle.svelte +19 -14
  67. package/src/lib/components/toggle/toggle.svelte.ts +15 -20
  68. package/src/lib/components/toggle-group/components/toggle-group-item.svelte +15 -8
  69. package/src/lib/components/toggle-group/components/toggle-group.svelte +28 -16
  70. package/src/lib/components/toggle-group/toggle-group.svelte.ts +33 -35
  71. package/src/lib/components/toolbar/components/toolbar-group-item.svelte +15 -8
  72. package/src/lib/components/toolbar/components/toolbar-group.svelte +19 -14
  73. package/src/lib/components/toolbar/toolbar.svelte.ts +28 -30
  74. package/src/lib/components/tooltip/components/tooltip-content.svelte +15 -8
  75. package/src/lib/components/tooltip/components/tooltip-provider.svelte +18 -7
  76. package/src/lib/components/tooltip/components/tooltip-trigger.svelte +21 -10
  77. package/src/lib/components/tooltip/components/tooltip.svelte +34 -21
  78. package/src/lib/components/tooltip/tooltip.svelte.ts +101 -125
  79. package/src/lib/internal/dismissible-layer/use-dismissable-layer.svelte.ts +3 -4
  80. package/src/lib/internal/escape-layer/use-escape-layer.svelte.ts +13 -29
  81. package/src/lib/internal/floating-layer/use-floating-layer.svelte.ts +26 -30
  82. package/src/lib/internal/group-value.svelte.ts +11 -10
  83. package/src/lib/internal/native-popover.svelte.ts +6 -6
  84. package/src/lib/internal/{roving-focus-group.ts → roving-focus-group.svelte.ts} +17 -19
  85. package/src/lib/internal/roving-focus-item.svelte.ts +12 -17
  86. package/src/lib/internal/selection.svelte.ts +55 -41
  87. package/src/lib/internal/{state-machine.ts → state-machine.svelte.ts} +6 -8
  88. package/src/lib/internal/timeout-fn.svelte.ts +6 -4
  89. package/src/lib/internal/tools/box.svelte.ts +5 -114
  90. package/src/lib/internal/tools/index.ts +1 -11
  91. package/src/lib/internal/tools/types.ts +0 -3
  92. package/src/lib/internal/typeahead.svelte.ts +9 -13
  93. package/src/lib/internal/types.ts +6 -0
  94. package/src/lib/internal/vaul/use-drawer-content.svelte.ts +2 -2
  95. package/src/lib/internal/vaul/use-drawer-root.svelte.ts +12 -7
  96. package/src/lib/components/accordion/accordion.svelte.ts +0 -43
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { bindableWith, boxWith } from '../../../internal/tools/index.js';
2
+ import { boxWith } from '../../../internal/tools/index.js';
3
3
  import { mergeProps } from '../../../internal/merge-props.js';
4
4
  import type { MenuCheckboxGroupProps } from '../types.js';
5
5
  import { MenuCheckboxGroupState } from '../menu.svelte.js';
@@ -18,10 +18,12 @@
18
18
  }: MenuCheckboxGroupProps = $props();
19
19
 
20
20
  const checkboxGroupState = MenuCheckboxGroupState.create({
21
- value: bindableWith(
21
+ value: boxWith(
22
22
  () => $state.snapshot(value),
23
- (v) => (value = $state.snapshot(v)),
24
- (v) => onValueChange(v),
23
+ (v) => {
24
+ value = $state.snapshot(v);
25
+ onValueChange(v);
26
+ },
25
27
  ),
26
28
  ref: boxWith(
27
29
  () => ref,
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { join } from 'overrule';
3
- import { bindableWith, boxWith, repairBindable } from '../../../internal/tools/index.js';
3
+ import { boxWith, repairBindable } from '../../../internal/tools/index.js';
4
4
  import { mergeProps } from '../../../internal/merge-props.js';
5
5
  import type { MenuCheckboxItemProps } from '../types.js';
6
6
  import { MenuCheckboxItemState } from '../menu.svelte.js';
@@ -26,10 +26,12 @@
26
26
  }: MenuCheckboxItemProps = $props();
27
27
 
28
28
  const checkboxItemState = MenuCheckboxItemState.create({
29
- checked: bindableWith(
29
+ checked: boxWith(
30
30
  () => checked,
31
- (v) => (checked = v),
32
- (v) => onCheckedChange(v),
31
+ (v) => {
32
+ checked = v;
33
+ onCheckedChange(v);
34
+ },
33
35
  ),
34
36
  id: boxWith(() => id),
35
37
  disabled: boxWith(() => disabled),
@@ -38,10 +40,12 @@
38
40
  () => ref,
39
41
  (v) => (ref = v),
40
42
  ),
41
- indeterminate: bindableWith(
43
+ indeterminate: boxWith(
42
44
  () => indeterminate,
43
- (v) => (indeterminate = v),
44
- (v) => onIndeterminateChange(v),
45
+ (v) => {
46
+ indeterminate = v;
47
+ onIndeterminateChange(v);
48
+ },
45
49
  ),
46
50
  value: boxWith(() => value),
47
51
  });
@@ -4,11 +4,11 @@ import {
4
4
  getWindow,
5
5
  type ReadableBoxedValues,
6
6
  type WritableBoxedValues,
7
- simpleBox,
8
7
  boxWith,
9
8
  type ReadableBox,
10
9
  } from '../../internal/tools/index.js';
11
10
  import { mergeProps } from '../../internal/merge-props.js';
11
+ import type { Measurable } from '../../internal/floating-layer/index.js';
12
12
  import { createContext, onDestroy, tick, untrack } from 'svelte';
13
13
  import { SUB_OPEN_KEYS, getCheckedState, isMouseEvent } from './utils.js';
14
14
  import { focusFirst } from '../../internal/focus.js';
@@ -35,7 +35,7 @@ import type { Direction } from '../../internal/index.js';
35
35
  import { getTabbableFrom, isTabbable } from '../../internal/tabbable.js';
36
36
  import type { KeyboardEventHandler, PointerEventHandler, MouseEventHandler } from 'svelte/elements';
37
37
  import { Typeahead, textContentOf } from '../../internal/typeahead.svelte.js';
38
- import { RovingFocusGroup } from '../../internal/roving-focus-group.js';
38
+ import { RovingFocusGroup } from '../../internal/roving-focus-group.svelte.js';
39
39
  import { useOpenChangeComplete } from '../../internal/animations-settled.svelte.js';
40
40
  import { joinGroup } from '../../internal/group-value.svelte.js';
41
41
  import { createEffectTimeout } from '../../internal/timeout-fn.svelte.js';
@@ -601,10 +601,10 @@ export class MenuContentState {
601
601
  getWindow: () => this.domContext.getWindow(),
602
602
  });
603
603
  this.rovingFocusGroup = new RovingFocusGroup({
604
- rootNode: boxWith(() => this.parentMenu.contentNode),
604
+ rootNode: () => this.parentMenu.contentNode,
605
605
  candidateSelector: this.#candidateSelector,
606
- loop: this.opts.loop,
607
- orientation: boxWith(() => 'vertical'),
606
+ loop: () => this.opts.loop.current,
607
+ orientation: () => 'vertical',
608
608
  });
609
609
 
610
610
  $effect(() => {
@@ -1139,7 +1139,26 @@ export class MenuCheckboxItemState {
1139
1139
  constructor(opts: MenuItemCombinedProps & MenuCheckboxItemStateOpts, shared: MenuItemSharedState) {
1140
1140
  this.opts = opts;
1141
1141
  this.item = new MenuItemState(opts, shared, { closes: false, act: () => this.#toggle() });
1142
- this.groupChecked = joinGroup(hasMenuCheckboxGroup() ? getMenuCheckboxGroup() : null, opts);
1142
+ // Accessor views over the boxed options until the menu engine converts.
1143
+ const group = hasMenuCheckboxGroup() ? getMenuCheckboxGroup() : null;
1144
+ this.groupChecked = joinGroup(
1145
+ group && {
1146
+ get value() {
1147
+ return group.opts.value.current;
1148
+ },
1149
+ set value(v) {
1150
+ group.opts.value.current = v;
1151
+ },
1152
+ },
1153
+ {
1154
+ get value() {
1155
+ return opts.value.current;
1156
+ },
1157
+ get checked() {
1158
+ return opts.checked.current;
1159
+ },
1160
+ },
1161
+ );
1143
1162
  }
1144
1163
 
1145
1164
  #toggle() {
@@ -1461,11 +1480,11 @@ export class ContextMenuTriggerState {
1461
1480
  readonly opts: ContextMenuTriggerStateOpts;
1462
1481
  readonly parentMenu: MenuMenuState;
1463
1482
  readonly attachment: RefAttachment;
1464
- #point = $state({ x: 0, y: 0 });
1465
-
1466
- virtualElement = simpleBox({
1483
+ #point = $state.raw({ x: 0, y: 0 });
1484
+ // One stable Measurable: readers that measure inside a derived track `#point` through it.
1485
+ readonly virtualElement: Measurable = {
1467
1486
  getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...this.#point }),
1468
- });
1487
+ };
1469
1488
  #longPressTimer = createEffectTimeout((e: BitsPointerEvent) => this.#handleOpen(e), () => 700);
1470
1489
 
1471
1490
  constructor(opts: ContextMenuTriggerStateOpts, parentMenu: MenuMenuState) {
@@ -1478,15 +1497,6 @@ export class ContextMenuTriggerState {
1478
1497
  this.onpointercancel = this.onpointercancel.bind(this);
1479
1498
  this.onpointerup = this.onpointerup.bind(this);
1480
1499
 
1481
- $effect(() => {
1482
- const point = this.#point;
1483
- untrack(() => {
1484
- this.virtualElement.current = {
1485
- getBoundingClientRect: () => DOMRect.fromRect({ width: 0, height: 0, ...point }),
1486
- };
1487
- });
1488
- });
1489
-
1490
1500
  $effect(() => {
1491
1501
  const isDisabled = this.opts.disabled.current;
1492
1502
  untrack(() => {
@@ -1,4 +1,4 @@
1
- import { type ReadableBox, boxWith, attachRef, type ReadableBoxedValues, type WritableBoxedValues } from '../../internal/tools/index.js';
1
+ import { type ReadableBox, attachRef, type ReadableBoxedValues, type WritableBoxedValues } from '../../internal/tools/index.js';
2
2
  import type { InteractOutsideBehaviorType } from '../../internal/dismissible-layer/types.js';
3
3
  import type { Direction } from '../../internal/index.js';
4
4
  import { createBitsAttrs, boolToStr, boolToEmptyStrOrUndef, getDataOpenClosed } from '../../internal/attrs.js';
@@ -7,7 +7,7 @@ import type { OnChangeFn, RefAttachment, WithRefOpts } from '../../internal/type
7
7
  import { createContext, onMount, tick, untrack } from 'svelte';
8
8
  import type { FocusEventHandler, KeyboardEventHandler, PointerEventHandler } from 'svelte/elements';
9
9
  import { getFloatingContentCSSVars } from '../../internal/floating-layer/index.js';
10
- import { RovingFocusGroup } from '../../internal/roving-focus-group.js';
10
+ import { RovingFocusGroup } from '../../internal/roving-focus-group.svelte.js';
11
11
  import { RovingFocusItem } from '../../internal/roving-focus-item.svelte.js';
12
12
 
13
13
  const menubarAttrs = createBitsAttrs({
@@ -45,10 +45,10 @@ export class MenubarRootState {
45
45
  this.opts = opts;
46
46
  this.attachment = attachRef(this.opts.ref);
47
47
  this.rovingFocusGroup = new RovingFocusGroup({
48
- rootNode: this.opts.ref,
48
+ rootNode: () => this.opts.ref.current,
49
49
  candidateAttr: menubarAttrs.trigger,
50
- loop: this.opts.loop,
51
- orientation: boxWith(() => 'horizontal'),
50
+ loop: () => this.opts.loop.current,
51
+ orientation: () => 'horizontal',
52
52
  });
53
53
  }
54
54
 
@@ -203,7 +203,8 @@ export class MenubarTriggerState {
203
203
  this.root = menu.root;
204
204
  this.rovingItem = new RovingFocusItem({
205
205
  group: this.root.rovingFocusGroup,
206
- ref: this.opts.ref,
206
+ ref: () => this.opts.ref.current,
207
+ setRef: (v) => (this.opts.ref.current = v),
207
208
  onRefChange: (v) => (this.menu.triggerNode = v),
208
209
  });
209
210
 
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { boxWith } from '../../../internal/tools/index.js';
3
2
  import { mergeProps } from '../../../internal/merge-props.js';
4
3
  import type { MeterRootProps } from '../types.js';
5
4
  import { MeterRootState } from '../meter.svelte.js';
@@ -19,14 +18,24 @@
19
18
  }: MeterRootProps = $props();
20
19
 
21
20
  const rootState = MeterRootState.create({
22
- value: boxWith(() => value),
23
- max: boxWith(() => max),
24
- min: boxWith(() => min),
25
- id: boxWith(() => id),
26
- ref: boxWith(
27
- () => ref,
28
- (v) => (ref = v),
29
- ),
21
+ get value() {
22
+ return value;
23
+ },
24
+ get max() {
25
+ return max;
26
+ },
27
+ get min() {
28
+ return min;
29
+ },
30
+ get id() {
31
+ return id;
32
+ },
33
+ get ref() {
34
+ return ref;
35
+ },
36
+ set ref(v) {
37
+ ref = v;
38
+ },
30
39
  });
31
40
 
32
41
  const mergedProps = $derived(mergeProps(restProps, rootState.props));
@@ -1,5 +1,5 @@
1
- import { attachRef, type ReadableBoxedValues } from '../../internal/tools/index.js';
2
- import type { RefAttachment, WithRefOpts } from '../../internal/types.js';
1
+ import { attachRef } from '../../internal/tools/index.js';
2
+ import type { RefAttachment, RefOpts } from '../../internal/types.js';
3
3
  import { createBitsAttrs } from '../../internal/attrs.js';
4
4
 
5
5
  const meterAttrs = createBitsAttrs({
@@ -7,14 +7,11 @@ const meterAttrs = createBitsAttrs({
7
7
  parts: ['root'],
8
8
  });
9
9
 
10
- interface MeterRootStateOpts
11
- extends
12
- WithRefOpts,
13
- ReadableBoxedValues<{
14
- value: number;
15
- max: number;
16
- min: number;
17
- }> {}
10
+ interface MeterRootStateOpts extends RefOpts {
11
+ readonly value: number;
12
+ readonly max: number;
13
+ readonly min: number;
14
+ }
18
15
 
19
16
  export class MeterRootState {
20
17
  static create(opts: MeterRootStateOpts) {
@@ -26,20 +23,20 @@ export class MeterRootState {
26
23
 
27
24
  constructor(opts: MeterRootStateOpts) {
28
25
  this.opts = opts;
29
- this.attachment = attachRef(this.opts.ref);
26
+ this.attachment = attachRef<HTMLElement>((v) => (opts.ref = v));
30
27
  }
31
28
 
32
29
  readonly props = $derived.by(
33
30
  () =>
34
31
  ({
35
32
  role: 'meter',
36
- value: this.opts.value.current,
37
- 'aria-valuemin': this.opts.min.current,
38
- 'aria-valuemax': this.opts.max.current,
39
- 'aria-valuenow': this.opts.value.current,
40
- 'data-value': this.opts.value.current,
41
- 'data-max': this.opts.max.current,
42
- 'data-min': this.opts.min.current,
33
+ value: this.opts.value,
34
+ 'aria-valuemin': this.opts.min,
35
+ 'aria-valuemax': this.opts.max,
36
+ 'aria-valuenow': this.opts.value,
37
+ 'data-value': this.opts.value,
38
+ 'data-max': this.opts.max,
39
+ 'data-min': this.opts.min,
43
40
  [meterAttrs.root]: '',
44
41
  ...this.attachment,
45
42
  }) as const,
@@ -53,6 +53,6 @@
53
53
  );
54
54
  </script>
55
55
 
56
- <Portal to={contentState.context.viewportRef.current || undefined} disabled={!contentState.context.viewportRef.current}>
56
+ <Portal to={contentState.context.viewportRef || undefined} disabled={!contentState.context.viewportRef}>
57
57
  <NavigationMenuContentImpl {...mergedProps} {children} {child} />
58
58
  </Portal>
@@ -41,8 +41,8 @@
41
41
  );
42
42
  </script>
43
43
 
44
- {#if indicatorState.context.indicatorTrackRef.current}
45
- <Portal to={indicatorState.context.indicatorTrackRef.current}>
44
+ {#if indicatorState.context.indicatorTrackRef}
45
+ <Portal to={indicatorState.context.indicatorTrackRef}>
46
46
  <NavigationMenuIndicatorImpl {...mergedProps} {id} bind:ref {child}>
47
47
  {#snippet children()}
48
48
  {#if children}
@@ -74,7 +74,7 @@
74
74
  <!-- focus proxy: visually hidden (the sr-only utility) but focusable, for nav-menu focus management -->
75
75
  <span {...mergeProps(triggerState.focusProxyProps, mounted, { class: 'sr-only' })}></span>
76
76
 
77
- {#if triggerState.context.viewportRef.current}
77
+ {#if triggerState.context.viewportRef}
78
78
  <span aria-owns={triggerState.itemContext.contentId ?? undefined}></span>
79
79
  {/if}
80
80
  {/if}
@@ -4,20 +4,16 @@
4
4
  */
5
5
  import {
6
6
  type AnyFn,
7
- type ReadableBox,
8
7
  type ReadableBoxedValues,
9
8
  type WithRefProps,
10
- type WritableBox,
11
9
  type WritableBoxedValues,
12
10
  attachRef,
13
- boxAutoReset,
14
11
  DOMContext,
15
12
  getWindow,
16
- simpleBox,
17
13
  boxWith,
18
14
  } from '../../internal/tools/index.js';
19
15
  import { createEffectTimeout } from '../../internal/timeout-fn.svelte.js';
20
- import { createContext, tick, untrack, type Snippet } from 'svelte';
16
+ import { createContext, tick, untrack } from 'svelte';
21
17
  import { SvelteMap } from 'svelte/reactivity';
22
18
  import { type Direction, type Orientation } from '../../internal/index.js';
23
19
  import { createId } from '../../internal/create-id.js';
@@ -29,7 +25,7 @@ import { kbd } from '../../internal/kbd.js';
29
25
  import { on } from 'svelte/events';
30
26
  import { isElement } from '../../internal/tools/utils/dom.js';
31
27
  import type { FocusEventHandler, KeyboardEventHandler, MouseEventHandler, PointerEventHandler } from 'svelte/elements';
32
- import { RovingFocusGroup } from '../../internal/roving-focus-group.js';
28
+ import { RovingFocusGroup } from '../../internal/roving-focus-group.svelte.js';
33
29
  import { observeResizeMany } from '../../internal/svelte-resize-observer.svelte.js';
34
30
 
35
31
  const navigationMenuAttrs = createBitsAttrs({
@@ -79,8 +75,8 @@ class NavigationMenuProviderState {
79
75
  return setNavigationMenuProvider(new NavigationMenuProviderState(opts));
80
76
  }
81
77
  readonly opts: NavigationMenuProviderStateOpts;
82
- indicatorTrackRef = simpleBox<HTMLElement | null>(null);
83
- viewportRef = simpleBox<HTMLElement | null>(null);
78
+ indicatorTrackRef = $state.raw<HTMLElement | null>(null);
79
+ viewportRef = $state.raw<HTMLElement | null>(null);
84
80
  viewportContent = new SvelteMap<string, NavigationMenuItemState>();
85
81
  onTriggerEnter: NavigationMenuProviderStateOpts['onTriggerEnter'];
86
82
  onTriggerLeave: () => void = () => {};
@@ -126,29 +122,27 @@ export class NavigationMenuRootState {
126
122
  readonly opts: NavigationMenuRootStateOpts;
127
123
  readonly attachment: RefAttachment;
128
124
  provider: NavigationMenuProviderState;
129
- previousValue = simpleBox('');
130
- isDelaySkipped: WritableBox<boolean>;
125
+ previousValue = $state.raw('');
126
+ // False from an open until skipDelayDuration after the close: a hover in that window skips delayDuration.
127
+ #isOpenDelayed = $state(true);
128
+ readonly #skipDelayTimer: ReturnType<typeof createEffectTimeout<() => void>>;
131
129
  readonly #derivedDelay = $derived.by(() => {
132
130
  const isOpen = this.opts?.value?.current !== '';
133
- if (isOpen || this.isDelaySkipped.current) {
134
- // 150 for user to switch trigger or move into content view
135
- return 150;
136
- } else {
137
- return this.opts.delayDuration.current;
138
- }
131
+ // 150ms while open or in the skip window: room to switch trigger or move into the content.
132
+ return isOpen || !this.#isOpenDelayed ? 150 : this.opts.delayDuration.current;
139
133
  });
140
134
 
141
135
  constructor(opts: NavigationMenuRootStateOpts) {
142
136
  this.opts = opts;
143
137
  this.attachment = attachRef(this.opts.ref);
144
- this.isDelaySkipped = boxAutoReset(false, {
145
- afterMs: this.opts.skipDelayDuration.current,
146
- getWindow: () => getWindow(opts.ref.current),
147
- });
138
+ this.#skipDelayTimer = createEffectTimeout(
139
+ () => (this.#isOpenDelayed = true),
140
+ () => this.opts.skipDelayDuration.current,
141
+ );
148
142
 
149
143
  this.provider = NavigationMenuProviderState.create({
150
144
  value: this.opts.value,
151
- previousValue: this.previousValue,
145
+ previousValue: boxWith(() => this.previousValue, (v) => (this.previousValue = v)),
152
146
  dir: this.opts.dir,
153
147
  orientation: this.opts.orientation,
154
148
  rootNavigationMenuRef: this.opts.ref,
@@ -179,7 +173,6 @@ export class NavigationMenuRootState {
179
173
  };
180
174
 
181
175
  #onTriggerLeave = () => {
182
- this.isDelaySkipped.current = false;
183
176
  this.#debouncedFn.start('', null);
184
177
  };
185
178
 
@@ -203,14 +196,19 @@ export class NavigationMenuRootState {
203
196
  };
204
197
 
205
198
  setValue = (newValue: string, itemState: NavigationMenuItemState | null) => {
206
- this.previousValue.current = this.opts.value.current;
199
+ this.previousValue = this.opts.value.current;
207
200
  this.opts.value.current = newValue;
208
201
  this.provider.setActiveItem(itemState);
209
202
 
210
203
  // When all menus are closed, we want to reset previousValue to prevent
211
204
  // weird transitions from old positions when opening fresh
212
205
  if (newValue === '') {
213
- this.previousValue.current = '';
206
+ this.previousValue = '';
207
+ if (this.opts.skipDelayDuration.current === 0) this.#isOpenDelayed = true;
208
+ else this.#skipDelayTimer.start();
209
+ } else {
210
+ this.#skipDelayTimer.stop();
211
+ this.#isOpenDelayed = false;
214
212
  }
215
213
  };
216
214
 
@@ -243,7 +241,7 @@ export class NavigationMenuSubState {
243
241
  }
244
242
  readonly opts: NavigationMenuSubStateOpts;
245
243
  readonly context: NavigationMenuProviderState;
246
- previousValue = simpleBox('');
244
+ previousValue = $state.raw('');
247
245
  readonly subProvider: NavigationMenuProviderState;
248
246
  readonly attachment: RefAttachment;
249
247
 
@@ -261,19 +259,19 @@ export class NavigationMenuSubState {
261
259
  onTriggerEnter: this.setValue,
262
260
  onItemSelect: this.setValue,
263
261
  onItemDismiss: () => this.setValue('', null),
264
- previousValue: this.previousValue,
262
+ previousValue: boxWith(() => this.previousValue, (v) => (this.previousValue = v)),
265
263
  });
266
264
  }
267
265
 
268
266
  setValue = (newValue: string, itemState: NavigationMenuItemState | null) => {
269
- this.previousValue.current = this.opts.value.current;
267
+ this.previousValue = this.opts.value.current;
270
268
  this.opts.value.current = newValue;
271
269
  this.subProvider.setActiveItem(itemState);
272
270
 
273
271
  // When all menus are closed, we want to reset previousValue to prevent
274
272
  // weird transitions from old positions when opening fresh
275
273
  if (newValue === '') {
276
- this.previousValue.current = '';
274
+ this.previousValue = '';
277
275
  }
278
276
  };
279
277
 
@@ -295,12 +293,11 @@ export class NavigationMenuListState {
295
293
  static create(opts: NavigationMenuListStateOpts) {
296
294
  return setNavigationMenuList(new NavigationMenuListState(opts, getNavigationMenuProvider()));
297
295
  }
298
- wrapperId = simpleBox('');
299
- wrapperRef = simpleBox<HTMLElement | null>(null);
296
+ readonly wrapperId: string;
300
297
  readonly opts: NavigationMenuListStateOpts;
301
298
  readonly context: NavigationMenuProviderState;
302
299
  readonly attachment: RefAttachment;
303
- readonly wrapperAttachment: RefAttachment = attachRef(this.wrapperRef, (v) => (this.context.indicatorTrackRef.current = v));
300
+ readonly wrapperAttachment: RefAttachment = attachRef((v) => (this.context.indicatorTrackRef = v));
304
301
  listTriggers = $state.raw<HTMLElement[]>([]);
305
302
  readonly rovingFocusGroup: RovingFocusGroup;
306
303
  wrapperMounted = $state(false);
@@ -308,13 +305,13 @@ export class NavigationMenuListState {
308
305
  constructor(opts: NavigationMenuListStateOpts, context: NavigationMenuProviderState) {
309
306
  this.opts = opts;
310
307
  this.context = context;
311
- this.wrapperId.current = createId('wrapper', opts.id.current);
308
+ this.wrapperId = createId('wrapper', opts.id.current);
312
309
  this.attachment = attachRef(this.opts.ref);
313
310
  this.rovingFocusGroup = new RovingFocusGroup({
314
- rootNode: opts.ref,
311
+ rootNode: () => opts.ref.current,
315
312
  candidateSelector: `${navigationMenuAttrs.selector('trigger')}:not([data-disabled]), ${navigationMenuAttrs.selector('link')}:not([data-disabled])`,
316
- loop: boxWith(() => false),
317
- orientation: this.context.opts.orientation,
313
+ loop: () => false,
314
+ orientation: () => this.context.opts.orientation.current,
318
315
  });
319
316
  }
320
317
 
@@ -328,7 +325,7 @@ export class NavigationMenuListState {
328
325
  readonly wrapperProps = $derived.by(
329
326
  () =>
330
327
  ({
331
- id: this.wrapperId.current,
328
+ id: this.wrapperId,
332
329
  ...this.wrapperAttachment,
333
330
  }) as const,
334
331
  );
@@ -366,9 +363,6 @@ export class NavigationMenuItemState {
366
363
  wasEscapeClose = false;
367
364
  readonly contentId = $derived.by(() => this.contentNode?.id);
368
365
  readonly triggerId = $derived.by(() => this.triggerNode?.id);
369
- contentChildren: ReadableBox<Snippet | undefined> = simpleBox(undefined);
370
- contentChild: ReadableBox<Snippet<[{ props: Record<string, unknown> }]> | undefined> = simpleBox(undefined);
371
- contentProps: ReadableBox<Record<string, unknown>> = simpleBox({});
372
366
  domContext: DOMContext;
373
367
  constructor(opts: NavigationMenuItemStateOpts, listContext: NavigationMenuListState) {
374
368
  this.opts = opts;
@@ -423,13 +417,13 @@ export class NavigationMenuTriggerState {
423
417
  }
424
418
  readonly opts: NavigationMenuTriggerStateOpts;
425
419
  readonly attachment: RefAttachment;
426
- focusProxyId = simpleBox('');
427
- focusProxyRef = simpleBox<HTMLElement | null>(null);
428
- readonly focusProxyAttachment: RefAttachment = attachRef(this.focusProxyRef, (v) => (this.itemContext.focusProxyNode = v));
420
+ readonly focusProxyId: string;
421
+ readonly focusProxyAttachment: RefAttachment = attachRef((v) => (this.itemContext.focusProxyNode = v));
429
422
  context: NavigationMenuProviderState;
430
423
  itemContext: NavigationMenuItemState;
431
424
  listContext: NavigationMenuListState;
432
- hasPointerMoveOpened = simpleBox(false);
425
+ #hasPointerMoveOpened = false;
426
+ readonly #pointerMoveResetTimer: ReturnType<typeof createEffectTimeout<() => void>>;
433
427
  wasClickClose = false;
434
428
  focusProxyMounted = $state(false);
435
429
  readonly open = $derived.by(() => this.itemContext.opts.value.current === this.context.opts.value.current);
@@ -444,12 +438,13 @@ export class NavigationMenuTriggerState {
444
438
  },
445
439
  ) {
446
440
  this.opts = opts;
447
- this.focusProxyId.current = createId('focus-proxy', opts.id.current);
441
+ this.focusProxyId = createId('focus-proxy', opts.id.current);
448
442
  this.attachment = attachRef(this.opts.ref, (v) => (this.itemContext.triggerNode = v));
449
- this.hasPointerMoveOpened = boxAutoReset(false, {
450
- afterMs: 300,
451
- getWindow: () => getWindow(opts.ref.current),
452
- });
443
+ this.#pointerMoveResetTimer = createEffectTimeout(
444
+ () => (this.#hasPointerMoveOpened = false),
445
+ () => 300,
446
+ () => getWindow(opts.ref.current),
447
+ );
453
448
  this.context = context.provider;
454
449
  this.itemContext = context.item;
455
450
  this.listContext = context.list;
@@ -474,24 +469,26 @@ export class NavigationMenuTriggerState {
474
469
  this.opts.disabled.current ||
475
470
  this.wasClickClose ||
476
471
  this.itemContext.wasEscapeClose ||
477
- this.hasPointerMoveOpened.current ||
472
+ this.#hasPointerMoveOpened ||
478
473
  !this.itemContext.opts.openOnHover.current
479
474
  ) {
480
475
  return;
481
476
  }
482
477
  this.context.onTriggerEnter(this.itemContext.opts.value.current, this.itemContext);
483
- this.hasPointerMoveOpened.current = true;
478
+ this.#hasPointerMoveOpened = true;
479
+ this.#pointerMoveResetTimer.start();
484
480
  });
485
481
 
486
482
  onpointerleave = whenMouse(() => {
487
483
  if (this.opts.disabled.current || !this.itemContext.opts.openOnHover.current) return;
488
484
  this.context.onTriggerLeave();
489
- this.hasPointerMoveOpened.current = false;
485
+ this.#hasPointerMoveOpened = false;
486
+ this.#pointerMoveResetTimer.stop();
490
487
  });
491
488
 
492
489
  onclick: MouseEventHandler<HTMLButtonElement> = () => {
493
490
  // if opened via pointer move, we prevent the click event
494
- if (this.hasPointerMoveOpened.current) return;
491
+ if (this.#hasPointerMoveOpened) return;
495
492
  const shouldClose = this.open && (!this.itemContext.opts.openOnHover.current || this.context.opts.isRootMenu);
496
493
  if (shouldClose) {
497
494
  this.context.onItemSelect('', null);
@@ -547,7 +544,7 @@ export class NavigationMenuTriggerState {
547
544
  readonly focusProxyProps = $derived.by(
548
545
  () =>
549
546
  ({
550
- id: this.focusProxyId.current,
547
+ id: this.focusProxyId,
551
548
  tabindex: 0,
552
549
  onfocus: this.focusProxyOnFocus,
553
550
  ...this.focusProxyAttachment,
@@ -692,7 +689,7 @@ export class NavigationMenuIndicatorImplState {
692
689
  this.listContext = context.list;
693
690
  this.attachment = attachRef(this.opts.ref);
694
691
 
695
- observeResizeMany(() => [this.activeTrigger, this.context.indicatorTrackRef.current], this.handlePositionChange);
692
+ observeResizeMany(() => [this.activeTrigger, this.context.indicatorTrackRef], this.handlePositionChange);
696
693
  }
697
694
 
698
695
  handlePositionChange = () => {
@@ -846,10 +843,10 @@ export class NavigationMenuContentImplState {
846
843
  this.context = itemContext.listContext.context;
847
844
  this.domContext = new DOMContext(opts.ref);
848
845
  this.rovingFocusGroup = new RovingFocusGroup({
849
- rootNode: this.opts.ref,
846
+ rootNode: () => this.opts.ref.current,
850
847
  candidateNodes: getTabbableCandidates,
851
- loop: boxWith(() => false),
852
- orientation: this.context.opts.orientation,
848
+ loop: () => false,
849
+ orientation: () => this.context.opts.orientation.current,
853
850
  });
854
851
 
855
852
  $effect(() => {
@@ -886,7 +883,7 @@ export class NavigationMenuContentImplState {
886
883
  onInteractOutside = (e: PointerEvent) => {
887
884
  const target = e.target as HTMLElement;
888
885
  const isTrigger = this.listContext.listTriggers.some((trigger) => trigger.contains(target));
889
- const isRootViewport = this.context.opts.isRootMenu && this.context.viewportRef.current?.contains(target);
886
+ const isRootViewport = this.context.opts.isRootMenu && this.context.viewportRef?.contains(target);
890
887
  if (!this.context.opts.isRootMenu && !isTrigger) {
891
888
  this.context.onItemDismiss();
892
889
  return;
@@ -988,14 +985,14 @@ export class NavigationMenuViewportState {
988
985
  constructor(opts: NavigationMenuViewportStateOpts, context: NavigationMenuProviderState) {
989
986
  this.opts = opts;
990
987
  this.context = context;
991
- this.attachment = attachRef(this.opts.ref, (v) => (this.context.viewportRef.current = v));
988
+ this.attachment = attachRef(this.opts.ref, (v) => (this.context.viewportRef = v));
992
989
 
993
990
  $effect(() => {
994
991
  const _activeContentValue = this.activeContentValue;
995
992
  const _open = this.open;
996
993
  untrack(() => {
997
994
  tick().then(() => {
998
- const currNode = this.context.viewportRef.current;
995
+ const currNode = this.context.viewportRef;
999
996
  if (!currNode) return;
1000
997
  const el = (currNode.querySelector<HTMLElement>('[data-state=open]')?.children?.[0] as HTMLElement | null) ?? null;
1001
998