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/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
- let { routes = [], children, link, menu_props, link_props, page, labels, ...rest } = $props();
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
- const panel_id = `nav-menu-${crypto.randomUUID()}`;
12
- // Detect touch device
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 !== `undefined`) {
15
- is_touch_device = `ontouchstart` in globalThis || navigator.maxTouchPoints > 0;
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 = hovered_dropdown !== href;
25
- hovered_dropdown = hovered_dropdown === href ? null : href;
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
- const dropdown = document.querySelector(`.dropdown[data-href="${href}"]`);
31
- const first_link = dropdown?.querySelector(`[role="menuitem"]`);
32
- first_link?.focus();
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 (hovered_dropdown === href && (key === `ArrowDown` || key === `ArrowUp`)) {
85
+ if (is_open && (key === `ArrowDown` || key === `ArrowUp`)) {
49
86
  event.preventDefault();
50
87
  const direction = key === `ArrowDown` ? 1 : -1;
51
- const new_index = Math.max(0, Math.min(sub_routes.length - 1, focused_item_index + direction));
52
- focused_item_index = new_index;
53
- const dropdown = document.querySelector(`.dropdown[data-href="${href}"]`);
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 (hovered_dropdown !== href && key === `ArrowDown`) {
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
- const label = text.replace(/^\//, ``).replaceAll(`-`, ` `);
92
- return { label, style: `text-transform: capitalize` };
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, label: route };
97
- const [first, second] = route;
98
- return Array.isArray(second)
99
- ? { href: first, label: first, children: second }
100
- : { href: first, label: second };
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 route (JSON.stringify(route))}
133
- {@const { href, label, children: sub_routes } = parse_route(route)}
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
- {#if sub_routes}
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((route) => route !== href)}
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
- data-href={href}
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={() => !is_touch_device && (hovered_dropdown = href)}
148
- onmouseleave={() => !is_touch_device && (hovered_dropdown = null)}
149
- onfocusin={() => (hovered_dropdown = href)}
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 parent_page_exists}
159
- {@const { label, style } = parent}
160
- <a {href} aria-current={is_current(href)} onclick={close_menus} {style}>
161
- {@html label}
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 style={parent.style}>{@html parent.label}</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 {parent.label} submenu"
170
- aria-expanded={hovered_dropdown === href}
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) => handle_dropdown_keydown(event, href, filtered_sub_routes)}
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={hovered_dropdown === href}
339
+ class:visible={dropdown_open}
183
340
  role="menu"
184
341
  tabindex="-1"
185
- onmouseenter={() => !is_touch_device && (hovered_dropdown = href)}
186
- onmouseleave={() => !is_touch_device && (hovered_dropdown = null)}
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 child = format_label(child_href, true)}
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: child.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={close_menus}
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={`${child.style}; ${link_props?.style ?? ``}`}
359
+ style={`${child_formatted.style}; ${link_props?.style ?? ``}`}
360
+ {@attach child_tooltip}
208
361
  >
209
- {@html child.label}
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
- {@const regular = format_label(label)}
218
- {#if link}
219
- {@render link({ href, label })}
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
- <a
222
- {href}
223
- aria-current={is_current(href)}
224
- onclick={close_menus}
225
- {...link_props}
226
- style={`${regular.style}; ${link_props?.style ?? ``}`}
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(rgba(128, 128, 128, 0.25), rgba(200, 200, 200, 0.2));
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 > a {
259
- line-height: 1.3;
260
- padding: 1pt 5pt;
428
+ .menu > span {
429
+ display: flex;
430
+ align-items: center;
261
431
  border-radius: var(--nav-border-radius);
262
- text-decoration: none;
263
- color: inherit;
432
+ background-color: var(--nav-link-bg);
264
433
  transition: background-color 0.2s;
265
434
  }
266
- .menu > a:hover {
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: 0;
286
- right: 0;
287
- height: var(--nav-dropdown-margin, 3pt);
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: 1pt 5pt;
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: 1pt 3pt;
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, 3pt 0 0 0);
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, 2pt 3pt);
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, 1pt 4pt);
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
- transition: background-color 0.2s;
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-color);
583
+ background-color: var(--text);
371
584
  border-radius: 8px;
372
585
  transition: all 0.2s linear;
373
- transform-origin: 1px;
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
- @media (max-width: 767px) {
386
- .burger {
387
- display: flex;
388
- }
389
- .menu {
390
- position: fixed;
391
- top: 3rem;
392
- left: 1rem;
393
- background-color: var(--nav-surface-bg);
394
- border: 1px solid var(--nav-surface-border);
395
- box-shadow: var(--nav-surface-shadow);
396
- opacity: 0;
397
- visibility: hidden;
398
- transition: all 0.3s ease;
399
- z-index: var(--nav-mobile-z-index, 2);
400
- flex-direction: column;
401
- align-items: stretch;
402
- justify-content: start;
403
- gap: 0.2em;
404
- max-width: 90vw;
405
- border-radius: 6px;
406
- }
407
- .menu.open {
408
- opacity: 1;
409
- visibility: visible;
410
- }
411
- .menu > a,
412
- .dropdown {
413
- padding: 2pt 8pt;
414
- }
415
-
416
- /* Mobile dropdown styles - show as expandable section */
417
- .dropdown {
418
- flex-direction: column;
419
- align-items: stretch;
420
- }
421
- .dropdown > div:first-child {
422
- display: flex;
423
- align-items: center;
424
- justify-content: space-between;
425
- }
426
- .dropdown > div:first-child > a,
427
- .dropdown > div:first-child > span {
428
- flex: 1;
429
- border-radius: var(--nav-border-radius);
430
- }
431
- .dropdown > div:first-child > button {
432
- padding: 4pt 8pt;
433
- border-radius: var(--nav-border-radius);
434
- }
435
- .dropdown > div:last-child {
436
- position: static;
437
- border: none;
438
- box-shadow: none;
439
- margin-top: 0.25em;
440
- padding: 0 0 0 1em;
441
- background-color: transparent;
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>