@marianmeres/stuic 2.12.0 → 2.13.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.
- package/dist/components/DropdownMenu/DropdownMenu.svelte +764 -0
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +148 -0
- package/dist/components/DropdownMenu/index.css +16 -0
- package/dist/components/DropdownMenu/index.d.ts +1 -0
- package/dist/components/DropdownMenu/index.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,764 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Valid positions for dropdown placement relative to the trigger element.
|
|
8
|
+
*/
|
|
9
|
+
export type DropdownMenuPosition =
|
|
10
|
+
| "top"
|
|
11
|
+
| "top-left"
|
|
12
|
+
| "top-right"
|
|
13
|
+
| "top-span-left"
|
|
14
|
+
| "top-span-right"
|
|
15
|
+
| "bottom"
|
|
16
|
+
| "bottom-left"
|
|
17
|
+
| "bottom-right"
|
|
18
|
+
| "bottom-span-left"
|
|
19
|
+
| "bottom-span-right"
|
|
20
|
+
| "left"
|
|
21
|
+
| "right";
|
|
22
|
+
|
|
23
|
+
/** Base properties shared by most item types */
|
|
24
|
+
interface DropdownMenuItemBase {
|
|
25
|
+
/** Unique identifier - required for keyboard navigation tracking */
|
|
26
|
+
id: string | number;
|
|
27
|
+
/** Whether this item is disabled */
|
|
28
|
+
disabled?: boolean;
|
|
29
|
+
/** Additional CSS classes for this specific item */
|
|
30
|
+
class?: string;
|
|
31
|
+
/** Arbitrary data to pass through */
|
|
32
|
+
data?: Record<string, any>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Clickable action item */
|
|
36
|
+
export interface DropdownMenuActionItem extends DropdownMenuItemBase {
|
|
37
|
+
type: "action";
|
|
38
|
+
/** Label displayed - supports THC for icons, HTML, etc. */
|
|
39
|
+
label: THC;
|
|
40
|
+
/** Shortcut hint displayed on the right (e.g., "Cmd+K") */
|
|
41
|
+
shortcut?: string;
|
|
42
|
+
/** Icon displayed before label - supports THC */
|
|
43
|
+
icon?: THC;
|
|
44
|
+
/** Callback when item is selected */
|
|
45
|
+
onSelect?: () => void | boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Visual divider/separator */
|
|
49
|
+
export interface DropdownMenuDividerItem {
|
|
50
|
+
type: "divider";
|
|
51
|
+
id?: string | number;
|
|
52
|
+
class?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Non-interactive section header */
|
|
56
|
+
export interface DropdownMenuHeaderItem {
|
|
57
|
+
type: "header";
|
|
58
|
+
id?: string | number;
|
|
59
|
+
label: THC;
|
|
60
|
+
class?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Custom static content (non-interactive) */
|
|
64
|
+
export interface DropdownMenuCustomItem {
|
|
65
|
+
type: "custom";
|
|
66
|
+
id?: string | number;
|
|
67
|
+
/** Content rendered via THC */
|
|
68
|
+
content: THC;
|
|
69
|
+
class?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Expandable section containing nested items */
|
|
73
|
+
export interface DropdownMenuExpandableItem extends DropdownMenuItemBase {
|
|
74
|
+
type: "expandable";
|
|
75
|
+
/** Label for the expandable header */
|
|
76
|
+
label: THC;
|
|
77
|
+
icon?: THC;
|
|
78
|
+
/** Nested items (single level only - no nested expandables) */
|
|
79
|
+
items: DropdownMenuFlatItem[];
|
|
80
|
+
/** Whether section starts expanded */
|
|
81
|
+
defaultExpanded?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Flat items (non-expandable) */
|
|
85
|
+
export type DropdownMenuFlatItem =
|
|
86
|
+
| DropdownMenuActionItem
|
|
87
|
+
| DropdownMenuDividerItem
|
|
88
|
+
| DropdownMenuHeaderItem
|
|
89
|
+
| DropdownMenuCustomItem;
|
|
90
|
+
|
|
91
|
+
/** All possible item types */
|
|
92
|
+
export type DropdownMenuItem = DropdownMenuFlatItem | DropdownMenuExpandableItem;
|
|
93
|
+
|
|
94
|
+
/** Internal type for navigable items (includes expandable headers) */
|
|
95
|
+
export interface NavigableExpandable {
|
|
96
|
+
type: "expandable-header";
|
|
97
|
+
id: string | number;
|
|
98
|
+
expandableItem: DropdownMenuExpandableItem;
|
|
99
|
+
}
|
|
100
|
+
export type NavigableItem = DropdownMenuActionItem | NavigableExpandable;
|
|
101
|
+
|
|
102
|
+
export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
103
|
+
/** Menu items to display */
|
|
104
|
+
items: DropdownMenuItem[];
|
|
105
|
+
/** Controlled open state */
|
|
106
|
+
isOpen?: boolean;
|
|
107
|
+
/** Popover position relative to trigger */
|
|
108
|
+
position?: DropdownMenuPosition;
|
|
109
|
+
/** Offset from trigger element (CSS value) */
|
|
110
|
+
offset?: string;
|
|
111
|
+
/** Max height of dropdown (CSS value) */
|
|
112
|
+
maxHeight?: string;
|
|
113
|
+
/** Whether to close menu when an action item is selected */
|
|
114
|
+
closeOnSelect?: boolean;
|
|
115
|
+
/** Close on click outside */
|
|
116
|
+
closeOnClickOutside?: boolean;
|
|
117
|
+
/** Close on Escape key */
|
|
118
|
+
closeOnEscape?: boolean;
|
|
119
|
+
/** Force fallback positioning mode (for testing) */
|
|
120
|
+
forceFallback?: boolean;
|
|
121
|
+
/** Classes for the wrapper element */
|
|
122
|
+
class?: string;
|
|
123
|
+
/** Classes for the trigger button */
|
|
124
|
+
classTrigger?: string;
|
|
125
|
+
/** Classes for the dropdown container */
|
|
126
|
+
classDropdown?: string;
|
|
127
|
+
/** Classes for action items */
|
|
128
|
+
classItem?: string;
|
|
129
|
+
/** Classes for active/focused action item */
|
|
130
|
+
classItemActive?: string;
|
|
131
|
+
/** Classes for disabled items */
|
|
132
|
+
classItemDisabled?: string;
|
|
133
|
+
/** Classes for dividers */
|
|
134
|
+
classDivider?: string;
|
|
135
|
+
/** Classes for header items */
|
|
136
|
+
classHeader?: string;
|
|
137
|
+
/** Classes for expandable section header */
|
|
138
|
+
classExpandable?: string;
|
|
139
|
+
/** Classes for expandable section content */
|
|
140
|
+
classExpandableContent?: string;
|
|
141
|
+
/** Custom trigger snippet - receives isOpen state, toggle function, and ARIA props for full control */
|
|
142
|
+
trigger?: Snippet<
|
|
143
|
+
[
|
|
144
|
+
{
|
|
145
|
+
isOpen: boolean;
|
|
146
|
+
toggle: () => void;
|
|
147
|
+
triggerProps: {
|
|
148
|
+
id: string;
|
|
149
|
+
"aria-haspopup": "menu";
|
|
150
|
+
"aria-expanded": boolean;
|
|
151
|
+
"aria-controls": string;
|
|
152
|
+
};
|
|
153
|
+
},
|
|
154
|
+
]
|
|
155
|
+
>;
|
|
156
|
+
/** Simple content rendered inside the default trigger button */
|
|
157
|
+
children?: Snippet;
|
|
158
|
+
/** Called when menu opens */
|
|
159
|
+
onOpen?: () => void;
|
|
160
|
+
/** Called when menu closes */
|
|
161
|
+
onClose?: () => void;
|
|
162
|
+
/** Called when any action item is selected */
|
|
163
|
+
onSelect?: (item: DropdownMenuActionItem) => void;
|
|
164
|
+
/** Reference to trigger element */
|
|
165
|
+
triggerEl?: HTMLButtonElement;
|
|
166
|
+
/** Reference to dropdown element */
|
|
167
|
+
dropdownEl?: HTMLDivElement;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const POSITION_MAP: Record<string, string> = {
|
|
171
|
+
top: "top",
|
|
172
|
+
"top-left": "top left",
|
|
173
|
+
"top-right": "top right",
|
|
174
|
+
"top-span-left": "top span-left",
|
|
175
|
+
"top-span-right": "top span-right",
|
|
176
|
+
bottom: "bottom",
|
|
177
|
+
"bottom-left": "bottom left",
|
|
178
|
+
"bottom-right": "bottom right",
|
|
179
|
+
"bottom-span-left": "bottom span-left",
|
|
180
|
+
"bottom-span-right": "bottom span-right",
|
|
181
|
+
left: "left",
|
|
182
|
+
right: "right",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Checks if the browser supports CSS Anchor Positioning.
|
|
187
|
+
*/
|
|
188
|
+
function isAnchorPositioningSupported(): boolean {
|
|
189
|
+
if (typeof CSS === "undefined") return false;
|
|
190
|
+
return (
|
|
191
|
+
CSS.supports("anchor-name", "--anchor") &&
|
|
192
|
+
CSS.supports("position-area", "top") &&
|
|
193
|
+
CSS.supports("position-try-fallbacks", "top")
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export const DROPDOWN_MENU_BASE_CLASSES = `stuic-dropdown-menu relative inline-block`;
|
|
198
|
+
|
|
199
|
+
export const DROPDOWN_MENU_TRIGGER_CLASSES = `
|
|
200
|
+
inline-flex items-center justify-center gap-2
|
|
201
|
+
px-3 py-2
|
|
202
|
+
rounded-md border
|
|
203
|
+
bg-white dark:bg-neutral-800
|
|
204
|
+
text-neutral-900 dark:text-neutral-100
|
|
205
|
+
border-neutral-200 dark:border-neutral-700
|
|
206
|
+
hover:brightness-95 dark:hover:brightness-110
|
|
207
|
+
focus-visible:outline-2 focus-visible:outline-offset-2
|
|
208
|
+
cursor-pointer
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
export const DROPDOWN_MENU_DROPDOWN_CLASSES = `
|
|
212
|
+
stuic-dropdown-menu-dropdown
|
|
213
|
+
bg-white dark:bg-neutral-800
|
|
214
|
+
text-neutral-900 dark:text-neutral-100
|
|
215
|
+
border border-neutral-200 dark:border-neutral-700
|
|
216
|
+
rounded-md shadow-lg
|
|
217
|
+
p-1
|
|
218
|
+
overflow-y-auto
|
|
219
|
+
z-50
|
|
220
|
+
min-w-48
|
|
221
|
+
`;
|
|
222
|
+
|
|
223
|
+
export const DROPDOWN_MENU_ITEM_CLASSES = `
|
|
224
|
+
w-full
|
|
225
|
+
flex items-center gap-2
|
|
226
|
+
px-2 py-1.5
|
|
227
|
+
min-h-[44px]
|
|
228
|
+
text-left text-sm
|
|
229
|
+
rounded-sm
|
|
230
|
+
cursor-pointer
|
|
231
|
+
touch-action-manipulation
|
|
232
|
+
hover:bg-neutral-100 dark:hover:bg-neutral-700
|
|
233
|
+
focus:outline-none
|
|
234
|
+
focus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600
|
|
235
|
+
`;
|
|
236
|
+
|
|
237
|
+
export const DROPDOWN_MENU_DIVIDER_CLASSES = `
|
|
238
|
+
h-px my-1
|
|
239
|
+
bg-neutral-200 dark:bg-neutral-700
|
|
240
|
+
`;
|
|
241
|
+
|
|
242
|
+
export const DROPDOWN_MENU_HEADER_CLASSES = `
|
|
243
|
+
px-2 py-1.5
|
|
244
|
+
text-xs font-semibold uppercase tracking-wide
|
|
245
|
+
text-neutral-500 dark:text-neutral-400
|
|
246
|
+
select-none
|
|
247
|
+
`;
|
|
248
|
+
</script>
|
|
249
|
+
|
|
250
|
+
<script lang="ts">
|
|
251
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
252
|
+
import { getId } from "../../utils/get-id.js";
|
|
253
|
+
import { prefersReducedMotion } from "../../utils/prefers-reduced-motion.svelte.js";
|
|
254
|
+
import { ItemCollection } from "@marianmeres/item-collection";
|
|
255
|
+
import { iconLucideChevronDown } from "@marianmeres/icons-fns/lucide/iconLucideChevronDown.js";
|
|
256
|
+
import { iconLucideChevronRight } from "@marianmeres/icons-fns/lucide/iconLucideChevronRight.js";
|
|
257
|
+
import { onClickOutside } from "runed";
|
|
258
|
+
import { slide } from "svelte/transition";
|
|
259
|
+
import { untrack } from "svelte";
|
|
260
|
+
import Thc from "../Thc/Thc.svelte";
|
|
261
|
+
import "./index.css";
|
|
262
|
+
|
|
263
|
+
let {
|
|
264
|
+
items,
|
|
265
|
+
isOpen = $bindable(false),
|
|
266
|
+
position = "bottom-span-left",
|
|
267
|
+
offset = "0.25rem",
|
|
268
|
+
maxHeight = "300px",
|
|
269
|
+
closeOnSelect = true,
|
|
270
|
+
closeOnClickOutside = true,
|
|
271
|
+
closeOnEscape = true,
|
|
272
|
+
forceFallback = false,
|
|
273
|
+
class: classProp,
|
|
274
|
+
classTrigger,
|
|
275
|
+
classDropdown,
|
|
276
|
+
classItem,
|
|
277
|
+
classItemActive,
|
|
278
|
+
classItemDisabled,
|
|
279
|
+
classDivider,
|
|
280
|
+
classHeader,
|
|
281
|
+
classExpandable,
|
|
282
|
+
classExpandableContent,
|
|
283
|
+
trigger,
|
|
284
|
+
children,
|
|
285
|
+
onOpen,
|
|
286
|
+
onClose,
|
|
287
|
+
onSelect,
|
|
288
|
+
triggerEl = $bindable(),
|
|
289
|
+
dropdownEl = $bindable(),
|
|
290
|
+
...rest
|
|
291
|
+
}: Props = $props();
|
|
292
|
+
|
|
293
|
+
// Unique IDs for ARIA
|
|
294
|
+
const triggerId = getId("dropdown-trigger-");
|
|
295
|
+
const dropdownId = getId("dropdown-menu-");
|
|
296
|
+
const anchorName = `--dropdown-anchor-${triggerId}`;
|
|
297
|
+
|
|
298
|
+
// State
|
|
299
|
+
let wrapperEl: HTMLDivElement = $state()!;
|
|
300
|
+
let activeItemEl: HTMLButtonElement | undefined = $state();
|
|
301
|
+
const reducedMotion = prefersReducedMotion();
|
|
302
|
+
const isSupported = !forceFallback && isAnchorPositioningSupported();
|
|
303
|
+
|
|
304
|
+
// Track expanded sections (independent toggle - multiple can be open)
|
|
305
|
+
let expandedSections = $state<Set<string | number>>(new Set());
|
|
306
|
+
|
|
307
|
+
// Initialize expanded sections from defaultExpanded
|
|
308
|
+
$effect(() => {
|
|
309
|
+
const initial = new Set<string | number>();
|
|
310
|
+
for (const item of items) {
|
|
311
|
+
if (item.type === "expandable" && item.defaultExpanded) {
|
|
312
|
+
initial.add(item.id);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
expandedSections = initial;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// ItemCollection for keyboard navigation (includes action items and expandable headers)
|
|
319
|
+
// svelte-ignore state_referenced_locally
|
|
320
|
+
const navItems = new ItemCollection<NavigableItem>([], {
|
|
321
|
+
idPropName: "id",
|
|
322
|
+
});
|
|
323
|
+
let _navItems = $derived($navItems);
|
|
324
|
+
|
|
325
|
+
// Flatten navigable items (respects expanded state)
|
|
326
|
+
let navigableItems = $derived.by(() => {
|
|
327
|
+
const flat: NavigableItem[] = [];
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
if (item.type === "action" && !item.disabled) {
|
|
330
|
+
flat.push(item);
|
|
331
|
+
} else if (item.type === "expandable" && !item.disabled) {
|
|
332
|
+
// Add expandable header as navigable
|
|
333
|
+
flat.push({ type: "expandable-header", id: item.id, expandableItem: item });
|
|
334
|
+
// If expanded, add child actions
|
|
335
|
+
if (expandedSections.has(item.id)) {
|
|
336
|
+
for (const child of item.items) {
|
|
337
|
+
if (child.type === "action" && !child.disabled) {
|
|
338
|
+
flat.push(child);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return flat;
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// Update collection when navigable items change, preserving active state
|
|
348
|
+
$effect(() => {
|
|
349
|
+
// Use untrack to read active ID without creating a dependency on $navItems
|
|
350
|
+
const activeId = untrack(() => $navItems.active?.id);
|
|
351
|
+
navItems.clear().addMany(navigableItems);
|
|
352
|
+
// Restore active item if it still exists
|
|
353
|
+
if (activeId !== undefined) {
|
|
354
|
+
const item = navigableItems.find((i) => i.id === activeId);
|
|
355
|
+
if (item) {
|
|
356
|
+
navItems.setActive(item);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
// Reset active when menu closes
|
|
362
|
+
$effect(() => {
|
|
363
|
+
if (!isOpen) {
|
|
364
|
+
navItems.unsetActive();
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// Scroll active item into view
|
|
369
|
+
$effect(() => {
|
|
370
|
+
if (isOpen && _navItems.active?.id) {
|
|
371
|
+
const active = _navItems.active;
|
|
372
|
+
const elId =
|
|
373
|
+
active.type === "expandable-header"
|
|
374
|
+
? expandableHeaderId(active.id)
|
|
375
|
+
: itemId(active.id);
|
|
376
|
+
const el = document.getElementById(elId);
|
|
377
|
+
if (el) {
|
|
378
|
+
el.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
379
|
+
el.focus();
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// Handle open/close callbacks
|
|
385
|
+
$effect(() => {
|
|
386
|
+
if (isOpen) {
|
|
387
|
+
onOpen?.();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
let wasOpen = $state(false);
|
|
392
|
+
$effect(() => {
|
|
393
|
+
if (wasOpen && !isOpen) {
|
|
394
|
+
onClose?.();
|
|
395
|
+
}
|
|
396
|
+
wasOpen = isOpen;
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
// Click outside handler
|
|
400
|
+
onClickOutside(
|
|
401
|
+
() => wrapperEl,
|
|
402
|
+
() => {
|
|
403
|
+
if (closeOnClickOutside && isOpen) {
|
|
404
|
+
isOpen = false;
|
|
405
|
+
triggerEl?.focus();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
// Helper to generate item IDs
|
|
411
|
+
function itemId(id: string | number): string {
|
|
412
|
+
return `${dropdownId}-item-${id}`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Helper to generate header IDs for expandable sections
|
|
416
|
+
function expandableHeaderId(id: string | number): string {
|
|
417
|
+
return `${dropdownId}-expandable-${id}`;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Toggle expandable section
|
|
421
|
+
function toggleExpanded(id: string | number) {
|
|
422
|
+
const newSet = new Set(expandedSections);
|
|
423
|
+
if (newSet.has(id)) {
|
|
424
|
+
newSet.delete(id);
|
|
425
|
+
} else {
|
|
426
|
+
newSet.add(id);
|
|
427
|
+
}
|
|
428
|
+
expandedSections = newSet;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Select an action item
|
|
432
|
+
function selectItem(item: DropdownMenuActionItem) {
|
|
433
|
+
if (item.disabled) return;
|
|
434
|
+
|
|
435
|
+
const result = item.onSelect?.();
|
|
436
|
+
onSelect?.(item);
|
|
437
|
+
|
|
438
|
+
if (result !== false && closeOnSelect) {
|
|
439
|
+
isOpen = false;
|
|
440
|
+
triggerEl?.focus();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Computed transition duration
|
|
445
|
+
let transitionDuration = $derived(reducedMotion.current ? 0 : 100);
|
|
446
|
+
|
|
447
|
+
// Position styles for CSS Anchor Positioning
|
|
448
|
+
let dropdownStyle = $derived.by(() => {
|
|
449
|
+
if (isSupported) {
|
|
450
|
+
return `
|
|
451
|
+
position: fixed;
|
|
452
|
+
position-anchor: ${anchorName};
|
|
453
|
+
position-area: ${POSITION_MAP[position] || "bottom"};
|
|
454
|
+
margin: ${offset};
|
|
455
|
+
max-height: ${maxHeight};
|
|
456
|
+
`;
|
|
457
|
+
} else {
|
|
458
|
+
// Fallback: absolute positioning
|
|
459
|
+
if (position === "left") {
|
|
460
|
+
return `position: absolute; right: 100%; top: 0; margin-right: ${offset}; max-height: ${maxHeight};`;
|
|
461
|
+
} else if (position === "right") {
|
|
462
|
+
return `position: absolute; left: 100%; top: 0; margin-left: ${offset}; max-height: ${maxHeight};`;
|
|
463
|
+
}
|
|
464
|
+
const isTop = position.startsWith("top");
|
|
465
|
+
const isLeft =
|
|
466
|
+
position.includes("left") || position === "bottom" || position === "top";
|
|
467
|
+
return `
|
|
468
|
+
position: absolute;
|
|
469
|
+
${isTop ? "bottom: 100%;" : "top: 100%;"}
|
|
470
|
+
${isLeft ? "left: 0;" : "right: 0;"}
|
|
471
|
+
margin-top: ${isTop ? "0" : offset};
|
|
472
|
+
margin-bottom: ${isTop ? offset : "0"};
|
|
473
|
+
max-height: ${maxHeight};
|
|
474
|
+
`;
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
</script>
|
|
478
|
+
|
|
479
|
+
<svelte:window
|
|
480
|
+
onkeydown={(e) => {
|
|
481
|
+
if (!isOpen) return;
|
|
482
|
+
|
|
483
|
+
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
|
|
484
|
+
e.preventDefault();
|
|
485
|
+
if (e.key === "ArrowUp") {
|
|
486
|
+
e.metaKey ? navItems.setActiveFirst() : navItems.setActivePrevious();
|
|
487
|
+
} else {
|
|
488
|
+
e.metaKey ? navItems.setActiveLast() : navItems.setActiveNext();
|
|
489
|
+
}
|
|
490
|
+
} else if (e.key === "Home") {
|
|
491
|
+
e.preventDefault();
|
|
492
|
+
navItems.setActiveFirst();
|
|
493
|
+
} else if (e.key === "End") {
|
|
494
|
+
e.preventDefault();
|
|
495
|
+
navItems.setActiveLast();
|
|
496
|
+
} else if (["Enter", " "].includes(e.key)) {
|
|
497
|
+
e.preventDefault();
|
|
498
|
+
const active = _navItems.active;
|
|
499
|
+
if (active) {
|
|
500
|
+
if (active.type === "expandable-header") {
|
|
501
|
+
toggleExpanded(active.id);
|
|
502
|
+
} else {
|
|
503
|
+
selectItem(active as DropdownMenuActionItem);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
} else if (e.key === "ArrowRight") {
|
|
507
|
+
const active = _navItems.active;
|
|
508
|
+
if (active?.type === "expandable-header" && !expandedSections.has(active.id)) {
|
|
509
|
+
e.preventDefault();
|
|
510
|
+
toggleExpanded(active.id);
|
|
511
|
+
}
|
|
512
|
+
} else if (e.key === "ArrowLeft") {
|
|
513
|
+
const active = _navItems.active;
|
|
514
|
+
if (active?.type === "expandable-header" && expandedSections.has(active.id)) {
|
|
515
|
+
e.preventDefault();
|
|
516
|
+
toggleExpanded(active.id);
|
|
517
|
+
}
|
|
518
|
+
} else if (e.key === "Escape" && closeOnEscape) {
|
|
519
|
+
e.preventDefault();
|
|
520
|
+
isOpen = false;
|
|
521
|
+
triggerEl?.focus();
|
|
522
|
+
} else if (e.key === "Tab") {
|
|
523
|
+
isOpen = false;
|
|
524
|
+
}
|
|
525
|
+
}}
|
|
526
|
+
/>
|
|
527
|
+
|
|
528
|
+
<div
|
|
529
|
+
bind:this={wrapperEl}
|
|
530
|
+
class={twMerge(DROPDOWN_MENU_BASE_CLASSES, classProp)}
|
|
531
|
+
style:anchor-name={isSupported ? anchorName : undefined}
|
|
532
|
+
>
|
|
533
|
+
<!-- Trigger -->
|
|
534
|
+
{#if trigger}
|
|
535
|
+
<!-- Custom trigger with full control -->
|
|
536
|
+
{@render trigger({
|
|
537
|
+
isOpen,
|
|
538
|
+
toggle: () => (isOpen = !isOpen),
|
|
539
|
+
triggerProps: {
|
|
540
|
+
id: triggerId,
|
|
541
|
+
"aria-haspopup": "menu",
|
|
542
|
+
"aria-expanded": isOpen,
|
|
543
|
+
"aria-controls": dropdownId,
|
|
544
|
+
},
|
|
545
|
+
})}
|
|
546
|
+
{:else}
|
|
547
|
+
<!-- Default button (optionally with children content) -->
|
|
548
|
+
<button
|
|
549
|
+
bind:this={triggerEl}
|
|
550
|
+
id={triggerId}
|
|
551
|
+
class={twMerge(DROPDOWN_MENU_TRIGGER_CLASSES, classTrigger)}
|
|
552
|
+
onclick={() => (isOpen = !isOpen)}
|
|
553
|
+
aria-haspopup="menu"
|
|
554
|
+
aria-expanded={isOpen}
|
|
555
|
+
aria-controls={dropdownId}
|
|
556
|
+
type="button"
|
|
557
|
+
{...rest}
|
|
558
|
+
>
|
|
559
|
+
{#if children}
|
|
560
|
+
{@render children()}
|
|
561
|
+
{:else}
|
|
562
|
+
Menu
|
|
563
|
+
<span class={isOpen ? "rotate-180 inline-block" : "inline-block"}>
|
|
564
|
+
{@html iconLucideChevronDown({ size: 16 })}
|
|
565
|
+
</span>
|
|
566
|
+
{/if}
|
|
567
|
+
</button>
|
|
568
|
+
{/if}
|
|
569
|
+
|
|
570
|
+
<!-- Dropdown Menu -->
|
|
571
|
+
{#if isOpen}
|
|
572
|
+
<div
|
|
573
|
+
bind:this={dropdownEl}
|
|
574
|
+
id={dropdownId}
|
|
575
|
+
role="menu"
|
|
576
|
+
aria-labelledby={triggerId}
|
|
577
|
+
class={twMerge(DROPDOWN_MENU_DROPDOWN_CLASSES, classDropdown)}
|
|
578
|
+
style={dropdownStyle}
|
|
579
|
+
transition:slide={{ duration: transitionDuration }}
|
|
580
|
+
>
|
|
581
|
+
{#each items as item}
|
|
582
|
+
{#if item.type === "action"}
|
|
583
|
+
{@const isActive = _navItems.active?.id === item.id}
|
|
584
|
+
<button
|
|
585
|
+
id={itemId(item.id)}
|
|
586
|
+
role="menuitem"
|
|
587
|
+
class={twMerge(
|
|
588
|
+
DROPDOWN_MENU_ITEM_CLASSES,
|
|
589
|
+
isActive && "bg-neutral-200 dark:bg-neutral-600",
|
|
590
|
+
isActive && classItemActive,
|
|
591
|
+
item.disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
|
592
|
+
item.disabled && classItemDisabled,
|
|
593
|
+
classItem,
|
|
594
|
+
item.class
|
|
595
|
+
)}
|
|
596
|
+
onclick={() => selectItem(item)}
|
|
597
|
+
onmouseenter={() => navItems.setActive(item)}
|
|
598
|
+
aria-disabled={item.disabled || undefined}
|
|
599
|
+
tabindex={-1}
|
|
600
|
+
type="button"
|
|
601
|
+
>
|
|
602
|
+
{#if item.icon}
|
|
603
|
+
<span class="shrink-0">
|
|
604
|
+
<Thc thc={item.icon} />
|
|
605
|
+
</span>
|
|
606
|
+
{/if}
|
|
607
|
+
<span class="flex-1">
|
|
608
|
+
<Thc thc={item.label} />
|
|
609
|
+
</span>
|
|
610
|
+
{#if item.shortcut}
|
|
611
|
+
<span
|
|
612
|
+
class="text-xs text-dropdown-header dark:text-dropdown-header-dark ml-auto"
|
|
613
|
+
>
|
|
614
|
+
{item.shortcut}
|
|
615
|
+
</span>
|
|
616
|
+
{/if}
|
|
617
|
+
</button>
|
|
618
|
+
{:else if item.type === "divider"}
|
|
619
|
+
<div
|
|
620
|
+
role="separator"
|
|
621
|
+
class={twMerge(DROPDOWN_MENU_DIVIDER_CLASSES, classDivider, item.class)}
|
|
622
|
+
></div>
|
|
623
|
+
{:else if item.type === "header"}
|
|
624
|
+
<div
|
|
625
|
+
role="presentation"
|
|
626
|
+
class={twMerge(DROPDOWN_MENU_HEADER_CLASSES, classHeader, item.class)}
|
|
627
|
+
>
|
|
628
|
+
<Thc thc={item.label} />
|
|
629
|
+
</div>
|
|
630
|
+
{:else if item.type === "custom"}
|
|
631
|
+
<div role="presentation" class={item.class}>
|
|
632
|
+
<Thc thc={item.content} />
|
|
633
|
+
</div>
|
|
634
|
+
{:else if item.type === "expandable"}
|
|
635
|
+
{@const isExpanded = expandedSections.has(item.id)}
|
|
636
|
+
{@const isExpandableActive =
|
|
637
|
+
_navItems.active?.type === "expandable-header" &&
|
|
638
|
+
_navItems.active?.id === item.id}
|
|
639
|
+
<div role="group" aria-labelledby={expandableHeaderId(item.id)}>
|
|
640
|
+
<!-- Expandable header -->
|
|
641
|
+
<button
|
|
642
|
+
id={expandableHeaderId(item.id)}
|
|
643
|
+
role="menuitem"
|
|
644
|
+
class={twMerge(
|
|
645
|
+
DROPDOWN_MENU_ITEM_CLASSES,
|
|
646
|
+
"font-medium",
|
|
647
|
+
isExpandableActive && "bg-neutral-200 dark:bg-neutral-600",
|
|
648
|
+
isExpandableActive && classItemActive,
|
|
649
|
+
item.disabled && "opacity-50 cursor-not-allowed pointer-events-none",
|
|
650
|
+
item.disabled && classItemDisabled,
|
|
651
|
+
classExpandable,
|
|
652
|
+
item.class
|
|
653
|
+
)}
|
|
654
|
+
onclick={() => toggleExpanded(item.id)}
|
|
655
|
+
onmouseenter={() =>
|
|
656
|
+
navItems.setActive({
|
|
657
|
+
type: "expandable-header",
|
|
658
|
+
id: item.id,
|
|
659
|
+
expandableItem: item,
|
|
660
|
+
})}
|
|
661
|
+
aria-expanded={isExpanded}
|
|
662
|
+
aria-disabled={item.disabled || undefined}
|
|
663
|
+
tabindex={-1}
|
|
664
|
+
type="button"
|
|
665
|
+
>
|
|
666
|
+
{#if item.icon}
|
|
667
|
+
<span class="shrink-0">
|
|
668
|
+
<Thc thc={item.icon} />
|
|
669
|
+
</span>
|
|
670
|
+
{/if}
|
|
671
|
+
<span class="flex-1">
|
|
672
|
+
<Thc thc={item.label} />
|
|
673
|
+
</span>
|
|
674
|
+
<span
|
|
675
|
+
class={twMerge(
|
|
676
|
+
"transition-transform inline-block",
|
|
677
|
+
isExpanded && "rotate-90"
|
|
678
|
+
)}
|
|
679
|
+
>
|
|
680
|
+
{@html iconLucideChevronRight({ size: 16 })}
|
|
681
|
+
</span>
|
|
682
|
+
</button>
|
|
683
|
+
|
|
684
|
+
<!-- Expandable content -->
|
|
685
|
+
{#if isExpanded}
|
|
686
|
+
<div
|
|
687
|
+
class={twMerge(
|
|
688
|
+
"stuic-dropdown-menu-expandable-content pl-4",
|
|
689
|
+
classExpandableContent
|
|
690
|
+
)}
|
|
691
|
+
transition:slide={{ duration: transitionDuration }}
|
|
692
|
+
>
|
|
693
|
+
{#each item.items as childItem}
|
|
694
|
+
{#if childItem.type === "action"}
|
|
695
|
+
{@const isChildActive = _navItems.active?.id === childItem.id}
|
|
696
|
+
<button
|
|
697
|
+
id={itemId(childItem.id)}
|
|
698
|
+
role="menuitem"
|
|
699
|
+
class={twMerge(
|
|
700
|
+
DROPDOWN_MENU_ITEM_CLASSES,
|
|
701
|
+
isChildActive && "bg-neutral-200 dark:bg-neutral-600",
|
|
702
|
+
isChildActive && classItemActive,
|
|
703
|
+
childItem.disabled &&
|
|
704
|
+
"opacity-50 cursor-not-allowed pointer-events-none",
|
|
705
|
+
childItem.disabled && classItemDisabled,
|
|
706
|
+
classItem,
|
|
707
|
+
childItem.class
|
|
708
|
+
)}
|
|
709
|
+
onclick={() => selectItem(childItem)}
|
|
710
|
+
onmouseenter={() => navItems.setActive(childItem)}
|
|
711
|
+
aria-disabled={childItem.disabled || undefined}
|
|
712
|
+
tabindex={-1}
|
|
713
|
+
type="button"
|
|
714
|
+
>
|
|
715
|
+
{#if childItem.icon}
|
|
716
|
+
<span class="shrink-0">
|
|
717
|
+
<Thc thc={childItem.icon} />
|
|
718
|
+
</span>
|
|
719
|
+
{/if}
|
|
720
|
+
<span class="flex-1">
|
|
721
|
+
<Thc thc={childItem.label} />
|
|
722
|
+
</span>
|
|
723
|
+
{#if childItem.shortcut}
|
|
724
|
+
<span
|
|
725
|
+
class="text-xs text-dropdown-header dark:text-dropdown-header-dark ml-auto"
|
|
726
|
+
>
|
|
727
|
+
{childItem.shortcut}
|
|
728
|
+
</span>
|
|
729
|
+
{/if}
|
|
730
|
+
</button>
|
|
731
|
+
{:else if childItem.type === "divider"}
|
|
732
|
+
<div
|
|
733
|
+
role="separator"
|
|
734
|
+
class={twMerge(
|
|
735
|
+
DROPDOWN_MENU_DIVIDER_CLASSES,
|
|
736
|
+
classDivider,
|
|
737
|
+
childItem.class
|
|
738
|
+
)}
|
|
739
|
+
></div>
|
|
740
|
+
{:else if childItem.type === "header"}
|
|
741
|
+
<div
|
|
742
|
+
role="presentation"
|
|
743
|
+
class={twMerge(
|
|
744
|
+
DROPDOWN_MENU_HEADER_CLASSES,
|
|
745
|
+
classHeader,
|
|
746
|
+
childItem.class
|
|
747
|
+
)}
|
|
748
|
+
>
|
|
749
|
+
<Thc thc={childItem.label} />
|
|
750
|
+
</div>
|
|
751
|
+
{:else if childItem.type === "custom"}
|
|
752
|
+
<div role="presentation" class={childItem.class}>
|
|
753
|
+
<Thc thc={childItem.content} />
|
|
754
|
+
</div>
|
|
755
|
+
{/if}
|
|
756
|
+
{/each}
|
|
757
|
+
</div>
|
|
758
|
+
{/if}
|
|
759
|
+
</div>
|
|
760
|
+
{/if}
|
|
761
|
+
{/each}
|
|
762
|
+
</div>
|
|
763
|
+
{/if}
|
|
764
|
+
</div>
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLButtonAttributes } from "svelte/elements";
|
|
3
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
4
|
+
/**
|
|
5
|
+
* Valid positions for dropdown placement relative to the trigger element.
|
|
6
|
+
*/
|
|
7
|
+
export type DropdownMenuPosition = "top" | "top-left" | "top-right" | "top-span-left" | "top-span-right" | "bottom" | "bottom-left" | "bottom-right" | "bottom-span-left" | "bottom-span-right" | "left" | "right";
|
|
8
|
+
/** Base properties shared by most item types */
|
|
9
|
+
interface DropdownMenuItemBase {
|
|
10
|
+
/** Unique identifier - required for keyboard navigation tracking */
|
|
11
|
+
id: string | number;
|
|
12
|
+
/** Whether this item is disabled */
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
/** Additional CSS classes for this specific item */
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Arbitrary data to pass through */
|
|
17
|
+
data?: Record<string, any>;
|
|
18
|
+
}
|
|
19
|
+
/** Clickable action item */
|
|
20
|
+
export interface DropdownMenuActionItem extends DropdownMenuItemBase {
|
|
21
|
+
type: "action";
|
|
22
|
+
/** Label displayed - supports THC for icons, HTML, etc. */
|
|
23
|
+
label: THC;
|
|
24
|
+
/** Shortcut hint displayed on the right (e.g., "Cmd+K") */
|
|
25
|
+
shortcut?: string;
|
|
26
|
+
/** Icon displayed before label - supports THC */
|
|
27
|
+
icon?: THC;
|
|
28
|
+
/** Callback when item is selected */
|
|
29
|
+
onSelect?: () => void | boolean;
|
|
30
|
+
}
|
|
31
|
+
/** Visual divider/separator */
|
|
32
|
+
export interface DropdownMenuDividerItem {
|
|
33
|
+
type: "divider";
|
|
34
|
+
id?: string | number;
|
|
35
|
+
class?: string;
|
|
36
|
+
}
|
|
37
|
+
/** Non-interactive section header */
|
|
38
|
+
export interface DropdownMenuHeaderItem {
|
|
39
|
+
type: "header";
|
|
40
|
+
id?: string | number;
|
|
41
|
+
label: THC;
|
|
42
|
+
class?: string;
|
|
43
|
+
}
|
|
44
|
+
/** Custom static content (non-interactive) */
|
|
45
|
+
export interface DropdownMenuCustomItem {
|
|
46
|
+
type: "custom";
|
|
47
|
+
id?: string | number;
|
|
48
|
+
/** Content rendered via THC */
|
|
49
|
+
content: THC;
|
|
50
|
+
class?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Expandable section containing nested items */
|
|
53
|
+
export interface DropdownMenuExpandableItem extends DropdownMenuItemBase {
|
|
54
|
+
type: "expandable";
|
|
55
|
+
/** Label for the expandable header */
|
|
56
|
+
label: THC;
|
|
57
|
+
icon?: THC;
|
|
58
|
+
/** Nested items (single level only - no nested expandables) */
|
|
59
|
+
items: DropdownMenuFlatItem[];
|
|
60
|
+
/** Whether section starts expanded */
|
|
61
|
+
defaultExpanded?: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Flat items (non-expandable) */
|
|
64
|
+
export type DropdownMenuFlatItem = DropdownMenuActionItem | DropdownMenuDividerItem | DropdownMenuHeaderItem | DropdownMenuCustomItem;
|
|
65
|
+
/** All possible item types */
|
|
66
|
+
export type DropdownMenuItem = DropdownMenuFlatItem | DropdownMenuExpandableItem;
|
|
67
|
+
/** Internal type for navigable items (includes expandable headers) */
|
|
68
|
+
export interface NavigableExpandable {
|
|
69
|
+
type: "expandable-header";
|
|
70
|
+
id: string | number;
|
|
71
|
+
expandableItem: DropdownMenuExpandableItem;
|
|
72
|
+
}
|
|
73
|
+
export type NavigableItem = DropdownMenuActionItem | NavigableExpandable;
|
|
74
|
+
export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
75
|
+
/** Menu items to display */
|
|
76
|
+
items: DropdownMenuItem[];
|
|
77
|
+
/** Controlled open state */
|
|
78
|
+
isOpen?: boolean;
|
|
79
|
+
/** Popover position relative to trigger */
|
|
80
|
+
position?: DropdownMenuPosition;
|
|
81
|
+
/** Offset from trigger element (CSS value) */
|
|
82
|
+
offset?: string;
|
|
83
|
+
/** Max height of dropdown (CSS value) */
|
|
84
|
+
maxHeight?: string;
|
|
85
|
+
/** Whether to close menu when an action item is selected */
|
|
86
|
+
closeOnSelect?: boolean;
|
|
87
|
+
/** Close on click outside */
|
|
88
|
+
closeOnClickOutside?: boolean;
|
|
89
|
+
/** Close on Escape key */
|
|
90
|
+
closeOnEscape?: boolean;
|
|
91
|
+
/** Force fallback positioning mode (for testing) */
|
|
92
|
+
forceFallback?: boolean;
|
|
93
|
+
/** Classes for the wrapper element */
|
|
94
|
+
class?: string;
|
|
95
|
+
/** Classes for the trigger button */
|
|
96
|
+
classTrigger?: string;
|
|
97
|
+
/** Classes for the dropdown container */
|
|
98
|
+
classDropdown?: string;
|
|
99
|
+
/** Classes for action items */
|
|
100
|
+
classItem?: string;
|
|
101
|
+
/** Classes for active/focused action item */
|
|
102
|
+
classItemActive?: string;
|
|
103
|
+
/** Classes for disabled items */
|
|
104
|
+
classItemDisabled?: string;
|
|
105
|
+
/** Classes for dividers */
|
|
106
|
+
classDivider?: string;
|
|
107
|
+
/** Classes for header items */
|
|
108
|
+
classHeader?: string;
|
|
109
|
+
/** Classes for expandable section header */
|
|
110
|
+
classExpandable?: string;
|
|
111
|
+
/** Classes for expandable section content */
|
|
112
|
+
classExpandableContent?: string;
|
|
113
|
+
/** Custom trigger snippet - receives isOpen state, toggle function, and ARIA props for full control */
|
|
114
|
+
trigger?: Snippet<[
|
|
115
|
+
{
|
|
116
|
+
isOpen: boolean;
|
|
117
|
+
toggle: () => void;
|
|
118
|
+
triggerProps: {
|
|
119
|
+
id: string;
|
|
120
|
+
"aria-haspopup": "menu";
|
|
121
|
+
"aria-expanded": boolean;
|
|
122
|
+
"aria-controls": string;
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
]>;
|
|
126
|
+
/** Simple content rendered inside the default trigger button */
|
|
127
|
+
children?: Snippet;
|
|
128
|
+
/** Called when menu opens */
|
|
129
|
+
onOpen?: () => void;
|
|
130
|
+
/** Called when menu closes */
|
|
131
|
+
onClose?: () => void;
|
|
132
|
+
/** Called when any action item is selected */
|
|
133
|
+
onSelect?: (item: DropdownMenuActionItem) => void;
|
|
134
|
+
/** Reference to trigger element */
|
|
135
|
+
triggerEl?: HTMLButtonElement;
|
|
136
|
+
/** Reference to dropdown element */
|
|
137
|
+
dropdownEl?: HTMLDivElement;
|
|
138
|
+
}
|
|
139
|
+
export declare const DROPDOWN_MENU_BASE_CLASSES = "stuic-dropdown-menu relative inline-block";
|
|
140
|
+
export declare const DROPDOWN_MENU_TRIGGER_CLASSES = "\n\t\tinline-flex items-center justify-center gap-2\n\t\tpx-3 py-2\n\t\trounded-md border\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder-neutral-200 dark:border-neutral-700\n\t\thover:brightness-95 dark:hover:brightness-110\n\t\tfocus-visible:outline-2 focus-visible:outline-offset-2\n\t\tcursor-pointer\n\t";
|
|
141
|
+
export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "\n\t\tstuic-dropdown-menu-dropdown\n\t\tbg-white dark:bg-neutral-800\n\t\ttext-neutral-900 dark:text-neutral-100\n\t\tborder border-neutral-200 dark:border-neutral-700\n\t\trounded-md shadow-lg\n\t\tp-1\n\t\toverflow-y-auto\n\t\tz-50\n\t\tmin-w-48\n\t";
|
|
142
|
+
export declare const DROPDOWN_MENU_ITEM_CLASSES = "\n\t\tw-full\n\t\tflex items-center gap-2\n\t\tpx-2 py-1.5\n\t\tmin-h-[44px]\n\t\ttext-left text-sm\n\t\trounded-sm\n\t\tcursor-pointer\n\t\ttouch-action-manipulation\n\t\thover:bg-neutral-100 dark:hover:bg-neutral-700\n\t\tfocus:outline-none\n\t\tfocus-visible:bg-neutral-200 dark:focus-visible:bg-neutral-600\n\t";
|
|
143
|
+
export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "\n\t\th-px my-1\n\t\tbg-neutral-200 dark:bg-neutral-700\n\t";
|
|
144
|
+
export declare const DROPDOWN_MENU_HEADER_CLASSES = "\n\t\tpx-2 py-1.5\n\t\ttext-xs font-semibold uppercase tracking-wide\n\t\ttext-neutral-500 dark:text-neutral-400\n\t\tselect-none\n\t";
|
|
145
|
+
import "./index.css";
|
|
146
|
+
declare const DropdownMenu: import("svelte").Component<Props, {}, "isOpen" | "triggerEl" | "dropdownEl">;
|
|
147
|
+
type DropdownMenu = ReturnType<typeof DropdownMenu>;
|
|
148
|
+
export default DropdownMenu;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/* Base dropdown styles */
|
|
2
|
+
.stuic-dropdown-menu-dropdown {
|
|
3
|
+
scrollbar-width: thin;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
/* CSS Anchor Positioning supported mode - flip only when viewport overflow */
|
|
7
|
+
@supports (anchor-name: --anchor) {
|
|
8
|
+
.stuic-dropdown-menu-dropdown {
|
|
9
|
+
position-try-fallbacks: flip-block, flip-inline;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* Expandable section animation handled by Svelte slide transition */
|
|
14
|
+
.stuic-dropdown-menu-expandable-content {
|
|
15
|
+
overflow: hidden;
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DropdownMenu, type Props as DropdownMenuProps, type DropdownMenuItem, type DropdownMenuActionItem, type DropdownMenuDividerItem, type DropdownMenuHeaderItem, type DropdownMenuCustomItem, type DropdownMenuExpandableItem, type DropdownMenuFlatItem, type DropdownMenuPosition, type NavigableItem, type NavigableExpandable, DROPDOWN_MENU_BASE_CLASSES, DROPDOWN_MENU_TRIGGER_CLASSES, DROPDOWN_MENU_DROPDOWN_CLASSES, DROPDOWN_MENU_ITEM_CLASSES, DROPDOWN_MENU_DIVIDER_CLASSES, DROPDOWN_MENU_HEADER_CLASSES, } from "./DropdownMenu.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as DropdownMenu, DROPDOWN_MENU_BASE_CLASSES, DROPDOWN_MENU_TRIGGER_CLASSES, DROPDOWN_MENU_DROPDOWN_CLASSES, DROPDOWN_MENU_ITEM_CLASSES, DROPDOWN_MENU_DIVIDER_CLASSES, DROPDOWN_MENU_HEADER_CLASSES, } from "./DropdownMenu.svelte";
|
package/dist/index.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from "./components/ColorScheme/index.js";
|
|
|
31
31
|
export * from "./components/CommandMenu/index.js";
|
|
32
32
|
export * from "./components/DismissibleMessage/index.js";
|
|
33
33
|
export * from "./components/Drawer/index.js";
|
|
34
|
+
export * from "./components/DropdownMenu/index.js";
|
|
34
35
|
export * from "./components/HoverExpandableWidth/index.js";
|
|
35
36
|
export * from "./components/Input/index.js";
|
|
36
37
|
export * from "./components/KbdShortcut/index.js";
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export * from "./components/ColorScheme/index.js";
|
|
|
32
32
|
export * from "./components/CommandMenu/index.js";
|
|
33
33
|
export * from "./components/DismissibleMessage/index.js";
|
|
34
34
|
export * from "./components/Drawer/index.js";
|
|
35
|
+
export * from "./components/DropdownMenu/index.js";
|
|
35
36
|
export * from "./components/HoverExpandableWidth/index.js";
|
|
36
37
|
export * from "./components/Input/index.js";
|
|
37
38
|
export * from "./components/KbdShortcut/index.js";
|