@dryui/ui 1.3.1 → 1.4.1
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.
- package/dist/accordion/accordion-content.svelte +1 -1
- package/dist/alert/alert.svelte +1 -1
- package/dist/app-frame/app-frame.svelte +131 -0
- package/dist/app-frame/app-frame.svelte.d.ts +10 -0
- package/dist/app-frame/index.d.ts +8 -0
- package/dist/app-frame/index.js +1 -0
- package/dist/aurora/aurora.svelte +22 -59
- package/dist/beam/beam.svelte +28 -9
- package/dist/carousel/carousel-button-dots.svelte +25 -8
- package/dist/carousel/carousel-button-thumbnails.svelte +25 -8
- package/dist/carousel/carousel-root.svelte +115 -4
- package/dist/carousel/carousel-slide.svelte +5 -1
- package/dist/carousel/carousel-viewport.svelte +2 -0
- package/dist/carousel/context.svelte.d.ts +5 -0
- package/dist/chart/chart-bars.svelte +25 -16
- package/dist/chart/chart-donut.svelte +25 -16
- package/dist/chart/chart-root.svelte +134 -30
- package/dist/chart/chart-root.svelte.d.ts +1 -0
- package/dist/chart/context.svelte.d.ts +3 -1
- package/dist/chart/context.svelte.js +1 -0
- package/dist/chart/index.d.ts +1 -0
- package/dist/chromatic-shift/chromatic-shift.svelte +36 -9
- package/dist/collapsible/collapsible-content.svelte +2 -1
- package/dist/combobox/combobox-content.svelte +26 -44
- package/dist/combobox/combobox-content.svelte.d.ts +1 -1
- package/dist/combobox/combobox-input-root.svelte +7 -1
- package/dist/combobox/combobox-input.svelte +21 -8
- package/dist/country-select/country-select-button-input.svelte +124 -260
- package/dist/date-picker/datepicker-content.svelte +18 -26
- package/dist/date-picker/datepicker-content.svelte.d.ts +2 -1
- package/dist/date-picker/datepicker-input-root.svelte +7 -1
- package/dist/date-range-picker/date-range-picker-content.svelte +18 -14
- package/dist/date-range-picker/date-range-picker-content.svelte.d.ts +2 -1
- package/dist/date-range-picker/date-range-picker-root.svelte +7 -1
- package/dist/displacement/displacement.svelte +16 -22
- package/dist/drag-and-drop/context.svelte.d.ts +2 -0
- package/dist/drag-and-drop/drag-and-drop-handle.svelte +34 -5
- package/dist/drag-and-drop/drag-and-drop-item.svelte +23 -14
- package/dist/drag-and-drop/drag-and-drop-root.svelte +60 -16
- package/dist/god-rays/god-rays.svelte +11 -0
- package/dist/gradient-mesh/gradient-mesh.svelte +27 -5
- package/dist/hover-card/context.svelte.d.ts +1 -10
- package/dist/hover-card/context.svelte.js +1 -2
- package/dist/hover-card/hover-card-content.svelte +41 -3
- package/dist/hover-card/hover-card-root.svelte +7 -55
- package/dist/hover-card/hover-card-trigger.svelte +79 -40
- package/dist/hover-card/hover-card-trigger.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal/motion.d.ts +1 -1
- package/dist/internal/motion.js +1 -1
- package/dist/marquee/marquee.svelte +42 -5
- package/dist/mega-menu/context.svelte.d.ts +2 -1
- package/dist/mega-menu/mega-menu-button-trigger.svelte +2 -14
- package/dist/mega-menu/mega-menu-item.svelte +3 -1
- package/dist/mega-menu/mega-menu-panel.svelte +35 -3
- package/dist/mega-menu/mega-menu-root.svelte +28 -13
- package/dist/menubar/context.svelte.d.ts +2 -2
- package/dist/menubar/menubar-button-trigger.svelte +5 -3
- package/dist/menubar/menubar-content.svelte +20 -12
- package/dist/menubar/menubar-root.svelte +4 -4
- package/dist/multi-select-combobox/multi-select-combobox-content.svelte +18 -55
- package/dist/multi-select-combobox/multi-select-combobox-content.svelte.d.ts +1 -1
- package/dist/noise/noise.svelte +38 -6
- package/dist/notification-center/context.svelte.d.ts +0 -1
- package/dist/notification-center/notification-center-panel.svelte +54 -35
- package/dist/notification-center/notification-center-root.svelte +0 -1
- package/dist/notification-center/notification-center-trigger-button.svelte +1 -8
- package/dist/option-picker/option-picker-description.svelte +2 -2
- package/dist/option-picker/option-picker-item.svelte +10 -3
- package/dist/option-picker/option-picker-label.svelte +2 -2
- package/dist/option-picker/option-picker-preview.svelte +18 -13
- package/dist/phone-input/phone-input-select.svelte +2 -152
- package/dist/phone-input/phone-input-select.svelte.d.ts +1 -7
- package/dist/rich-text-editor/rich-text-editor-toolbar-button-input.svelte +84 -29
- package/dist/scroll-area/scroll-area.svelte +16 -4
- package/dist/select/select-content.svelte +21 -31
- package/dist/select/select-content.svelte.d.ts +1 -1
- package/dist/select/select-root-input.svelte +7 -1
- package/dist/shimmer/shimmer.svelte +22 -12
- package/dist/transfer/transfer-item.svelte +0 -3
- package/dist/transfer/transfer-list-input.svelte +1 -6
- package/dist/tree/context.svelte.d.ts +7 -1
- package/dist/tree/tree-item-children.svelte +12 -10
- package/dist/tree/tree-item-label.svelte +6 -17
- package/dist/tree/tree-item-label.svelte.d.ts +2 -2
- package/dist/tree/tree-item.svelte +28 -1
- package/dist/tree/tree-root.svelte +135 -59
- package/package.json +8 -2
- package/skills/dryui/SKILL.md +1 -0
- 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<
|
|
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
|
|
16
|
+
let triggerEl = $state<HTMLAnchorElement | HTMLButtonElement>();
|
|
16
17
|
|
|
17
18
|
$effect(() => {
|
|
18
|
-
if (
|
|
19
|
-
ctx.triggerEl =
|
|
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
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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-
|
|
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<
|
|
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];
|
package/dist/internal/motion.js
CHANGED
|
@@ -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 {
|
|
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;
|
|
@@ -69,12 +95,14 @@
|
|
|
69
95
|
</script>
|
|
70
96
|
|
|
71
97
|
<div
|
|
98
|
+
bind:this={rootEl}
|
|
72
99
|
class={className}
|
|
73
100
|
data-marquee
|
|
74
101
|
data-direction={direction}
|
|
75
102
|
data-pause-on-hover={pauseOnHover || undefined}
|
|
76
103
|
data-fade={fade || undefined}
|
|
77
104
|
data-reduced-motion={prefersReducedMotion || undefined}
|
|
105
|
+
data-paused={paused || undefined}
|
|
78
106
|
use:applyRootStyles
|
|
79
107
|
{...rest}
|
|
80
108
|
>
|
|
@@ -96,6 +124,7 @@
|
|
|
96
124
|
|
|
97
125
|
overflow: hidden;
|
|
98
126
|
position: relative;
|
|
127
|
+
contain: content;
|
|
99
128
|
}
|
|
100
129
|
|
|
101
130
|
[data-marquee-track] {
|
|
@@ -104,6 +133,10 @@
|
|
|
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
|
|
|
@@ -143,6 +176,10 @@
|
|
|
143
176
|
animation-play-state: paused;
|
|
144
177
|
}
|
|
145
178
|
|
|
179
|
+
[data-marquee][data-paused] [data-marquee-track] {
|
|
180
|
+
animation-play-state: paused;
|
|
181
|
+
}
|
|
182
|
+
|
|
146
183
|
@keyframes marquee-horizontal {
|
|
147
184
|
from {
|
|
148
185
|
transform: translateX(0);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
clearTimeout(closeTimer);
|
|
51
|
-
activeItem = null;
|
|
67
|
+
closeImmediately();
|
|
52
68
|
}
|
|
53
69
|
}
|
|
54
70
|
|
|
55
71
|
$effect(() => {
|
|
56
72
|
return () => {
|
|
57
|
-
|
|
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}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
|
+
import { fromAction } from 'svelte/attachments';
|
|
3
4
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
5
|
import { createAnchoredPopover, createMenuNavigation } from '@dryui/primitives';
|
|
5
6
|
import { getMenubarCtx, getMenubarMenuCtx } from './context.svelte.js';
|
|
@@ -22,14 +23,21 @@
|
|
|
22
23
|
const ctx = getMenubarCtx();
|
|
23
24
|
const menuCtx = getMenubarMenuCtx();
|
|
24
25
|
|
|
25
|
-
let el = $state<HTMLDivElement>();
|
|
26
|
-
|
|
26
|
+
let el = $state<HTMLDivElement | null>(null);
|
|
27
|
+
const triggerEl = $derived(
|
|
28
|
+
ctx.rootElement?.querySelector<HTMLElement>(`[data-menubar-trigger="${menuCtx.menuId}"]`) ??
|
|
29
|
+
null
|
|
30
|
+
);
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
function attachContent(node: HTMLDivElement) {
|
|
33
|
+
el = node;
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
if (el === node) {
|
|
37
|
+
el = null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
33
41
|
|
|
34
42
|
const popover = createAnchoredPopover({
|
|
35
43
|
triggerEl: () => triggerEl,
|
|
@@ -61,12 +69,12 @@
|
|
|
61
69
|
switch (e.key) {
|
|
62
70
|
case 'ArrowRight': {
|
|
63
71
|
e.preventDefault();
|
|
64
|
-
ctx.focusNextMenu(menuCtx.menuId);
|
|
72
|
+
ctx.focusNextMenu(menuCtx.menuId, true);
|
|
65
73
|
return;
|
|
66
74
|
}
|
|
67
75
|
case 'ArrowLeft': {
|
|
68
76
|
e.preventDefault();
|
|
69
|
-
ctx.focusPrevMenu(menuCtx.menuId);
|
|
77
|
+
ctx.focusPrevMenu(menuCtx.menuId, true);
|
|
70
78
|
return;
|
|
71
79
|
}
|
|
72
80
|
case 'Escape': {
|
|
@@ -84,15 +92,15 @@
|
|
|
84
92
|
</script>
|
|
85
93
|
|
|
86
94
|
<div
|
|
87
|
-
|
|
95
|
+
{@attach attachContent}
|
|
96
|
+
{@attach fromAction(popover.applyPosition, () => style)}
|
|
88
97
|
popover="auto"
|
|
89
98
|
role="menu"
|
|
90
99
|
tabindex="-1"
|
|
91
|
-
aria-labelledby={
|
|
100
|
+
aria-labelledby={triggerEl?.id}
|
|
92
101
|
data-menubar-content
|
|
93
102
|
data-state={menuCtx.open ? 'open' : 'closed'}
|
|
94
103
|
class={className}
|
|
95
|
-
use:popover.applyPosition={style}
|
|
96
104
|
ontoggle={(e) => {
|
|
97
105
|
const newState = (e as ToggleEvent).newState === 'open';
|
|
98
106
|
if (!newState && menuCtx.open) {
|
|
@@ -48,20 +48,20 @@
|
|
|
48
48
|
getMenuIds() {
|
|
49
49
|
return menuIds;
|
|
50
50
|
},
|
|
51
|
-
focusNextMenu(currentId) {
|
|
51
|
+
focusNextMenu(currentId, open = false) {
|
|
52
52
|
const idx = menuIds.indexOf(currentId);
|
|
53
53
|
const nextIdx = (idx + 1) % menuIds.length;
|
|
54
54
|
const nextId = menuIds[nextIdx] ?? null;
|
|
55
|
-
activeMenu = nextId;
|
|
55
|
+
activeMenu = open ? nextId : null;
|
|
56
56
|
requestAnimationFrame(() => {
|
|
57
57
|
rootEl?.querySelector<HTMLButtonElement>(`[data-menubar-trigger="${nextId}"]`)?.focus();
|
|
58
58
|
});
|
|
59
59
|
},
|
|
60
|
-
focusPrevMenu(currentId) {
|
|
60
|
+
focusPrevMenu(currentId, open = false) {
|
|
61
61
|
const idx = menuIds.indexOf(currentId);
|
|
62
62
|
const prevIdx = (idx - 1 + menuIds.length) % menuIds.length;
|
|
63
63
|
const prevId = menuIds[prevIdx] ?? null;
|
|
64
|
-
activeMenu = prevId;
|
|
64
|
+
activeMenu = open ? prevId : null;
|
|
65
65
|
requestAnimationFrame(() => {
|
|
66
66
|
rootEl?.querySelector<HTMLButtonElement>(`[data-menubar-trigger="${prevId}"]`)?.focus();
|
|
67
67
|
});
|