svelte-multiselect 11.5.0 → 11.5.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.
@@ -0,0 +1,25 @@
1
+ import type { MultiSelectProps } from './types';
2
+ declare function $$render<Option extends import('./types').Option>(): {
3
+ props: MultiSelectProps<Option>;
4
+ exports: {};
5
+ bindings: "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText" | "collapsedGroups" | "collapseAllGroups" | "expandAllGroups";
6
+ slots: {};
7
+ events: {};
8
+ };
9
+ declare class __sveltets_Render<Option extends import('./types').Option> {
10
+ props(): ReturnType<typeof $$render<Option>>['props'];
11
+ events(): ReturnType<typeof $$render<Option>>['events'];
12
+ slots(): ReturnType<typeof $$render<Option>>['slots'];
13
+ bindings(): "input" | "invalid" | "value" | "selected" | "open" | "activeIndex" | "activeOption" | "form_input" | "matchingOptions" | "maxSelect" | "options" | "outerDiv" | "searchText" | "collapsedGroups" | "collapseAllGroups" | "expandAllGroups";
14
+ exports(): {};
15
+ }
16
+ interface $$IsomorphicComponent {
17
+ new <Option extends import('./types').Option>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<Option>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<Option>['props']>, ReturnType<__sveltets_Render<Option>['events']>, ReturnType<__sveltets_Render<Option>['slots']>> & {
18
+ $$bindings?: ReturnType<__sveltets_Render<Option>['bindings']>;
19
+ } & ReturnType<__sveltets_Render<Option>['exports']>;
20
+ <Option extends import('./types').Option>(internal: unknown, props: ReturnType<__sveltets_Render<Option>['props']> & {}): ReturnType<__sveltets_Render<Option>['exports']>;
21
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
22
+ }
23
+ declare const MultiSelect: $$IsomorphicComponent;
24
+ type MultiSelect<Option extends import('./types').Option> = InstanceType<typeof MultiSelect<Option>>;
25
+ export default MultiSelect;
@@ -0,0 +1,627 @@
1
+ <script lang="ts">import { click_outside, tooltip } from './attachments';
2
+ import Icon from './Icon.svelte';
3
+ let { routes = [], children, item, link, menu_props, link_props, page, labels, tooltips, tooltip_options, breakpoint = 767, onnavigate, onopen, onclose, ...rest } = $props();
4
+ let is_open = $state(false);
5
+ let hovered_dropdown = $state(null);
6
+ let focused_item_index = $state(-1);
7
+ let is_touch_device = $state(false);
8
+ let is_mobile = $state(false);
9
+ const panel_id = `nav-menu-${crypto.randomUUID()}`;
10
+ // Track previous is_open state for callbacks
11
+ let prev_is_open = $state(false);
12
+ // Detect touch device and handle responsive breakpoint
13
+ $effect(() => {
14
+ if (typeof globalThis === `undefined`)
15
+ return;
16
+ is_touch_device = `ontouchstart` in globalThis || navigator.maxTouchPoints > 0;
17
+ // Handle responsive breakpoint via JS since CSS variables don't work in media queries
18
+ const check_mobile = () => {
19
+ is_mobile = globalThis.innerWidth <= breakpoint;
20
+ };
21
+ check_mobile();
22
+ globalThis.addEventListener(`resize`, check_mobile);
23
+ return () => globalThis.removeEventListener(`resize`, check_mobile);
24
+ });
25
+ // Call onopen/onclose callbacks when menu state changes
26
+ $effect(() => {
27
+ if (is_open && !prev_is_open) {
28
+ onopen?.();
29
+ }
30
+ else if (!is_open && prev_is_open) {
31
+ onclose?.();
32
+ }
33
+ prev_is_open = is_open;
34
+ });
35
+ function close_menus() {
36
+ is_open = false;
37
+ hovered_dropdown = null;
38
+ focused_item_index = -1;
39
+ }
40
+ function toggle_dropdown(href, focus_first = false) {
41
+ const is_opening = hovered_dropdown !== href;
42
+ hovered_dropdown = hovered_dropdown === href ? null : href;
43
+ focused_item_index = is_opening && focus_first ? 0 : -1;
44
+ // Focus management for keyboard users
45
+ if (is_opening && focus_first) {
46
+ setTimeout(() => {
47
+ document.querySelector(`.dropdown[data-href="${href}"] [role="menuitem"]`)?.focus();
48
+ }, 0);
49
+ }
50
+ }
51
+ function onkeydown(event) {
52
+ if (event.key === `Escape`)
53
+ close_menus();
54
+ }
55
+ function handle_dropdown_keydown(event, href, sub_routes) {
56
+ const { key } = event;
57
+ if (key === `Enter` || key === ` `) {
58
+ event.preventDefault();
59
+ toggle_dropdown(href, true);
60
+ return;
61
+ }
62
+ // Arrow key navigation within open dropdown
63
+ if (hovered_dropdown === href && (key === `ArrowDown` || key === `ArrowUp`)) {
64
+ event.preventDefault();
65
+ const direction = key === `ArrowDown` ? 1 : -1;
66
+ focused_item_index = Math.max(0, Math.min(sub_routes.length - 1, focused_item_index + direction));
67
+ document.querySelectorAll(`.dropdown[data-href="${href}"] [role="menuitem"]`)?.[focused_item_index]?.focus();
68
+ }
69
+ // Open dropdown with ArrowDown when closed
70
+ if (hovered_dropdown !== href && key === `ArrowDown`) {
71
+ event.preventDefault();
72
+ toggle_dropdown(href, true);
73
+ }
74
+ }
75
+ function handle_dropdown_item_keydown(event, href) {
76
+ if (event.key === `Escape`) {
77
+ event.preventDefault();
78
+ close_menus();
79
+ document.querySelector(`.dropdown[data-href="${href}"] [data-dropdown-toggle]`)?.focus();
80
+ }
81
+ }
82
+ function is_current(path) {
83
+ if (!path)
84
+ return undefined;
85
+ if (path === `/`)
86
+ return page?.url.pathname === `/` ? `page` : undefined;
87
+ // Match exact path or path followed by / to avoid partial matches
88
+ // e.g. /tc-periodic-v2 should not match /tc-periodic
89
+ const pathname = page?.url.pathname;
90
+ const exact_match = pathname === path;
91
+ const prefix_match = pathname?.startsWith(path + `/`);
92
+ return exact_match || prefix_match ? `page` : undefined;
93
+ }
94
+ const is_child_current = (sub_routes) => sub_routes.some((child_path) => is_current(child_path) === `page`);
95
+ function format_label(text, remove_parent = false) {
96
+ if (!text)
97
+ return { label: ``, style: `` };
98
+ const custom_label = labels?.[text];
99
+ if (custom_label)
100
+ return { label: custom_label, style: `` };
101
+ if (remove_parent)
102
+ text = text.split(`/`).filter(Boolean).pop() ?? text;
103
+ let label = text.replace(/^\//, ``).replaceAll(`-`, ` `);
104
+ // Handle root path '/' which becomes empty after stripping
105
+ if (!label && text === `/`)
106
+ label = `Home`;
107
+ return { label, style: label ? `text-transform: capitalize` : `` };
108
+ }
109
+ // Normalize all route formats to NavRouteObject
110
+ function parse_route(route) {
111
+ if (typeof route === `string`)
112
+ return { href: route };
113
+ if (Array.isArray(route)) {
114
+ const [href, second] = route;
115
+ return Array.isArray(second)
116
+ ? { href, children: second }
117
+ : { href, label: second };
118
+ }
119
+ return route;
120
+ }
121
+ function get_tooltip(route) {
122
+ // Priority: disabled message > route.tooltip > tooltips[href]
123
+ if (typeof route.disabled === `string`) {
124
+ return tooltip({ ...tooltip_options, content: route.disabled });
125
+ }
126
+ const content = route.tooltip ?? tooltips?.[route.href];
127
+ if (!content)
128
+ return undefined;
129
+ // Support both string (content only) and object (full options) formats
130
+ const opts = typeof content === `string` ? { content } : content;
131
+ return tooltip({ ...tooltip_options, ...opts });
132
+ }
133
+ // Handle link click with onnavigate callback
134
+ function handle_link_click(event, route) {
135
+ if (route.disabled) {
136
+ event.preventDefault();
137
+ return;
138
+ }
139
+ if (onnavigate) {
140
+ const result = onnavigate({ href: route.href, event, route });
141
+ if (result === false) {
142
+ event.preventDefault();
143
+ return;
144
+ }
145
+ }
146
+ close_menus();
147
+ }
148
+ // Get external link attributes
149
+ function get_external_attrs(route) {
150
+ if (!route.external)
151
+ return {};
152
+ return { target: `_blank`, rel: `noopener noreferrer` };
153
+ }
154
+ </script>
155
+
156
+ <svelte:window {onkeydown} />
157
+
158
+ <!-- Default item rendering snippet for escape hatch -->
159
+ {#snippet default_item_render(
160
+ parsed_route: NavRouteObject,
161
+ formatted: { label: string; style: string },
162
+ item_tooltip: ReturnType<typeof tooltip> | undefined,
163
+ )}
164
+ {@const is_disabled = Boolean(parsed_route.disabled)}
165
+ {#if is_disabled}
166
+ <span
167
+ class="disabled {parsed_route.class ?? ``}"
168
+ style={`${formatted.style}; ${parsed_route.style ?? ``}`}
169
+ aria-disabled="true"
170
+ {@attach item_tooltip}
171
+ >{@html formatted.label}</span>
172
+ {:else if link}
173
+ {@render link({ href: parsed_route.href, label: formatted.label })}
174
+ {:else}
175
+ <a
176
+ href={parsed_route.href}
177
+ aria-current={is_current(parsed_route.href)}
178
+ onclick={(event) => handle_link_click(event, parsed_route)}
179
+ class={parsed_route.class}
180
+ {...link_props}
181
+ {...get_external_attrs(parsed_route)}
182
+ style={`${formatted.style}; ${link_props?.style ?? ``}; ${parsed_route.style ?? ``}`}
183
+ {@attach item_tooltip}
184
+ >
185
+ {@html formatted.label}
186
+ </a>
187
+ {/if}
188
+ {/snippet}
189
+
190
+ <nav
191
+ {...rest}
192
+ class:mobile={is_mobile}
193
+ {@attach click_outside({ callback: close_menus })}
194
+ >
195
+ <button
196
+ class="burger"
197
+ type="button"
198
+ onclick={() => is_open = !is_open}
199
+ aria-label="Toggle navigation menu"
200
+ aria-expanded={is_open}
201
+ aria-controls={panel_id}
202
+ >
203
+ <span aria-hidden="true"></span>
204
+ <span aria-hidden="true"></span>
205
+ <span aria-hidden="true"></span>
206
+ </button>
207
+
208
+ <div
209
+ id={panel_id}
210
+ class="menu"
211
+ class:open={is_open}
212
+ tabindex="0"
213
+ role="menu"
214
+ {onkeydown}
215
+ {...menu_props}
216
+ >
217
+ {#each routes as
218
+ route,
219
+ route_idx
220
+ (`${route_idx}-${
221
+ typeof route === `string`
222
+ ? route
223
+ : Array.isArray(route)
224
+ ? route[0]
225
+ : route.href ?? `sep-${route_idx}`
226
+ }`)
227
+ }
228
+ {@const parsed_route = parse_route(route)}
229
+ {@const formatted = format_label(parsed_route.label ?? parsed_route.href)}
230
+ {@const sub_routes = parsed_route.children}
231
+ {@const is_active = is_current(parsed_route.href) === `page`}
232
+ {@const is_dropdown = Boolean(sub_routes)}
233
+ {@const is_right = parsed_route.align === `right`}
234
+ {@const item_tooltip = get_tooltip(parsed_route)}
235
+
236
+ <!-- Separator-only item -->
237
+ {#if parsed_route.separator && !parsed_route.href}
238
+ <div class="separator" role="separator"></div>
239
+ {:else if sub_routes}
240
+ <!-- Dropdown menu item -->
241
+ {@const child_is_active = is_child_current(sub_routes)}
242
+ {@const parent_page_exists = sub_routes.includes(parsed_route.href)}
243
+ {@const filtered_sub_routes = sub_routes.filter((r) => r !== parsed_route.href)}
244
+ <div
245
+ class="dropdown"
246
+ class:active={child_is_active}
247
+ class:align-right={is_right}
248
+ data-href={parsed_route.href}
249
+ role="group"
250
+ aria-current={child_is_active ? `true` : undefined}
251
+ onmouseenter={() => !is_touch_device && (hovered_dropdown = parsed_route.href)}
252
+ onmouseleave={() => !is_touch_device && (hovered_dropdown = null)}
253
+ onfocusin={() => (hovered_dropdown = parsed_route.href)}
254
+ onfocusout={(event) => {
255
+ const next = event.relatedTarget as Node | null
256
+ if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
257
+ hovered_dropdown = null
258
+ }
259
+ }}
260
+ >
261
+ <div>
262
+ {#if parsed_route.disabled}
263
+ <span
264
+ class="disabled {parsed_route.class ?? ``}"
265
+ style={`${formatted.style}; ${parsed_route.style ?? ``}`}
266
+ aria-disabled="true"
267
+ {@attach item_tooltip}
268
+ >{@html formatted.label}</span>
269
+ {:else if parent_page_exists}
270
+ <a
271
+ href={parsed_route.href}
272
+ aria-current={is_current(parsed_route.href)}
273
+ onclick={(event) => handle_link_click(event, parsed_route)}
274
+ class={parsed_route.class}
275
+ style={`${formatted.style}; ${parsed_route.style ?? ``}`}
276
+ {...get_external_attrs(parsed_route)}
277
+ {@attach item_tooltip}
278
+ >
279
+ {@html formatted.label}
280
+ </a>
281
+ {:else}
282
+ <span
283
+ class={parsed_route.class}
284
+ style={`${formatted.style}; ${parsed_route.style ?? ``}`}
285
+ {@attach item_tooltip}
286
+ >{@html formatted.label}</span>
287
+ {/if}
288
+ <button
289
+ type="button"
290
+ data-dropdown-toggle
291
+ aria-label="Toggle {formatted.label} submenu"
292
+ aria-expanded={hovered_dropdown === parsed_route.href}
293
+ aria-haspopup="true"
294
+ onclick={() => toggle_dropdown(parsed_route.href, false)}
295
+ onkeydown={(event) =>
296
+ handle_dropdown_keydown(
297
+ event,
298
+ parsed_route.href,
299
+ filtered_sub_routes,
300
+ )}
301
+ >
302
+ <Icon
303
+ icon="ChevronExpand"
304
+ style="width: 0.8em; height: 0.8em"
305
+ />
306
+ </button>
307
+ </div>
308
+ <div
309
+ class:visible={hovered_dropdown === parsed_route.href}
310
+ role="menu"
311
+ tabindex="-1"
312
+ onmouseenter={() => !is_touch_device && (hovered_dropdown = parsed_route.href)}
313
+ onmouseleave={() => !is_touch_device && (hovered_dropdown = null)}
314
+ onfocusin={() => (hovered_dropdown = parsed_route.href)}
315
+ onfocusout={(event) => {
316
+ const next = event.relatedTarget as Node | null
317
+ if (!next || !(event.currentTarget as HTMLElement).contains(next)) {
318
+ hovered_dropdown = null
319
+ }
320
+ }}
321
+ >
322
+ {#each filtered_sub_routes as child_href (child_href)}
323
+ {@const child_formatted = format_label(child_href, true)}
324
+ {@const child_tooltip = get_tooltip({ href: child_href })}
325
+ {#if link}
326
+ {@render link({ href: child_href, label: child_formatted.label })}
327
+ {:else}
328
+ <a
329
+ href={child_href}
330
+ role="menuitem"
331
+ aria-current={is_current(child_href)}
332
+ onclick={(event) => handle_link_click(event, { href: child_href })}
333
+ onkeydown={(event) => handle_dropdown_item_keydown(event, parsed_route.href)}
334
+ {...link_props}
335
+ style={`${child_formatted.style}; ${link_props?.style ?? ``}`}
336
+ {@attach child_tooltip}
337
+ >
338
+ {@html child_formatted.label}
339
+ </a>
340
+ {/if}
341
+ {/each}
342
+ </div>
343
+ </div>
344
+ <!-- Separator after dropdown if specified -->
345
+ {#if parsed_route.separator}
346
+ <div class="separator" role="separator"></div>
347
+ {/if}
348
+ {:else}
349
+ <!-- Regular link item -->
350
+ {#if item}
351
+ <!-- User-provided item snippet with render_default escape hatch -->
352
+ {#snippet render_default_snippet()}
353
+ {@render default_item_render(parsed_route, formatted, item_tooltip)}
354
+ {/snippet}
355
+ <span class:align-right={is_right}>
356
+ {@render item({
357
+ route: parsed_route,
358
+ href: parsed_route.href,
359
+ label: formatted.label,
360
+ is_active,
361
+ is_dropdown,
362
+ render_default: render_default_snippet,
363
+ })}
364
+ </span>
365
+ {:else}
366
+ <span class:align-right={is_right}>
367
+ {@render default_item_render(parsed_route, formatted, item_tooltip)}
368
+ </span>
369
+ {/if}
370
+ <!-- Separator after item if specified -->
371
+ {#if parsed_route.separator}
372
+ <div class="separator" role="separator"></div>
373
+ {/if}
374
+ {/if}
375
+ {/each}
376
+
377
+ {@render children?.({ is_open, panel_id, routes })}
378
+ </div>
379
+ </nav>
380
+
381
+ <style>
382
+ nav {
383
+ position: relative;
384
+ margin: -0.75em auto 1.25em;
385
+ --nav-border-radius: 6pt;
386
+ --nav-surface-bg: light-dark(#fafafa, #1a1a1a);
387
+ --nav-surface-border: light-dark(rgba(128, 128, 128, 0.25), rgba(200, 200, 200, 0.2));
388
+ --nav-surface-shadow: light-dark(
389
+ 0 2px 8px rgba(0, 0, 0, 0.15),
390
+ 0 4px 12px rgba(0, 0, 0, 0.5)
391
+ );
392
+ }
393
+ .menu {
394
+ display: flex;
395
+ gap: 1em;
396
+ place-content: center;
397
+ place-items: center;
398
+ flex-wrap: wrap;
399
+ padding: 0.5em;
400
+ }
401
+ .menu > span,
402
+ .menu > span > a {
403
+ line-height: 1.3;
404
+ padding: 1pt 5pt;
405
+ border-radius: var(--nav-border-radius);
406
+ text-decoration: none;
407
+ color: inherit;
408
+ transition: background-color 0.2s;
409
+ }
410
+ .menu > span > a:hover {
411
+ background-color: var(--nav-link-bg-hover);
412
+ }
413
+ .menu > span > a[aria-current='page'] {
414
+ color: var(--nav-link-active-color);
415
+ }
416
+ /* Disabled items */
417
+ .menu .disabled {
418
+ opacity: var(--nav-disabled-opacity, 0.5);
419
+ cursor: not-allowed;
420
+ pointer-events: none;
421
+ }
422
+ /* Right-aligned items - only first one gets margin-left: auto */
423
+ .menu > .align-right,
424
+ .menu > .dropdown.align-right {
425
+ margin-left: auto;
426
+ }
427
+ .menu > .align-right + .align-right,
428
+ .menu > .align-right + .dropdown.align-right,
429
+ .menu > .dropdown.align-right + .align-right,
430
+ .menu > .dropdown.align-right + .dropdown.align-right {
431
+ margin-left: 0;
432
+ }
433
+ /* Separator */
434
+ .menu > .separator {
435
+ width: 1px;
436
+ height: 1.2em;
437
+ background-color: var(--nav-separator-color, currentColor);
438
+ opacity: 0.3;
439
+ margin: var(--nav-separator-margin, 0 0.25em);
440
+ }
441
+ /* Dropdown styles */
442
+ .dropdown {
443
+ position: relative;
444
+ }
445
+ .dropdown.active > div:first-child a,
446
+ .dropdown.active > div:first-child span {
447
+ color: var(--nav-link-active-color);
448
+ }
449
+ .dropdown::after {
450
+ content: '';
451
+ position: absolute;
452
+ top: 100%;
453
+ left: 0;
454
+ right: 0;
455
+ height: var(--nav-dropdown-margin, 3pt);
456
+ }
457
+ .dropdown > div:first-child {
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 0;
461
+ border-radius: var(--nav-border-radius);
462
+ transition: background-color 0.2s;
463
+ }
464
+ .dropdown > div:first-child:hover {
465
+ background-color: var(--nav-link-bg-hover);
466
+ }
467
+ .dropdown > div:first-child > a,
468
+ .dropdown > div:first-child > span {
469
+ line-height: 1.3;
470
+ padding: 1pt 5pt;
471
+ text-decoration: none;
472
+ color: inherit;
473
+ border-radius: var(--nav-border-radius) 0 0 var(--nav-border-radius);
474
+ }
475
+ .dropdown > div:first-child > a[aria-current='page'] {
476
+ color: var(--nav-link-active-color);
477
+ }
478
+ .dropdown > div:first-child > button {
479
+ padding: 1pt 3pt;
480
+ border: none;
481
+ background: transparent;
482
+ color: inherit;
483
+ cursor: pointer;
484
+ display: flex;
485
+ align-items: center;
486
+ justify-content: center;
487
+ border-radius: 0 var(--nav-border-radius) var(--nav-border-radius) 0;
488
+ outline-offset: -1px;
489
+ }
490
+ .dropdown > div:first-child > button:focus-visible {
491
+ outline: 2px solid currentColor;
492
+ outline-offset: -2px;
493
+ }
494
+ .dropdown > div:last-child {
495
+ position: absolute;
496
+ top: 100%;
497
+ left: 0;
498
+ margin: var(--nav-dropdown-margin, 3pt 0 0 0);
499
+ min-width: max-content;
500
+ background-color: var(--nav-dropdown-bg, var(--nav-surface-bg));
501
+ border: 1px solid var(--nav-dropdown-border-color, var(--nav-surface-border));
502
+ border-radius: var(--nav-border-radius, 6pt);
503
+ box-shadow: var(--nav-dropdown-shadow, var(--nav-surface-shadow));
504
+ padding: var(--nav-dropdown-padding, 2pt 3pt);
505
+ display: none;
506
+ flex-direction: column;
507
+ gap: var(--nav-dropdown-gap, 5pt);
508
+ z-index: var(--nav-dropdown-z-index, 100);
509
+ }
510
+ .dropdown > div:last-child.visible {
511
+ display: flex;
512
+ }
513
+ .dropdown > div:last-child a {
514
+ padding: var(--nav-dropdown-link-padding, 1pt 4pt);
515
+ border-radius: var(--nav-border-radius);
516
+ text-decoration: none;
517
+ color: inherit;
518
+ white-space: nowrap;
519
+ transition: background-color 0.2s;
520
+ }
521
+ .dropdown > div:last-child a:hover {
522
+ background-color: var(--nav-link-bg-hover);
523
+ }
524
+ .dropdown > div:last-child a[aria-current='page'] {
525
+ color: var(--nav-link-active-color);
526
+ }
527
+ /* Mobile burger button */
528
+ .burger {
529
+ display: none;
530
+ position: fixed;
531
+ top: 1rem;
532
+ left: 1rem;
533
+ flex-direction: column;
534
+ justify-content: space-around;
535
+ width: 1.4rem;
536
+ height: 1.4rem;
537
+ background: transparent;
538
+ padding: 0;
539
+ z-index: var(--nav-toggle-btn-z-index, 10);
540
+ }
541
+ .burger span {
542
+ width: 100%;
543
+ height: 0.18rem;
544
+ background-color: var(--text);
545
+ border-radius: 8px;
546
+ transition: all 0.2s linear;
547
+ transform-origin: center;
548
+ }
549
+ .burger[aria-expanded='true'] span:first-child {
550
+ transform: translateY(0.4rem) rotate(45deg);
551
+ }
552
+ .burger[aria-expanded='true'] span:nth-child(2) {
553
+ opacity: 0;
554
+ }
555
+ .burger[aria-expanded='true'] span:nth-child(3) {
556
+ transform: translateY(-0.4rem) rotate(-45deg);
557
+ }
558
+ /* Mobile styles - using .mobile class set via JS based on breakpoint prop */
559
+ nav.mobile .burger {
560
+ display: flex;
561
+ }
562
+ nav.mobile .menu {
563
+ position: fixed;
564
+ top: 3rem;
565
+ left: 1rem;
566
+ background-color: var(--nav-surface-bg);
567
+ border: 1px solid var(--nav-surface-border);
568
+ box-shadow: var(--nav-surface-shadow);
569
+ opacity: 0;
570
+ visibility: hidden;
571
+ transition: all 0.3s ease;
572
+ z-index: var(--nav-mobile-z-index, 2);
573
+ flex-direction: column;
574
+ align-items: stretch;
575
+ justify-content: start;
576
+ gap: 0.2em;
577
+ max-width: 90vw;
578
+ border-radius: 6px;
579
+ }
580
+ nav.mobile .menu.open {
581
+ opacity: 1;
582
+ visibility: visible;
583
+ }
584
+ nav.mobile .menu > span,
585
+ nav.mobile .menu > span > a,
586
+ nav.mobile .dropdown {
587
+ padding: 2pt 8pt;
588
+ }
589
+ /* Mobile separator */
590
+ nav.mobile .menu > .separator {
591
+ width: 100%;
592
+ height: 1px;
593
+ margin: var(--nav-separator-margin, 0.25em 0);
594
+ }
595
+ /* Mobile dropdown styles - show as expandable section */
596
+ nav.mobile .dropdown {
597
+ flex-direction: column;
598
+ align-items: stretch;
599
+ }
600
+ nav.mobile .dropdown > div:first-child {
601
+ display: flex;
602
+ align-items: center;
603
+ justify-content: space-between;
604
+ }
605
+ nav.mobile .dropdown > div:first-child > a,
606
+ nav.mobile .dropdown > div:first-child > span {
607
+ flex: 1;
608
+ border-radius: var(--nav-border-radius);
609
+ }
610
+ nav.mobile .dropdown > div:first-child > button {
611
+ padding: 4pt 8pt;
612
+ border-radius: var(--nav-border-radius);
613
+ }
614
+ nav.mobile .dropdown > div:last-child {
615
+ position: static;
616
+ border: none;
617
+ box-shadow: none;
618
+ margin-top: 0.25em;
619
+ padding: 0 0 0 1em;
620
+ background-color: transparent;
621
+ }
622
+ /* Mobile right-aligned items stack normally */
623
+ nav.mobile .menu > .align-right,
624
+ nav.mobile .menu > .dropdown.align-right {
625
+ margin-left: 0;
626
+ }
627
+ </style>
@@ -0,0 +1,43 @@
1
+ import type { Page } from '@sveltejs/kit';
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { type TooltipOptions } from './attachments';
5
+ import type { NavRoute, NavRouteObject } from './types';
6
+ interface ItemSnippetParams {
7
+ route: NavRouteObject;
8
+ href: string;
9
+ label: string;
10
+ is_active: boolean;
11
+ is_dropdown: boolean;
12
+ render_default: Snippet;
13
+ }
14
+ type $$ComponentProps = {
15
+ routes: NavRoute[];
16
+ children?: Snippet<[{
17
+ is_open: boolean;
18
+ panel_id: string;
19
+ routes: NavRoute[];
20
+ }]>;
21
+ item?: Snippet<[ItemSnippetParams]>;
22
+ link?: Snippet<[{
23
+ href: string;
24
+ label: string;
25
+ }]>;
26
+ menu_props?: HTMLAttributes<HTMLDivElement>;
27
+ link_props?: HTMLAttributes<HTMLAnchorElement>;
28
+ page?: Page;
29
+ labels?: Record<string, string>;
30
+ tooltips?: Record<string, string | Omit<TooltipOptions, `disabled`>>;
31
+ tooltip_options?: Omit<TooltipOptions, `content`>;
32
+ breakpoint?: number;
33
+ onnavigate?: (data: {
34
+ href: string;
35
+ event: MouseEvent;
36
+ route: NavRouteObject;
37
+ }) => void | false;
38
+ onopen?: () => void;
39
+ onclose?: () => void;
40
+ } & Omit<HTMLAttributes<HTMLElementTagNameMap[`nav`]>, `children`>;
41
+ declare const Nav: import("svelte").Component<$$ComponentProps, {}, "">;
42
+ type Nav = ReturnType<typeof Nav>;
43
+ export default Nav;