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.
- package/dist/CircleSpinner.svelte +29 -0
- package/dist/CircleSpinner.svelte.d.ts +8 -0
- package/dist/CmdPalette.svelte +74 -0
- package/dist/CmdPalette.svelte.d.ts +76 -0
- package/dist/CodeExample.svelte +85 -0
- package/dist/CodeExample.svelte.d.ts +25 -0
- package/dist/CopyButton.svelte +67 -0
- package/dist/CopyButton.svelte.d.ts +25 -0
- package/dist/FileDetails.svelte +65 -0
- package/dist/FileDetails.svelte.d.ts +22 -0
- package/dist/GitHubCorner.svelte +82 -0
- package/dist/GitHubCorner.svelte.d.ts +13 -0
- package/dist/Icon.svelte +23 -0
- package/dist/Icon.svelte.d.ts +8 -0
- package/dist/MultiSelect.svelte +1725 -0
- package/dist/MultiSelect.svelte.d.ts +25 -0
- package/dist/Nav.svelte +627 -0
- package/dist/Nav.svelte.d.ts +43 -0
- package/dist/PrevNext.svelte +105 -0
- package/dist/PrevNext.svelte.d.ts +56 -0
- package/dist/Toggle.svelte +77 -0
- package/dist/Toggle.svelte.d.ts +11 -0
- package/dist/Wiggle.svelte +22 -0
- package/dist/Wiggle.svelte.d.ts +18 -0
- package/dist/attachments.d.ts +72 -0
- package/dist/attachments.js +698 -0
- package/dist/heading-anchors.d.ts +14 -0
- package/dist/heading-anchors.js +120 -0
- package/dist/icons.d.ts +55 -0
- package/dist/icons.js +54 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +43 -0
- package/dist/types.d.ts +246 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +63 -0
- package/package.json +20 -17
- package/readme.md +25 -1
|
@@ -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;
|
package/dist/Nav.svelte
ADDED
|
@@ -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;
|