@marianmeres/stuic 3.0.0 → 3.0.2
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/actions/index.d.ts +1 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/typeahead.svelte.d.ts +53 -0
- package/dist/actions/typeahead.svelte.js +328 -0
- package/dist/base.css +17 -0
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte +4 -3
- package/dist/components/AlertConfirmPrompt/AlertConfirmPrompt.svelte.d.ts +4 -3
- package/dist/components/AlertConfirmPrompt/Current.svelte +1 -2
- package/dist/components/AlertConfirmPrompt/Current.svelte.d.ts +0 -1
- package/dist/components/AlertConfirmPrompt/index.css +47 -43
- package/dist/components/AssetsPreview/AssetsPreview.svelte +0 -1
- package/dist/components/AssetsPreview/AssetsPreview.svelte.d.ts +0 -1
- package/dist/components/AssetsPreview/index.css +31 -29
- package/dist/components/Avatar/Avatar.svelte +0 -1
- package/dist/components/Avatar/Avatar.svelte.d.ts +0 -1
- package/dist/components/Avatar/index.css +87 -85
- package/dist/components/Backdrop/Backdrop.svelte +0 -1
- package/dist/components/Backdrop/Backdrop.svelte.d.ts +0 -1
- package/dist/components/Backdrop/index.css +15 -13
- package/dist/components/Button/Button.svelte +0 -1
- package/dist/components/Button/Button.svelte.d.ts +0 -1
- package/dist/components/Button/index.css +431 -429
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte +0 -1
- package/dist/components/ButtonGroupRadio/ButtonGroupRadio.svelte.d.ts +0 -1
- package/dist/components/ButtonGroupRadio/index.css +123 -117
- package/dist/components/Collapsible/index.css +17 -15
- package/dist/components/CommandMenu/CommandMenu.svelte +7 -4
- package/dist/components/CommandMenu/CommandMenu.svelte.d.ts +0 -1
- package/dist/components/CommandMenu/index.css +27 -25
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte +0 -2
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte.d.ts +0 -1
- package/dist/components/DismissibleMessage/index.css +116 -110
- package/dist/components/DropdownMenu/DropdownMenu.svelte +317 -74
- package/dist/components/DropdownMenu/DropdownMenu.svelte.d.ts +19 -1
- package/dist/components/DropdownMenu/index.css +236 -170
- package/dist/components/DropdownMenu/index.d.ts +1 -1
- package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte +3 -1
- package/dist/components/HoverExpandableWidth/HoverExpandableWidth.svelte.d.ts +1 -0
- package/dist/components/Input/FieldInput.svelte +8 -0
- package/dist/components/Input/FieldInput.svelte.d.ts +2 -0
- package/dist/components/Input/FieldOptions.svelte +1 -1
- package/dist/components/Input/index.css +411 -398
- package/dist/components/KbdShortcut/KbdShortcut.svelte +4 -12
- package/dist/components/KbdShortcut/README.md +34 -0
- package/dist/components/KbdShortcut/index.css +55 -0
- package/dist/components/ListItemButton/ListItemButton.svelte +0 -1
- package/dist/components/ListItemButton/ListItemButton.svelte.d.ts +0 -1
- package/dist/components/ListItemButton/index.css +118 -116
- package/dist/components/Modal/Modal.svelte +0 -1
- package/dist/components/Modal/Modal.svelte.d.ts +0 -1
- package/dist/components/Modal/index.css +18 -16
- package/dist/components/ModalDialog/index.css +29 -27
- package/dist/components/Nav/Nav.svelte +732 -0
- package/dist/components/Nav/Nav.svelte.d.ts +110 -0
- package/dist/components/Nav/README.md +334 -0
- package/dist/components/Nav/index.css +318 -0
- package/dist/components/Nav/index.d.ts +1 -0
- package/dist/components/Nav/index.js +1 -0
- package/dist/components/Notifications/Notifications.svelte +2 -3
- package/dist/components/Notifications/Notifications.svelte.d.ts +0 -1
- package/dist/components/Notifications/index.css +158 -158
- package/dist/components/Notifications/notifications-stack.svelte.d.ts +4 -0
- package/dist/components/Notifications/notifications-stack.svelte.js +8 -0
- package/dist/components/Progress/Progress.svelte +4 -2
- package/dist/components/Progress/Progress.svelte.d.ts +1 -0
- package/dist/components/Progress/README.md +86 -15
- package/dist/components/Progress/_internal/Bar.svelte +4 -15
- package/dist/components/Progress/_internal/Bar.svelte.d.ts +1 -1
- package/dist/components/Progress/_internal/Circle.svelte +30 -2
- package/dist/components/Progress/_internal/Circle.svelte.d.ts +1 -0
- package/dist/components/Progress/index.css +47 -1
- package/dist/components/Skeleton/README.md +152 -0
- package/dist/components/Skeleton/Skeleton.svelte +6 -7
- package/dist/components/Skeleton/Skeleton.svelte.d.ts +0 -1
- package/dist/components/Skeleton/index.css +73 -43
- package/dist/components/Spinner/README.md +149 -37
- package/dist/components/Spinner/Spinner.svelte +14 -38
- package/dist/components/Spinner/Spinner.svelte.d.ts +2 -1
- package/dist/components/Spinner/SpinnerCircle.svelte +6 -34
- package/dist/components/Spinner/SpinnerCircle.svelte.d.ts +1 -0
- package/dist/components/Spinner/SpinnerCircleOscillate.svelte +10 -5
- package/dist/components/Spinner/SpinnerUnicode.svelte +3 -1
- package/dist/components/Spinner/SpinnerUnicode.svelte.d.ts +1 -0
- package/dist/components/Spinner/index.css +104 -0
- package/dist/components/Switch/README.md +34 -18
- package/dist/components/Switch/Switch.svelte +24 -46
- package/dist/components/Switch/Switch.svelte.d.ts +4 -2
- package/dist/components/Switch/index.css +120 -2
- package/dist/components/Switch/index.d.ts +1 -2
- package/dist/components/Switch/index.js +1 -2
- package/dist/components/TabbedMenu/README.md +28 -17
- package/dist/components/TabbedMenu/TabbedMenu.svelte +5 -46
- package/dist/components/TabbedMenu/TabbedMenu.svelte.d.ts +0 -1
- package/dist/components/TabbedMenu/index.css +85 -3
- package/dist/components/ThemePreview/ThemePreview.svelte +86 -33
- package/dist/components/ThemePreview/ThemePreview.svelte.d.ts +3 -1
- package/dist/components/ThemePreview/index.css +24 -8
- package/dist/components/TwCheck/README.md +32 -13
- package/dist/components/TwCheck/TwCheck.svelte +11 -9
- package/dist/components/TwCheck/TwCheck.svelte.d.ts +0 -1
- package/dist/components/TwCheck/index.css +14 -0
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte +19 -187
- package/dist/components/TypeaheadInput/TypeaheadInput.svelte.d.ts +4 -2
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +1 -0
- package/dist/index.css +44 -39
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/themes/blue-orange.css +246 -156
- package/dist/themes/blue-orange.js +24 -0
- package/dist/themes/cyan-red.css +246 -156
- package/dist/themes/cyan-red.js +24 -0
- package/dist/themes/cyan-slate.css +246 -156
- package/dist/themes/cyan-slate.js +25 -1
- package/dist/themes/emerald-pink.css +246 -156
- package/dist/themes/emerald-pink.js +25 -1
- package/dist/themes/fuchsia-emerald.css +246 -156
- package/dist/themes/fuchsia-emerald.js +25 -1
- package/dist/themes/gray.css +246 -156
- package/dist/themes/gray.js +24 -0
- package/dist/themes/indigo-amber.css +246 -156
- package/dist/themes/indigo-amber.js +26 -2
- package/dist/themes/neutral.css +246 -156
- package/dist/themes/neutral.js +24 -0
- package/dist/themes/pink-emerald.css +246 -156
- package/dist/themes/pink-emerald.js +25 -1
- package/dist/themes/pink-teal.css +253 -0
- package/dist/themes/pink-teal.d.ts +6 -0
- package/dist/themes/pink-teal.js +175 -0
- package/dist/themes/purple-yellow.css +246 -156
- package/dist/themes/purple-yellow.js +24 -0
- package/dist/themes/rainbow.css +246 -156
- package/dist/themes/rainbow.js +25 -1
- package/dist/themes/red-blue.css +246 -156
- package/dist/themes/red-blue.js +24 -0
- package/dist/themes/red-cyan.css +246 -156
- package/dist/themes/red-cyan.js +24 -0
- package/dist/themes/red-sky.css +253 -0
- package/dist/themes/red-sky.d.ts +6 -0
- package/dist/themes/red-sky.js +175 -0
- package/dist/themes/rose-teal.css +246 -156
- package/dist/themes/rose-teal.js +24 -0
- package/dist/themes/sky-amber.css +246 -156
- package/dist/themes/sky-amber.js +26 -2
- package/dist/themes/slate-cyan.css +246 -156
- package/dist/themes/slate-cyan.js +25 -1
- package/dist/themes/teal-rose.css +246 -156
- package/dist/themes/teal-rose.js +24 -0
- package/dist/themes/violet-lime.css +246 -156
- package/dist/themes/violet-lime.js +27 -3
- package/dist/utils/design-tokens.d.ts +1 -1
- package/dist/utils/design-tokens.js +44 -3
- package/dist/utils/storage-abstraction.js +1 -1
- package/package.json +11 -28
- package/dist/components/Switch/SwitchButton.svelte +0 -134
- package/dist/components/Switch/SwitchButton.svelte.d.ts +0 -21
|
@@ -100,6 +100,20 @@
|
|
|
100
100
|
}
|
|
101
101
|
export type NavigableItem = DropdownMenuActionItem | NavigableExpandable;
|
|
102
102
|
|
|
103
|
+
/** Search configuration options */
|
|
104
|
+
export interface DropdownMenuSearchConfig {
|
|
105
|
+
/** Placeholder text for search input */
|
|
106
|
+
placeholder?: string;
|
|
107
|
+
/** Search strategy */
|
|
108
|
+
strategy?: "prefix" | "exact" | "fuzzy";
|
|
109
|
+
/** Custom function to extract searchable text from an item */
|
|
110
|
+
getContent?: (item: DropdownMenuActionItem | DropdownMenuExpandableItem) => string;
|
|
111
|
+
/** Auto-focus search input when menu opens */
|
|
112
|
+
autoFocus?: boolean;
|
|
113
|
+
/** Message shown when no results found */
|
|
114
|
+
noResultsMessage?: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
103
117
|
export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
104
118
|
/** Menu items to display */
|
|
105
119
|
items: DropdownMenuItem[];
|
|
@@ -119,6 +133,8 @@
|
|
|
119
133
|
closeOnEscape?: boolean;
|
|
120
134
|
/** Force fallback positioning mode (for testing) */
|
|
121
135
|
forceFallback?: boolean;
|
|
136
|
+
/** Enable search/filter functionality (true for defaults, or config object) */
|
|
137
|
+
search?: boolean | DropdownMenuSearchConfig;
|
|
122
138
|
/** Classes for the wrapper element */
|
|
123
139
|
class?: string;
|
|
124
140
|
/** Classes for the trigger button */
|
|
@@ -145,6 +161,10 @@
|
|
|
145
161
|
classExpandableContent?: string;
|
|
146
162
|
/** Classes for backdrop (fallback mode only) */
|
|
147
163
|
classBackdrop?: string;
|
|
164
|
+
/** Classes for search input container */
|
|
165
|
+
classSearchContainer?: string;
|
|
166
|
+
/** Classes for search input */
|
|
167
|
+
classSearchInput?: string;
|
|
148
168
|
/** Show backdrop in fallback mode (default: true) */
|
|
149
169
|
showBackdrop?: boolean;
|
|
150
170
|
/** Custom trigger snippet - receives isOpen state, toggle function, and ARIA props for full control */
|
|
@@ -223,13 +243,12 @@
|
|
|
223
243
|
import { getId } from "../../utils/get-id.js";
|
|
224
244
|
import { prefersReducedMotion } from "../../utils/prefers-reduced-motion.svelte.js";
|
|
225
245
|
import { ItemCollection } from "@marianmeres/item-collection";
|
|
226
|
-
import { iconChevronDown, iconChevronRight } from "../../icons/index.js";
|
|
246
|
+
import { iconChevronDown, iconChevronRight, iconSearch, iconX } from "../../icons/index.js";
|
|
227
247
|
import { onClickOutside } from "runed";
|
|
228
248
|
import { slide, fade } from "svelte/transition";
|
|
229
249
|
import { untrack } from "svelte";
|
|
230
250
|
import Thc from "../Thc/Thc.svelte";
|
|
231
251
|
import ListItemButton from "../ListItemButton/ListItemButton.svelte";
|
|
232
|
-
import "./index.css";
|
|
233
252
|
import { BodyScroll } from "../../utils/body-scroll-locker.js";
|
|
234
253
|
import { waitForTwoRepaints } from "../../utils/paint.js";
|
|
235
254
|
|
|
@@ -243,6 +262,7 @@
|
|
|
243
262
|
closeOnClickOutside = true,
|
|
244
263
|
closeOnEscape = true,
|
|
245
264
|
forceFallback = false,
|
|
265
|
+
search,
|
|
246
266
|
class: classProp,
|
|
247
267
|
classTrigger,
|
|
248
268
|
classDropdown,
|
|
@@ -256,6 +276,8 @@
|
|
|
256
276
|
classExpandable,
|
|
257
277
|
classExpandableContent,
|
|
258
278
|
classBackdrop,
|
|
279
|
+
classSearchContainer,
|
|
280
|
+
classSearchInput,
|
|
259
281
|
showBackdrop = true,
|
|
260
282
|
trigger,
|
|
261
283
|
children,
|
|
@@ -289,6 +311,91 @@
|
|
|
289
311
|
// Track expanded sections (independent toggle - multiple can be open)
|
|
290
312
|
let expandedSections = $state<Set<string | number>>(new Set());
|
|
291
313
|
|
|
314
|
+
// Search state
|
|
315
|
+
let searchQuery = $state("");
|
|
316
|
+
let searchInputEl: HTMLInputElement | undefined = $state();
|
|
317
|
+
|
|
318
|
+
// Default content extractor for search
|
|
319
|
+
function defaultGetContent(
|
|
320
|
+
item: DropdownMenuActionItem | DropdownMenuExpandableItem
|
|
321
|
+
): string {
|
|
322
|
+
if (typeof item.label === "string") return item.label;
|
|
323
|
+
if (item.label && typeof item.label === "object" && "text" in item.label) {
|
|
324
|
+
return (item.label as { text?: string }).text || "";
|
|
325
|
+
}
|
|
326
|
+
return String(item.id);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Normalize search config
|
|
330
|
+
let searchConfig = $derived.by(() => {
|
|
331
|
+
if (!search) return null;
|
|
332
|
+
const defaults = {
|
|
333
|
+
placeholder: "Search...",
|
|
334
|
+
strategy: "prefix" as const,
|
|
335
|
+
getContent: defaultGetContent,
|
|
336
|
+
autoFocus: true,
|
|
337
|
+
noResultsMessage: "No results found",
|
|
338
|
+
};
|
|
339
|
+
return search === true ? defaults : { ...defaults, ...search };
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
// Extract all searchable items (action + expandable + nested actions)
|
|
343
|
+
let allSearchableItems = $derived.by(() => {
|
|
344
|
+
const result: (DropdownMenuActionItem | DropdownMenuExpandableItem)[] = [];
|
|
345
|
+
for (const item of items) {
|
|
346
|
+
if (item.type === "action") result.push(item);
|
|
347
|
+
if (item.type === "expandable") {
|
|
348
|
+
result.push(item);
|
|
349
|
+
for (const child of item.items) {
|
|
350
|
+
if (child.type === "action") result.push(child);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return result;
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Searchable collection (recreate when items or config changes)
|
|
358
|
+
let searchableCollection = $derived.by(() => {
|
|
359
|
+
if (!searchConfig) return null;
|
|
360
|
+
return new ItemCollection(allSearchableItems, {
|
|
361
|
+
idPropName: "id",
|
|
362
|
+
searchable: { getContent: searchConfig.getContent },
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Filtered items based on search
|
|
367
|
+
let filteredItems = $derived.by(() => {
|
|
368
|
+
if (!searchConfig || !searchQuery.trim() || !searchableCollection) {
|
|
369
|
+
return items;
|
|
370
|
+
}
|
|
371
|
+
const results = searchableCollection.search(searchQuery, searchConfig.strategy);
|
|
372
|
+
const matchedIds = new Set(results.map((r) => r.id));
|
|
373
|
+
|
|
374
|
+
return items.filter((item) => {
|
|
375
|
+
if (item.type === "divider" || item.type === "header" || item.type === "custom") {
|
|
376
|
+
return false; // Hide during search
|
|
377
|
+
}
|
|
378
|
+
if (item.type === "action") return matchedIds.has(item.id);
|
|
379
|
+
if (item.type === "expandable") {
|
|
380
|
+
return (
|
|
381
|
+
matchedIds.has(item.id) ||
|
|
382
|
+
item.items.some((c) => c.type === "action" && matchedIds.has(c.id))
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
return false;
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Matched IDs for use in template filtering during search
|
|
390
|
+
let searchMatchedIds = $derived.by(() => {
|
|
391
|
+
if (!searchConfig || !searchQuery.trim() || !searchableCollection) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
return new Set(
|
|
395
|
+
searchableCollection.search(searchQuery, searchConfig.strategy).map((r) => r.id)
|
|
396
|
+
);
|
|
397
|
+
});
|
|
398
|
+
|
|
292
399
|
// Initialize expanded sections from defaultExpanded
|
|
293
400
|
$effect(() => {
|
|
294
401
|
const initial = new Set<string | number>();
|
|
@@ -307,10 +414,20 @@
|
|
|
307
414
|
});
|
|
308
415
|
let _navItems = $derived($navItems);
|
|
309
416
|
|
|
310
|
-
// Flatten navigable items (respects expanded state)
|
|
417
|
+
// Flatten navigable items (respects expanded state and search filter)
|
|
311
418
|
let navigableItems = $derived.by(() => {
|
|
419
|
+
const sourceItems = searchConfig && searchQuery.trim() ? filteredItems : items;
|
|
312
420
|
const flat: NavigableItem[] = [];
|
|
313
|
-
|
|
421
|
+
|
|
422
|
+
// When searching, get matched IDs to filter expandable children
|
|
423
|
+
const matchedIds =
|
|
424
|
+
searchConfig && searchQuery.trim() && searchableCollection
|
|
425
|
+
? new Set(
|
|
426
|
+
searchableCollection.search(searchQuery, searchConfig.strategy).map((r) => r.id)
|
|
427
|
+
)
|
|
428
|
+
: null;
|
|
429
|
+
|
|
430
|
+
for (const item of sourceItems) {
|
|
314
431
|
if (item.type === "action" && !item.disabled) {
|
|
315
432
|
flat.push(item);
|
|
316
433
|
} else if (item.type === "expandable" && !item.disabled) {
|
|
@@ -320,7 +437,14 @@
|
|
|
320
437
|
if (expandedSections.has(item.id)) {
|
|
321
438
|
for (const child of item.items) {
|
|
322
439
|
if (child.type === "action" && !child.disabled) {
|
|
323
|
-
|
|
440
|
+
// During search, only include matched children
|
|
441
|
+
if (matchedIds) {
|
|
442
|
+
if (matchedIds.has(child.id)) {
|
|
443
|
+
flat.push(child);
|
|
444
|
+
}
|
|
445
|
+
} else {
|
|
446
|
+
flat.push(child);
|
|
447
|
+
}
|
|
324
448
|
}
|
|
325
449
|
}
|
|
326
450
|
}
|
|
@@ -350,6 +474,45 @@
|
|
|
350
474
|
}
|
|
351
475
|
});
|
|
352
476
|
|
|
477
|
+
// Auto-focus search input when menu opens
|
|
478
|
+
$effect(() => {
|
|
479
|
+
if (isOpen && searchConfig?.autoFocus && searchInputEl) {
|
|
480
|
+
requestAnimationFrame(() => searchInputEl?.focus());
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
// Reset search when menu closes
|
|
485
|
+
$effect(() => {
|
|
486
|
+
if (!isOpen) {
|
|
487
|
+
searchQuery = "";
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
// Auto-expand sections with matching children during search
|
|
492
|
+
$effect(() => {
|
|
493
|
+
if (!searchConfig || !searchQuery.trim() || !searchableCollection) return;
|
|
494
|
+
const matchedIds = new Set(
|
|
495
|
+
searchableCollection.search(searchQuery, searchConfig.strategy).map((r) => r.id)
|
|
496
|
+
);
|
|
497
|
+
// Use untrack to avoid infinite loop (read without creating dependency)
|
|
498
|
+
const currentExpanded = untrack(() => expandedSections);
|
|
499
|
+
const newExpanded = new Set(currentExpanded);
|
|
500
|
+
let hasChanges = false;
|
|
501
|
+
for (const item of items) {
|
|
502
|
+
if (item.type === "expandable") {
|
|
503
|
+
if (item.items.some((c) => c.type === "action" && matchedIds.has(c.id))) {
|
|
504
|
+
if (!newExpanded.has(item.id)) {
|
|
505
|
+
newExpanded.add(item.id);
|
|
506
|
+
hasChanges = true;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (hasChanges) {
|
|
512
|
+
expandedSections = newExpanded;
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
353
516
|
// Reset runtime fallback when menu closes
|
|
354
517
|
$effect(() => {
|
|
355
518
|
if (!isOpen) {
|
|
@@ -475,28 +638,57 @@
|
|
|
475
638
|
}
|
|
476
639
|
}
|
|
477
640
|
|
|
641
|
+
// Handle keyboard events in search input
|
|
642
|
+
function handleSearchKeydown(e: KeyboardEvent) {
|
|
643
|
+
if (e.key === "Escape" && searchQuery) {
|
|
644
|
+
e.preventDefault();
|
|
645
|
+
e.stopPropagation();
|
|
646
|
+
searchQuery = "";
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (e.key === "ArrowDown") {
|
|
650
|
+
e.preventDefault();
|
|
651
|
+
e.stopPropagation();
|
|
652
|
+
navItems.setActiveFirst();
|
|
653
|
+
}
|
|
654
|
+
if (e.key === "Enter" && filteredItems.length > 0) {
|
|
655
|
+
e.preventDefault();
|
|
656
|
+
const first = filteredItems.find(
|
|
657
|
+
(i): i is DropdownMenuActionItem => i.type === "action" && !i.disabled
|
|
658
|
+
);
|
|
659
|
+
if (first) selectItem(first);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
478
663
|
// Computed transition duration
|
|
479
664
|
let transitionDuration = $derived(reducedMotion.current ? 0 : 100);
|
|
480
665
|
|
|
481
666
|
// Position styles for CSS Anchor Positioning
|
|
482
667
|
let dropdownStyle = $derived.by(() => {
|
|
483
668
|
if (isSupported) {
|
|
669
|
+
// Use fixed height when search is enabled AND position is a "top" variant
|
|
670
|
+
// to prevent jarring resize during filtering (dropdown grows upward)
|
|
671
|
+
const isTopPosition = position.startsWith("top");
|
|
672
|
+
const heightStyle =
|
|
673
|
+
searchConfig && isTopPosition ? `height: ${maxHeight};` : `max-height: ${maxHeight};`;
|
|
484
674
|
return `
|
|
485
675
|
position: fixed;
|
|
486
676
|
position-anchor: ${anchorName};
|
|
487
677
|
position-area: ${POSITION_MAP[position] || "bottom"};
|
|
488
678
|
margin: ${offset};
|
|
489
|
-
|
|
679
|
+
${heightStyle}
|
|
490
680
|
`;
|
|
491
681
|
} else {
|
|
492
682
|
// Fallback: centered modal overlay
|
|
683
|
+
// Use fixed height when search is enabled to prevent jarring resize during filtering
|
|
684
|
+
const heightStyle = searchConfig ? `height: ${maxHeight};` : `max-height: ${maxHeight};`;
|
|
493
685
|
return `
|
|
494
686
|
position: fixed;
|
|
495
687
|
top: 50%;
|
|
496
688
|
left: 50%;
|
|
497
689
|
transform: translate(-50%, -50%);
|
|
498
690
|
max-width: 90vw;
|
|
499
|
-
|
|
691
|
+
${heightStyle}
|
|
500
692
|
z-index: 50;
|
|
501
693
|
`;
|
|
502
694
|
}
|
|
@@ -510,7 +702,13 @@
|
|
|
510
702
|
if (["ArrowDown", "ArrowUp"].includes(e.key)) {
|
|
511
703
|
e.preventDefault();
|
|
512
704
|
if (e.key === "ArrowUp") {
|
|
513
|
-
|
|
705
|
+
// Check if at first item and search is active - return to search input
|
|
706
|
+
if (searchConfig && _navItems.active?.id === navigableItems[0]?.id) {
|
|
707
|
+
navItems.unsetActive();
|
|
708
|
+
searchInputEl?.focus();
|
|
709
|
+
} else {
|
|
710
|
+
e.metaKey ? navItems.setActiveFirst() : navItems.setActivePrevious();
|
|
711
|
+
}
|
|
514
712
|
} else {
|
|
515
713
|
e.metaKey ? navItems.setActiveLast() : navItems.setActiveNext();
|
|
516
714
|
}
|
|
@@ -521,6 +719,8 @@
|
|
|
521
719
|
e.preventDefault();
|
|
522
720
|
navItems.setActiveLast();
|
|
523
721
|
} else if (["Enter", " "].includes(e.key)) {
|
|
722
|
+
// Don't intercept space when typing in search input
|
|
723
|
+
if (e.key === " " && e.target === searchInputEl) return;
|
|
524
724
|
e.preventDefault();
|
|
525
725
|
const active = _navItems.active;
|
|
526
726
|
if (active) {
|
|
@@ -653,29 +853,65 @@
|
|
|
653
853
|
</button>
|
|
654
854
|
</div>
|
|
655
855
|
{/if}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
<
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
<
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
856
|
+
<!-- Search input -->
|
|
857
|
+
{#if searchConfig}
|
|
858
|
+
<div class={twMerge("stuic-dropdown-menu-search", classSearchContainer)}>
|
|
859
|
+
<span class="stuic-dropdown-menu-search-icon">
|
|
860
|
+
{@html iconSearch({ size: 16 })}
|
|
861
|
+
</span>
|
|
862
|
+
<input
|
|
863
|
+
bind:this={searchInputEl}
|
|
864
|
+
bind:value={searchQuery}
|
|
865
|
+
type="text"
|
|
866
|
+
placeholder={searchConfig.placeholder}
|
|
867
|
+
class={twMerge("stuic-dropdown-menu-search-input", classSearchInput)}
|
|
868
|
+
autocomplete="off"
|
|
869
|
+
onfocus={() => navItems.unsetActive()}
|
|
870
|
+
onkeydown={handleSearchKeydown}
|
|
871
|
+
aria-label="Search menu items"
|
|
872
|
+
/>
|
|
873
|
+
{#if searchQuery}
|
|
874
|
+
<button
|
|
875
|
+
type="button"
|
|
876
|
+
class="stuic-dropdown-menu-search-clear"
|
|
877
|
+
onclick={() => {
|
|
878
|
+
searchQuery = "";
|
|
879
|
+
searchInputEl?.focus();
|
|
880
|
+
}}
|
|
881
|
+
aria-label="Clear search"
|
|
882
|
+
>
|
|
883
|
+
{@html iconX({ size: 14 })}
|
|
884
|
+
</button>
|
|
885
|
+
{/if}
|
|
886
|
+
</div>
|
|
887
|
+
{/if}
|
|
888
|
+
<!-- Menu items -->
|
|
889
|
+
{#if searchConfig && searchQuery && filteredItems.length === 0}
|
|
890
|
+
<div class="stuic-dropdown-menu-no-results">{searchConfig.noResultsMessage}</div>
|
|
891
|
+
{:else}
|
|
892
|
+
{#each filteredItems as item}
|
|
893
|
+
{#if item.type === "action"}
|
|
894
|
+
{@const isActive = _navItems.active?.id === item.id}
|
|
895
|
+
<ListItemButton
|
|
896
|
+
id={itemId(item.id)}
|
|
897
|
+
role="menuitem"
|
|
898
|
+
focused={isActive}
|
|
899
|
+
contentBefore={item.contentBefore}
|
|
900
|
+
contentAfter={item.contentAfter}
|
|
901
|
+
class={twMerge(classItem, item.class)}
|
|
902
|
+
classFocused={classItemActive}
|
|
903
|
+
classContentBefore={classItemBefore}
|
|
904
|
+
classContentAfter={classItemAfter}
|
|
905
|
+
onclick={() => selectItem(item)}
|
|
906
|
+
onmouseenter={() => navItems.setActive(item)}
|
|
907
|
+
disabled={item.disabled}
|
|
908
|
+
tabindex={-1}
|
|
909
|
+
>
|
|
910
|
+
<Thc thc={item.label} />
|
|
911
|
+
</ListItemButton>
|
|
912
|
+
{:else if item.type === "divider"}
|
|
913
|
+
<div
|
|
914
|
+
role="separator"
|
|
679
915
|
class={twMerge(DROPDOWN_MENU_DIVIDER_CLASSES, classDivider, item.class)}
|
|
680
916
|
></div>
|
|
681
917
|
{:else if item.type === "header"}
|
|
@@ -731,55 +967,62 @@
|
|
|
731
967
|
>
|
|
732
968
|
{#each item.items as childItem}
|
|
733
969
|
{#if childItem.type === "action"}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
id
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
970
|
+
<!-- During search, only show matched action items -->
|
|
971
|
+
{#if !searchMatchedIds || searchMatchedIds.has(childItem.id)}
|
|
972
|
+
{@const isChildActive = _navItems.active?.id === childItem.id}
|
|
973
|
+
<ListItemButton
|
|
974
|
+
id={itemId(childItem.id)}
|
|
975
|
+
role="menuitem"
|
|
976
|
+
focused={isChildActive}
|
|
977
|
+
contentBefore={childItem.contentBefore}
|
|
978
|
+
contentAfter={childItem.contentAfter}
|
|
979
|
+
class={twMerge(classItem, childItem.class)}
|
|
980
|
+
classFocused={classItemActive}
|
|
981
|
+
classContentBefore={classItemBefore}
|
|
982
|
+
classContentAfter={classItemAfter}
|
|
983
|
+
onclick={() => selectItem(childItem)}
|
|
984
|
+
onmouseenter={() => navItems.setActive(childItem)}
|
|
985
|
+
disabled={childItem.disabled}
|
|
986
|
+
tabindex={-1}
|
|
987
|
+
>
|
|
988
|
+
<Thc thc={childItem.label} />
|
|
989
|
+
</ListItemButton>
|
|
990
|
+
{/if}
|
|
991
|
+
{:else if !searchMatchedIds}
|
|
992
|
+
<!-- Only show non-action items (divider, header, custom) when NOT searching -->
|
|
993
|
+
{#if childItem.type === "divider"}
|
|
994
|
+
<div
|
|
995
|
+
role="separator"
|
|
996
|
+
class={twMerge(
|
|
997
|
+
DROPDOWN_MENU_DIVIDER_CLASSES,
|
|
998
|
+
classDivider,
|
|
999
|
+
childItem.class
|
|
1000
|
+
)}
|
|
1001
|
+
></div>
|
|
1002
|
+
{:else if childItem.type === "header"}
|
|
1003
|
+
<div
|
|
1004
|
+
role="presentation"
|
|
1005
|
+
class={twMerge(
|
|
1006
|
+
DROPDOWN_MENU_HEADER_CLASSES,
|
|
1007
|
+
classHeader,
|
|
1008
|
+
childItem.class
|
|
1009
|
+
)}
|
|
1010
|
+
>
|
|
1011
|
+
<Thc thc={childItem.label} />
|
|
1012
|
+
</div>
|
|
1013
|
+
{:else if childItem.type === "custom"}
|
|
1014
|
+
<div role="presentation" class={childItem.class}>
|
|
1015
|
+
<Thc thc={childItem.content} />
|
|
1016
|
+
</div>
|
|
1017
|
+
{/if}
|
|
776
1018
|
{/if}
|
|
777
1019
|
{/each}
|
|
778
1020
|
</div>
|
|
779
1021
|
{/if}
|
|
780
1022
|
</div>
|
|
781
1023
|
{/if}
|
|
782
|
-
|
|
1024
|
+
{/each}
|
|
1025
|
+
{/if}
|
|
783
1026
|
</div>
|
|
784
1027
|
{/if}
|
|
785
1028
|
</div>
|
|
@@ -72,6 +72,19 @@ export interface NavigableExpandable {
|
|
|
72
72
|
expandableItem: DropdownMenuExpandableItem;
|
|
73
73
|
}
|
|
74
74
|
export type NavigableItem = DropdownMenuActionItem | NavigableExpandable;
|
|
75
|
+
/** Search configuration options */
|
|
76
|
+
export interface DropdownMenuSearchConfig {
|
|
77
|
+
/** Placeholder text for search input */
|
|
78
|
+
placeholder?: string;
|
|
79
|
+
/** Search strategy */
|
|
80
|
+
strategy?: "prefix" | "exact" | "fuzzy";
|
|
81
|
+
/** Custom function to extract searchable text from an item */
|
|
82
|
+
getContent?: (item: DropdownMenuActionItem | DropdownMenuExpandableItem) => string;
|
|
83
|
+
/** Auto-focus search input when menu opens */
|
|
84
|
+
autoFocus?: boolean;
|
|
85
|
+
/** Message shown when no results found */
|
|
86
|
+
noResultsMessage?: string;
|
|
87
|
+
}
|
|
75
88
|
export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
76
89
|
/** Menu items to display */
|
|
77
90
|
items: DropdownMenuItem[];
|
|
@@ -91,6 +104,8 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
91
104
|
closeOnEscape?: boolean;
|
|
92
105
|
/** Force fallback positioning mode (for testing) */
|
|
93
106
|
forceFallback?: boolean;
|
|
107
|
+
/** Enable search/filter functionality (true for defaults, or config object) */
|
|
108
|
+
search?: boolean | DropdownMenuSearchConfig;
|
|
94
109
|
/** Classes for the wrapper element */
|
|
95
110
|
class?: string;
|
|
96
111
|
/** Classes for the trigger button */
|
|
@@ -117,6 +132,10 @@ export interface Props extends Omit<HTMLButtonAttributes, "children"> {
|
|
|
117
132
|
classExpandableContent?: string;
|
|
118
133
|
/** Classes for backdrop (fallback mode only) */
|
|
119
134
|
classBackdrop?: string;
|
|
135
|
+
/** Classes for search input container */
|
|
136
|
+
classSearchContainer?: string;
|
|
137
|
+
/** Classes for search input */
|
|
138
|
+
classSearchInput?: string;
|
|
120
139
|
/** Show backdrop in fallback mode (default: true) */
|
|
121
140
|
showBackdrop?: boolean;
|
|
122
141
|
/** Custom trigger snippet - receives isOpen state, toggle function, and ARIA props for full control */
|
|
@@ -153,7 +172,6 @@ export declare const DROPDOWN_MENU_DROPDOWN_CLASSES = "stuic-dropdown-menu-dropd
|
|
|
153
172
|
export declare const DROPDOWN_MENU_DIVIDER_CLASSES = "stuic-dropdown-menu-divider";
|
|
154
173
|
export declare const DROPDOWN_MENU_HEADER_CLASSES = "stuic-dropdown-menu-header";
|
|
155
174
|
export declare const DROPDOWN_MENU_BACKDROP_CLASSES = "stuic-dropdown-menu-backdrop";
|
|
156
|
-
import "./index.css";
|
|
157
175
|
declare const DropdownMenu: import("svelte").Component<Props, {}, "isOpen" | "triggerEl" | "dropdownEl">;
|
|
158
176
|
type DropdownMenu = ReturnType<typeof DropdownMenu>;
|
|
159
177
|
export default DropdownMenu;
|