@dryui/ui 1.3.0 → 1.4.0

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 (93) hide show
  1. package/dist/accordion/accordion-content.svelte +1 -1
  2. package/dist/alert/alert.svelte +1 -1
  3. package/dist/app-frame/app-frame.svelte +125 -0
  4. package/dist/app-frame/app-frame.svelte.d.ts +10 -0
  5. package/dist/app-frame/index.d.ts +8 -0
  6. package/dist/app-frame/index.js +1 -0
  7. package/dist/aurora/aurora.svelte +22 -59
  8. package/dist/beam/beam.svelte +28 -9
  9. package/dist/carousel/carousel-button-dots.svelte +25 -8
  10. package/dist/carousel/carousel-button-thumbnails.svelte +25 -8
  11. package/dist/carousel/carousel-root.svelte +115 -4
  12. package/dist/carousel/carousel-slide.svelte +5 -1
  13. package/dist/carousel/carousel-viewport.svelte +2 -0
  14. package/dist/carousel/context.svelte.d.ts +5 -0
  15. package/dist/chart/chart-bars.svelte +25 -16
  16. package/dist/chart/chart-donut.svelte +25 -16
  17. package/dist/chart/chart-root.svelte +134 -30
  18. package/dist/chart/chart-root.svelte.d.ts +1 -0
  19. package/dist/chart/context.svelte.d.ts +3 -1
  20. package/dist/chart/context.svelte.js +1 -0
  21. package/dist/chart/index.d.ts +1 -0
  22. package/dist/chromatic-shift/chromatic-shift.svelte +36 -9
  23. package/dist/collapsible/collapsible-content.svelte +2 -1
  24. package/dist/combobox/combobox-content.svelte +26 -44
  25. package/dist/combobox/combobox-content.svelte.d.ts +1 -1
  26. package/dist/combobox/combobox-input-root.svelte +7 -1
  27. package/dist/combobox/combobox-input.svelte +21 -8
  28. package/dist/country-select/country-select-button-input.svelte +124 -260
  29. package/dist/date-picker/datepicker-content.svelte +18 -26
  30. package/dist/date-picker/datepicker-content.svelte.d.ts +2 -1
  31. package/dist/date-picker/datepicker-input-root.svelte +7 -1
  32. package/dist/date-range-picker/date-range-picker-content.svelte +18 -14
  33. package/dist/date-range-picker/date-range-picker-content.svelte.d.ts +2 -1
  34. package/dist/date-range-picker/date-range-picker-root.svelte +7 -1
  35. package/dist/displacement/displacement.svelte +16 -22
  36. package/dist/drag-and-drop/context.svelte.d.ts +2 -0
  37. package/dist/drag-and-drop/drag-and-drop-handle.svelte +34 -5
  38. package/dist/drag-and-drop/drag-and-drop-item.svelte +23 -14
  39. package/dist/drag-and-drop/drag-and-drop-root.svelte +60 -16
  40. package/dist/god-rays/god-rays.svelte +11 -0
  41. package/dist/gradient-mesh/gradient-mesh.svelte +27 -5
  42. package/dist/hover-card/context.svelte.d.ts +1 -10
  43. package/dist/hover-card/context.svelte.js +1 -2
  44. package/dist/hover-card/hover-card-content.svelte +41 -3
  45. package/dist/hover-card/hover-card-root.svelte +7 -55
  46. package/dist/hover-card/hover-card-trigger.svelte +79 -40
  47. package/dist/hover-card/hover-card-trigger.svelte.d.ts +1 -1
  48. package/dist/index.d.ts +2 -0
  49. package/dist/index.js +1 -0
  50. package/dist/internal/motion.d.ts +1 -1
  51. package/dist/internal/motion.js +1 -1
  52. package/dist/marquee/marquee.svelte +56 -8
  53. package/dist/mega-menu/context.svelte.d.ts +2 -1
  54. package/dist/mega-menu/mega-menu-button-trigger.svelte +2 -14
  55. package/dist/mega-menu/mega-menu-item.svelte +3 -1
  56. package/dist/mega-menu/mega-menu-panel.svelte +35 -3
  57. package/dist/mega-menu/mega-menu-root.svelte +28 -13
  58. package/dist/menubar/context.svelte.d.ts +2 -2
  59. package/dist/menubar/menubar-button-trigger.svelte +5 -3
  60. package/dist/menubar/menubar-content.svelte +20 -12
  61. package/dist/menubar/menubar-root.svelte +4 -4
  62. package/dist/multi-select-combobox/multi-select-combobox-content.svelte +18 -55
  63. package/dist/multi-select-combobox/multi-select-combobox-content.svelte.d.ts +1 -1
  64. package/dist/noise/noise.svelte +38 -6
  65. package/dist/notification-center/context.svelte.d.ts +0 -1
  66. package/dist/notification-center/notification-center-panel.svelte +54 -35
  67. package/dist/notification-center/notification-center-root.svelte +0 -1
  68. package/dist/notification-center/notification-center-trigger-button.svelte +1 -8
  69. package/dist/option-picker/option-picker-description.svelte +2 -2
  70. package/dist/option-picker/option-picker-item.svelte +10 -3
  71. package/dist/option-picker/option-picker-label.svelte +2 -2
  72. package/dist/option-picker/option-picker-preview.svelte +18 -13
  73. package/dist/phone-input/phone-input-select.svelte +2 -152
  74. package/dist/phone-input/phone-input-select.svelte.d.ts +1 -7
  75. package/dist/rich-text-editor/rich-text-editor-toolbar-button-input.svelte +84 -29
  76. package/dist/scroll-area/scroll-area.svelte +16 -4
  77. package/dist/select/select-content.svelte +21 -31
  78. package/dist/select/select-content.svelte.d.ts +1 -1
  79. package/dist/select/select-root-input.svelte +7 -1
  80. package/dist/shimmer/shimmer.svelte +22 -12
  81. package/dist/tabs/tabs-list.svelte +12 -0
  82. package/dist/transfer/transfer-item.svelte +0 -3
  83. package/dist/transfer/transfer-list-input.svelte +1 -6
  84. package/dist/tree/context.svelte.d.ts +7 -1
  85. package/dist/tree/tree-item-children.svelte +12 -10
  86. package/dist/tree/tree-item-label.svelte +6 -17
  87. package/dist/tree/tree-item-label.svelte.d.ts +2 -2
  88. package/dist/tree/tree-item.svelte +28 -1
  89. package/dist/tree/tree-root.svelte +135 -59
  90. package/dist/typography/heading.svelte +1 -0
  91. package/package.json +8 -2
  92. package/skills/dryui/SKILL.md +1 -0
  93. package/dist/hover-card/hover-card-root.svelte.d.ts +0 -9
@@ -1,9 +1,10 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
+ import { Button } from '../button/index.js';
4
5
  import { getHoverCardCtx } from './context.svelte.js';
5
6
 
6
- interface Props extends HTMLAttributes<HTMLAnchorElement> {
7
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
7
8
  href?: string;
8
9
  children: Snippet;
9
10
  }
@@ -12,57 +13,95 @@
12
13
 
13
14
  const ctx = getHoverCardCtx();
14
15
 
15
- let anchorEl = $state<HTMLAnchorElement>();
16
+ let triggerEl = $state<HTMLAnchorElement | HTMLButtonElement>();
16
17
 
17
18
  $effect(() => {
18
- if (anchorEl) {
19
- ctx.triggerEl = anchorEl;
19
+ if (triggerEl) {
20
+ ctx.triggerEl = triggerEl;
21
+
22
+ return () => {
23
+ if (ctx.triggerEl === triggerEl) {
24
+ ctx.triggerEl = null;
25
+ }
26
+ };
20
27
  }
21
28
  });
29
+
30
+ function handlePointerEnter() {
31
+ ctx.triggerHovered = true;
32
+ ctx.show();
33
+ }
34
+
35
+ function handlePointerLeave() {
36
+ ctx.triggerHovered = false;
37
+ ctx.close();
38
+ }
39
+
40
+ function handleFocusIn() {
41
+ ctx.triggerFocused = true;
42
+
43
+ if (ctx.ignoreNextTriggerFocusOpen) {
44
+ ctx.ignoreNextTriggerFocusOpen = false;
45
+ return;
46
+ }
47
+
48
+ ctx.showImmediate();
49
+ }
50
+
51
+ function handleFocusOut() {
52
+ ctx.triggerFocused = false;
53
+ ctx.close();
54
+ }
55
+
56
+ function handleKeydown(event: KeyboardEvent) {
57
+ if (event.key === 'Escape' && ctx.open) {
58
+ event.preventDefault();
59
+ ctx.forceClose();
60
+ triggerEl?.focus();
61
+ }
62
+ }
63
+
64
+ function handleRef(el: HTMLButtonElement | HTMLAnchorElement | null) {
65
+ triggerEl = el ?? undefined;
66
+ }
22
67
  </script>
23
68
 
24
- <a
25
- bind:this={anchorEl}
26
- id={ctx.triggerId}
27
- {href}
28
- data-hover-card-trigger
29
- aria-haspopup="true"
30
- aria-expanded={ctx.open}
31
- data-state={ctx.open ? 'open' : 'closed'}
32
- onpointerenter={() => ctx.show()}
33
- onpointerleave={() => ctx.close()}
34
- onfocus={() => ctx.show()}
35
- onblur={() => ctx.close()}
36
- class={className}
37
- {...rest}
38
- >
39
- {@render children()}
40
- </a>
69
+ <span class={className} data-hover-card-trigger-shell>
70
+ <Button
71
+ ref={handleRef}
72
+ variant="link"
73
+ size="sm"
74
+ color="primary"
75
+ id={ctx.triggerId}
76
+ {href}
77
+ data-hover-card-trigger
78
+ aria-haspopup="dialog"
79
+ aria-controls={ctx.contentId}
80
+ aria-expanded={ctx.open}
81
+ data-state={ctx.open ? 'open' : 'closed'}
82
+ onpointerenter={handlePointerEnter}
83
+ onpointerleave={handlePointerLeave}
84
+ onfocusin={handleFocusIn}
85
+ onfocusout={handleFocusOut}
86
+ onkeydown={handleKeydown}
87
+ {...rest}
88
+ >
89
+ {@render children()}
90
+ </Button>
91
+ </span>
41
92
 
42
93
  <style>
43
- [data-hover-card-trigger] {
94
+ [data-hover-card-trigger-shell] {
95
+ --dry-btn-min-height: auto;
96
+ --dry-btn-padding-x: 0;
97
+ --dry-btn-padding-y: 0;
98
+ --dry-btn-font-size: inherit;
99
+ --dry-btn-radius: var(--dry-radius-sm);
44
100
  display: inline-grid;
45
- grid-auto-flow: column;
46
- grid-auto-columns: max-content;
47
- align-items: center;
48
- gap: var(--dry-space-1);
49
- color: var(--dry-color-text-brand);
50
- text-decoration: underline;
51
- text-decoration-color: var(--dry-color-stroke-brand);
52
- text-decoration-thickness: 1px;
53
- text-underline-offset: 0.18em;
54
101
  cursor: help;
55
- transition:
56
- color var(--dry-duration-fast) var(--dry-ease-default),
57
- text-decoration-color var(--dry-duration-fast) var(--dry-ease-default);
58
- }
59
-
60
- [data-hover-card-trigger]:hover {
61
- color: var(--dry-color-fill-brand-hover);
62
- text-decoration-color: currentColor;
63
102
  }
64
103
 
65
- [data-hover-card-trigger]:focus-visible {
104
+ [data-hover-card-trigger-shell]:focus-within {
66
105
  outline: 2px solid var(--dry-color-stroke-focus);
67
106
  outline-offset: 3px;
68
107
  border-radius: var(--dry-radius-sm);
@@ -1,6 +1,6 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
- interface Props extends HTMLAttributes<HTMLAnchorElement> {
3
+ interface Props extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
4
4
  href?: string;
5
5
  children: Snippet;
6
6
  }
package/dist/index.d.ts CHANGED
@@ -98,6 +98,8 @@ export type { BadgeProps, BadgeColor } from './badge/index.js';
98
98
  export { Chart } from './chart/index.js';
99
99
  export type { ChartDataPoint, ChartStackedDataPoint } from './chart/index.js';
100
100
  export type { ChartRootProps, ChartBarsProps, ChartLineProps, ChartAreaProps, ChartDonutProps, ChartStackedBarProps, ChartHorizontalBarProps, ChartXAxisProps, ChartYAxisProps } from './chart/index.js';
101
+ export { AppFrame } from './app-frame/index.js';
102
+ export type { AppFrameProps } from './app-frame/index.js';
101
103
  export { ChatThread } from './chat-thread/index.js';
102
104
  export type { ChatThreadProps } from './chat-thread/index.js';
103
105
  export { TypingIndicator } from './typing-indicator/index.js';
package/dist/index.js CHANGED
@@ -50,6 +50,7 @@ export { Table } from './table/index.js';
50
50
  export { Avatar } from './avatar/index.js';
51
51
  export { Badge } from './badge/index.js';
52
52
  export { Chart } from './chart/index.js';
53
+ export { AppFrame } from './app-frame/index.js';
53
54
  export { ChatThread } from './chat-thread/index.js';
54
55
  export { TypingIndicator } from './typing-indicator/index.js';
55
56
  export { Icon } from './icon/index.js';
@@ -1,4 +1,4 @@
1
- export { getReducedMotionPreference, observeReducedMotionPreference, supportsIntersectionObservers, supportsPointerTracking, supportsPropertyRegistration, registerPropertyOnce, supportsWebGL2 } from '@dryui/primitives/internal/motion';
1
+ export { getReducedMotionPreference, observeReducedMotionPreference, supportsIntersectionObservers, supportsPointerTracking, supportsPropertyRegistration, registerPropertyOnce, supportsWebGL2, observeInViewport, observePageVisibility, observeOffscreenState } from '@dryui/primitives/internal/motion';
2
2
  export declare function supportsViewTransitions(): boolean;
3
3
  export declare function supportsScrollTimelines(): boolean;
4
4
  export declare function extractThemeColor(property: string, element?: HTMLElement): [number, number, number];
@@ -1,5 +1,5 @@
1
1
  // Re-export shared motion utilities from primitives (single source of truth)
2
- export { getReducedMotionPreference, observeReducedMotionPreference, supportsIntersectionObservers, supportsPointerTracking, supportsPropertyRegistration, registerPropertyOnce, supportsWebGL2 } from '@dryui/primitives/internal/motion';
2
+ export { getReducedMotionPreference, observeReducedMotionPreference, supportsIntersectionObservers, supportsPointerTracking, supportsPropertyRegistration, registerPropertyOnce, supportsWebGL2, observeInViewport, observePageVisibility, observeOffscreenState } from '@dryui/primitives/internal/motion';
3
3
  // UI-only motion utilities
4
4
  export function supportsViewTransitions() {
5
5
  return typeof document !== 'undefined' && 'startViewTransition' in document;
@@ -2,7 +2,11 @@
2
2
  import { onMount } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import type { HTMLAttributes } from 'svelte/elements';
5
- import { observeReducedMotionPreference } from '@dryui/primitives/internal/motion';
5
+ import {
6
+ observeInViewport,
7
+ observePageVisibility,
8
+ observeReducedMotionPreference
9
+ } from '@dryui/primitives/internal/motion';
6
10
 
7
11
  interface Props extends HTMLAttributes<HTMLDivElement> {
8
12
  speed?: number;
@@ -24,18 +28,40 @@
24
28
  ...rest
25
29
  }: Props = $props();
26
30
 
31
+ let rootEl: HTMLDivElement | undefined = $state();
27
32
  let contentEl: HTMLDivElement | undefined = $state();
28
33
  let contentSize = $state(0);
29
34
  let prefersReducedMotion = $state(false);
35
+ let onScreen = $state(true);
36
+ let tabVisible = $state(true);
30
37
 
31
38
  const isVertical = $derived(direction === 'up' || direction === 'down');
32
39
  const duration = $derived(contentSize > 0 && speed > 0 ? contentSize / speed : 0);
40
+ const paused = $derived(!onScreen || !tabVisible);
33
41
 
34
- onMount(() =>
35
- observeReducedMotionPreference((matches) => {
42
+ onMount(() => {
43
+ const unsubscribeMotion = observeReducedMotionPreference((matches) => {
36
44
  prefersReducedMotion = matches;
37
- })
38
- );
45
+ });
46
+ const unsubscribeVisibility = observePageVisibility((visible) => {
47
+ tabVisible = visible;
48
+ });
49
+ return () => {
50
+ unsubscribeMotion();
51
+ unsubscribeVisibility();
52
+ };
53
+ });
54
+
55
+ $effect(() => {
56
+ if (!rootEl) return;
57
+ return observeInViewport(
58
+ rootEl,
59
+ (inView) => {
60
+ onScreen = inView;
61
+ },
62
+ { rootMargin: '200px' }
63
+ );
64
+ });
39
65
 
40
66
  $effect(() => {
41
67
  if (!contentEl) return;
@@ -57,6 +83,7 @@
57
83
  $effect(() => {
58
84
  node.style.setProperty('--marquee-duration', `${duration}s`);
59
85
  node.style.setProperty('--marquee-gap', gap);
86
+ node.style.setProperty('--marquee-shift', `${contentSize}px`);
60
87
  });
61
88
  }
62
89
 
@@ -68,12 +95,14 @@
68
95
  </script>
69
96
 
70
97
  <div
98
+ bind:this={rootEl}
71
99
  class={className}
72
100
  data-marquee
73
101
  data-direction={direction}
74
102
  data-pause-on-hover={pauseOnHover || undefined}
75
103
  data-fade={fade || undefined}
76
104
  data-reduced-motion={prefersReducedMotion || undefined}
105
+ data-paused={paused || undefined}
77
106
  use:applyRootStyles
78
107
  {...rest}
79
108
  >
@@ -95,15 +124,19 @@
95
124
 
96
125
  overflow: hidden;
97
126
  position: relative;
127
+ contain: content;
98
128
  }
99
129
 
100
130
  [data-marquee-track] {
101
131
  display: grid;
102
132
  grid-auto-flow: var(--_flow, column);
103
- gap: var(--marquee-gap, 1rem);
104
133
  animation-duration: var(--dry-marquee-speed);
105
134
  animation-timing-function: linear;
106
135
  animation-iteration-count: infinite;
136
+ backface-visibility: hidden;
137
+ }
138
+
139
+ [data-marquee]:not([data-paused]) [data-marquee-track] {
107
140
  will-change: transform;
108
141
  }
109
142
 
@@ -113,6 +146,17 @@
113
146
  gap: var(--marquee-gap, 1rem);
114
147
  }
115
148
 
149
+ /* Trailing padding on content (not gap on track) keeps the keyframe loop seamless. */
150
+ [data-marquee][data-direction='left'] [data-marquee-content],
151
+ [data-marquee][data-direction='right'] [data-marquee-content] {
152
+ padding-inline-end: var(--marquee-gap, 1rem);
153
+ }
154
+
155
+ [data-marquee][data-direction='up'] [data-marquee-content],
156
+ [data-marquee][data-direction='down'] [data-marquee-content] {
157
+ padding-block-end: var(--marquee-gap, 1rem);
158
+ }
159
+
116
160
  [data-marquee][data-direction='left'] [data-marquee-track],
117
161
  [data-marquee][data-direction='right'] [data-marquee-track] {
118
162
  animation-name: marquee-horizontal;
@@ -132,12 +176,16 @@
132
176
  animation-play-state: paused;
133
177
  }
134
178
 
179
+ [data-marquee][data-paused] [data-marquee-track] {
180
+ animation-play-state: paused;
181
+ }
182
+
135
183
  @keyframes marquee-horizontal {
136
184
  from {
137
185
  transform: translateX(0);
138
186
  }
139
187
  to {
140
- transform: translateX(-50%);
188
+ transform: translateX(calc(-1 * var(--marquee-shift, 0px)));
141
189
  }
142
190
  }
143
191
 
@@ -146,7 +194,7 @@
146
194
  transform: translateY(0);
147
195
  }
148
196
  to {
149
- transform: translateY(-50%);
197
+ transform: translateY(calc(-1 * var(--marquee-shift, 0px)));
150
198
  }
151
199
  }
152
200
 
@@ -1,12 +1,13 @@
1
1
  export interface MegaMenuContext {
2
2
  readonly activeItem: string | null;
3
- openItem: (id: string) => void;
3
+ openItem: (id: string, triggerId: string) => void;
4
4
  closeItem: () => void;
5
5
  }
6
6
  export declare const setMegaMenuCtx: (ctx: MegaMenuContext) => MegaMenuContext, getMegaMenuCtx: () => MegaMenuContext;
7
7
  export interface MegaMenuItemContext {
8
8
  readonly itemId: string;
9
9
  readonly triggerId: string;
10
+ readonly panelId: string;
10
11
  readonly open: boolean;
11
12
  }
12
13
  export declare const setMegaMenuItemCtx: (ctx: MegaMenuItemContext) => MegaMenuItemContext, getMegaMenuItemCtx: () => MegaMenuItemContext;
@@ -17,18 +17,7 @@
17
17
  if (itemCtx.open) {
18
18
  ctx.closeItem();
19
19
  } else {
20
- ctx.openItem(itemCtx.itemId);
21
- }
22
- }
23
-
24
- function handleKeydown(e: KeyboardEvent) {
25
- if (e.key === 'Enter' || e.key === ' ') {
26
- e.preventDefault();
27
- if (itemCtx.open) {
28
- ctx.closeItem();
29
- } else {
30
- ctx.openItem(itemCtx.itemId);
31
- }
20
+ ctx.openItem(itemCtx.itemId, itemCtx.triggerId);
32
21
  }
33
22
  }
34
23
  </script>
@@ -37,13 +26,12 @@
37
26
  variant="trigger"
38
27
  type="button"
39
28
  id={itemCtx.triggerId}
29
+ aria-controls={itemCtx.open ? itemCtx.panelId : undefined}
40
30
  aria-expanded={itemCtx.open}
41
- aria-haspopup="true"
42
31
  data-mega-menu-trigger
43
32
  data-state={itemCtx.open ? 'open' : 'closed'}
44
33
  {...rest}
45
34
  onclick={handleClick}
46
- onkeydown={handleKeydown}
47
35
  >
48
36
  {@render children()}
49
37
  </Button>
@@ -13,17 +13,19 @@
13
13
  const ctx = getMegaMenuCtx();
14
14
  const itemId = generateFormId('mm-item');
15
15
  const triggerId = generateFormId('mm-trigger');
16
+ const panelId = generateFormId('mm-panel');
16
17
 
17
18
  setMegaMenuItemCtx({
18
19
  itemId,
19
20
  triggerId,
21
+ panelId,
20
22
  get open() {
21
23
  return ctx.activeItem === itemId;
22
24
  }
23
25
  });
24
26
 
25
27
  function handlePointerEnter() {
26
- ctx.openItem(itemId);
28
+ ctx.openItem(itemId, triggerId);
27
29
  }
28
30
 
29
31
  function handlePointerLeave() {
@@ -24,6 +24,8 @@
24
24
  const itemCtx = getMegaMenuItemCtx();
25
25
 
26
26
  let panelEl = $state<HTMLDivElement | null>(null);
27
+ let layout = $state<'columns' | 'stacked'>('columns');
28
+ const STACK_THRESHOLD = 32 * 16;
27
29
 
28
30
  const popover = createPositionedPopover({
29
31
  triggerEl: () => document.getElementById(itemCtx.triggerId),
@@ -36,8 +38,18 @@
36
38
  offset: () => 4
37
39
  });
38
40
 
41
+ function watchLayout(node: HTMLDivElement) {
42
+ const update = () => {
43
+ const next = window.innerWidth < STACK_THRESHOLD ? 'stacked' : 'columns';
44
+ if (next !== layout) layout = next;
45
+ };
46
+ update();
47
+ window.addEventListener('resize', update);
48
+ return () => window.removeEventListener('resize', update);
49
+ }
50
+
39
51
  function handlePointerEnter() {
40
- ctx.openItem(itemCtx.itemId);
52
+ ctx.openItem(itemCtx.itemId, itemCtx.triggerId);
41
53
  }
42
54
 
43
55
  function handlePointerLeave() {
@@ -70,12 +82,15 @@
70
82
  {@attach attachPanel}
71
83
  {@attach syncPopover(itemCtx.open)}
72
84
  {@attach fromAction(popover.applyPosition, () => style)}
73
- role="region"
85
+ {@attach watchLayout}
86
+ id={itemCtx.panelId}
87
+ role="group"
74
88
  popover="manual"
75
89
  data-mega-menu-panel
76
90
  aria-labelledby={itemCtx.triggerId}
77
91
  data-state={itemCtx.open ? 'open' : 'closed'}
78
92
  data-full-width={fullWidth || undefined}
93
+ data-layout={layout}
79
94
  class={className}
80
95
  onpointerenter={handlePointerEnter}
81
96
  onpointerleave={handlePointerLeave}
@@ -86,6 +101,13 @@
86
101
  {/if}
87
102
 
88
103
  <style>
104
+ @position-try --dry-mega-menu-panel-viewport-fit {
105
+ position-area: none;
106
+ inset-inline: 4vw;
107
+ inset-block-start: anchor(bottom);
108
+ margin-top: 0.5rem;
109
+ }
110
+
89
111
  [data-mega-menu-panel] {
90
112
  inset: unset;
91
113
  margin: 0;
@@ -95,11 +117,14 @@
95
117
  border-radius: var(--dry-radius-lg, 0.5rem);
96
118
  box-shadow: var(--dry-shadow-lg, 0 10px 15px -3px rgba(0, 0, 0, 0.1));
97
119
  padding: var(--dry-space-4, 1rem);
120
+ /* dryui-allow width */
121
+ max-inline-size: var(--dry-mega-menu-panel-max-width, min(92vw, 60rem));
98
122
  display: grid;
99
123
  grid-auto-flow: column;
100
- grid-auto-columns: max-content;
124
+ grid-auto-columns: minmax(var(--dry-mega-menu-panel-column-min, 12rem), max-content);
101
125
  align-items: start;
102
126
  gap: var(--dry-space-6, 1.5rem);
127
+ --dry-anchor-try-fallbacks: flip-block, flip-inline, --dry-mega-menu-panel-viewport-fit;
103
128
  }
104
129
 
105
130
  [data-mega-menu-panel]:not(:popover-open) {
@@ -110,6 +135,13 @@
110
135
  display: grid;
111
136
  }
112
137
 
138
+ [data-mega-menu-panel][data-layout='stacked'] {
139
+ grid-auto-flow: row;
140
+ grid-auto-columns: auto;
141
+ grid-auto-rows: auto;
142
+ gap: var(--dry-space-4, 1rem);
143
+ }
144
+
113
145
  [data-mega-menu-panel][data-full-width] {
114
146
  justify-self: stretch;
115
147
  }
@@ -10,25 +10,45 @@
10
10
  let { class: className, children, ...rest }: Props = $props();
11
11
 
12
12
  let activeItem = $state<string | null>(null);
13
+ let activeTriggerId = $state<string | null>(null);
13
14
  let openTimer: ReturnType<typeof setTimeout> | undefined;
14
15
  let closeTimer: ReturnType<typeof setTimeout> | undefined;
15
16
 
17
+ function clearPendingTimers() {
18
+ clearTimeout(openTimer);
19
+ clearTimeout(closeTimer);
20
+ }
21
+
22
+ function closeImmediately(options?: { restoreFocus?: boolean }) {
23
+ const triggerId = options?.restoreFocus ? activeTriggerId : null;
24
+ clearPendingTimers();
25
+ activeItem = null;
26
+ activeTriggerId = null;
27
+
28
+ if (!triggerId) return;
29
+
30
+ const trigger = document.getElementById(triggerId);
31
+ if (trigger instanceof HTMLElement) {
32
+ trigger.focus();
33
+ }
34
+ }
35
+
16
36
  setMegaMenuCtx({
17
37
  get activeItem() {
18
38
  return activeItem;
19
39
  },
20
- openItem(id) {
21
- clearTimeout(closeTimer);
22
- clearTimeout(openTimer);
40
+ openItem(id, triggerId) {
41
+ clearPendingTimers();
23
42
  openTimer = setTimeout(() => {
24
43
  activeItem = id;
44
+ activeTriggerId = triggerId;
25
45
  }, 150);
26
46
  },
27
47
  closeItem() {
28
- clearTimeout(openTimer);
29
- clearTimeout(closeTimer);
48
+ clearPendingTimers();
30
49
  closeTimer = setTimeout(() => {
31
50
  activeItem = null;
51
+ activeTriggerId = null;
32
52
  }, 300);
33
53
  }
34
54
  });
@@ -36,9 +56,7 @@
36
56
  function handleKeydown(e: KeyboardEvent) {
37
57
  if (e.key === 'Escape' && activeItem) {
38
58
  e.preventDefault();
39
- clearTimeout(openTimer);
40
- clearTimeout(closeTimer);
41
- activeItem = null;
59
+ closeImmediately({ restoreFocus: true });
42
60
  }
43
61
  }
44
62
 
@@ -46,16 +64,13 @@
46
64
  const nav = e.currentTarget as HTMLElement;
47
65
  const related = e.relatedTarget as Node | null;
48
66
  if (related && !nav.contains(related)) {
49
- clearTimeout(openTimer);
50
- clearTimeout(closeTimer);
51
- activeItem = null;
67
+ closeImmediately();
52
68
  }
53
69
  }
54
70
 
55
71
  $effect(() => {
56
72
  return () => {
57
- clearTimeout(openTimer);
58
- clearTimeout(closeTimer);
73
+ clearPendingTimers();
59
74
  };
60
75
  });
61
76
  </script>
@@ -7,8 +7,8 @@ export interface MenubarContext {
7
7
  registerMenu: (id: string) => void;
8
8
  unregisterMenu: (id: string) => void;
9
9
  getMenuIds: () => string[];
10
- focusNextMenu: (currentId: string) => void;
11
- focusPrevMenu: (currentId: string) => void;
10
+ focusNextMenu: (currentId: string, open?: boolean) => void;
11
+ focusPrevMenu: (currentId: string, open?: boolean) => void;
12
12
  }
13
13
  export declare const setMenubarCtx: (ctx: MenubarContext) => MenubarContext, getMenubarCtx: () => MenubarContext;
14
14
  export interface MenubarMenuContext {
@@ -8,10 +8,11 @@
8
8
  children: Snippet;
9
9
  }
10
10
 
11
- let { children, onclick, onkeydown, ...rest }: Props = $props();
11
+ let { children, onclick, onkeydown, id, ...rest }: Props = $props();
12
12
 
13
13
  const ctx = getMenubarCtx();
14
14
  const menuCtx = getMenubarMenuCtx();
15
+ const triggerId = $derived(id ?? `menubar-trigger-${menuCtx.menuId}`);
15
16
 
16
17
  function handleClick(e: MouseEvent & { currentTarget: HTMLButtonElement }) {
17
18
  if (menuCtx.open) {
@@ -32,12 +33,12 @@
32
33
  switch (e.key) {
33
34
  case 'ArrowRight': {
34
35
  e.preventDefault();
35
- ctx.focusNextMenu(menuCtx.menuId);
36
+ ctx.focusNextMenu(menuCtx.menuId, ctx.hasOpenMenu);
36
37
  break;
37
38
  }
38
39
  case 'ArrowLeft': {
39
40
  e.preventDefault();
40
- ctx.focusPrevMenu(menuCtx.menuId);
41
+ ctx.focusPrevMenu(menuCtx.menuId, ctx.hasOpenMenu);
41
42
  break;
42
43
  }
43
44
  case 'ArrowDown': {
@@ -59,6 +60,7 @@
59
60
  <Button
60
61
  variant="trigger"
61
62
  type="button"
63
+ id={triggerId}
62
64
  role="menuitem"
63
65
  aria-haspopup="menu"
64
66
  aria-expanded={menuCtx.open}