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