@14ch/svelte-ui 0.0.28 → 0.0.30

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.
@@ -2,20 +2,30 @@
2
2
 
3
3
  <script lang="ts">
4
4
  import Icon from './Icon.svelte';
5
+ import NavItem from './NavItem.svelte';
6
+ import PopupMenu from './PopupMenu.svelte';
7
+ import { fade, fly, slide } from 'svelte/transition';
8
+ import type { PopupPosition } from '../types/propOptions';
5
9
  import type { MenuItem } from '../types/menuItem';
6
- import type { NavVariant } from '../types/propOptions';
10
+ import type { NavVariant, ChildrenVariant } from '../types/propOptions';
7
11
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
12
+ import { tick } from 'svelte';
13
+ import { matchPath } from '../utils/navPath';
8
14
 
9
15
  // =========================================================================
10
16
  // Props, States & Constants
11
17
  // =========================================================================
12
- export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal';
18
+ export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal' | 'underline';
13
19
 
14
20
  export type NavItemProps = {
15
21
  // 基本プロパティ
16
22
  item: MenuItem;
17
23
  variant?: NavVariant;
18
24
  pathPrefix?: string;
25
+ /** Current URL path passed from Nav for computing child selected state. */
26
+ resolvedCurrentPath?: string;
27
+ /** Custom function to determine if a child item is active. */
28
+ customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
19
29
 
20
30
  // アイコン関連
21
31
  iconFilled?: boolean;
@@ -24,17 +34,35 @@
24
34
  iconOpticalSize?: IconOpticalSize;
25
35
  iconVariant?: IconVariant;
26
36
 
37
+ // スタイル/レイアウト
38
+ /** Show chevron icon on parent items. @default true */
39
+ chevron?: boolean;
40
+ selectedStyle?: NavItemSelectedStyle;
41
+ /** How child items are displayed. Defaults to `accordion` (vertical), `bar` (horizontal), `bottom-sheet` (mobile). */
42
+ childrenVariant?: ChildrenVariant;
43
+
27
44
  // 状態/動作
28
45
  isSelected?: boolean;
29
46
  isDisabled?: boolean;
30
- selectedStyle?: NavItemSelectedStyle;
47
+ /** When true, this item does not render its own children (prevents infinite recursion). */
48
+ isChild?: boolean;
49
+ /** For bar/accordion mode: whether this item's sub-menu is currently expanded. */
50
+ isChildrenExpanded?: boolean;
51
+
52
+ // イベントハンドラ
53
+ /** For bar/accordion mode: called when this parent is clicked. */
54
+ onChildrenToggle?: (item: MenuItem) => void;
55
+ /** Called when a leaf item (no children) is clicked — used to close any open sub-menu. */
56
+ onChildrenClose?: () => void;
31
57
  };
32
58
 
33
59
  let {
34
60
  // 基本プロパティ
35
61
  item,
36
- variant = 'tab',
62
+ variant = 'horizontal',
37
63
  pathPrefix = '',
64
+ resolvedCurrentPath = '',
65
+ customPathMatcher,
38
66
 
39
67
  // アイコン関連
40
68
  iconFilled = false,
@@ -43,42 +71,185 @@
43
71
  iconOpticalSize = 24,
44
72
  iconVariant = 'outlined',
45
73
 
74
+ // スタイル/レイアウト
75
+ chevron = true,
76
+ selectedStyle,
77
+ childrenVariant = variant === 'mobile' ? 'bottom-sheet' : variant === 'vertical' ? 'accordion' : 'bar',
78
+
46
79
  // 状態/動作
47
80
  isSelected = false,
48
81
  isDisabled = false,
49
- selectedStyle
82
+ isChild = false,
83
+ isChildrenExpanded = false,
84
+
85
+ // イベントハンドラ
86
+ onChildrenToggle,
87
+ onChildrenClose
50
88
  }: NavItemProps = $props();
51
89
 
52
90
  // =========================================================================
53
91
  // $derived
54
92
  // =========================================================================
55
- const hrefWithPrefix = $derived.by(() => {
56
- if (!item.href) return undefined;
57
- if (!pathPrefix) return item.href;
58
- if (item.href === pathPrefix || item.href.startsWith(`${pathPrefix}/`)) return item.href;
59
- return `${pathPrefix}${item.href.startsWith('/') ? '' : '/'}${item.href}`;
60
- });
93
+ const withPrefix = (href: string) => {
94
+ if (!pathPrefix) return href;
95
+ if (href === pathPrefix || href.startsWith(`${pathPrefix}/`)) return href;
96
+ return `${pathPrefix}${href.startsWith('/') ? '' : '/'}${href}`;
97
+ };
61
98
 
62
- const isTabVariant = $derived(variant === 'tab');
99
+ const hrefWithPrefix = $derived(item.href ? withPrefix(item.href) : undefined);
63
100
 
64
- // selectedStyle 未指定時のバリアント別デフォルト
65
101
  const resolvedSelectedStyle = $derived(
66
102
  selectedStyle ?? (variant === 'vertical' || variant === 'horizontal' ? 'tonal' : 'color')
67
103
  );
104
+
105
+ // isChild=true のアイテムでは children を展開しない(無限再帰防止)
106
+ const hasChildren = $derived(!isChild && !!item.children?.length);
107
+
108
+ // 親クリック時の遷移先: 自身の href、なければ選択中の子 → 最初の子 の優先順で決定
109
+ const resolvedParentHref = $derived.by(() => {
110
+ if (hrefWithPrefix) return hrefWithPrefix;
111
+ if (!hasChildren) return undefined;
112
+ const activeChild = item.children!.find(
113
+ (child) =>
114
+ !!child.href &&
115
+ matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher)
116
+ );
117
+ const target = activeChild ?? item.children![0];
118
+ return target.href ? withPrefix(target.href) : undefined;
119
+ });
120
+
121
+ // =========================================================================
122
+ // States
123
+ // =========================================================================
124
+ let isChildrenOpen = $state(false);
125
+ let anchorEl: HTMLElement | undefined = $state();
126
+ let popupMenuRef: PopupMenu | undefined = $state();
127
+ let isPopupOpen = $state(false);
128
+ let bottomSheetEl: HTMLElement | undefined = $state();
129
+
130
+ const focusFirstChild = (container: HTMLElement | undefined) =>
131
+ container?.querySelector<HTMLElement>('[data-nav-item-child]:not([tabindex="-1"])')?.focus();
132
+
133
+ $effect(() => {
134
+ if (isChildrenOpen) tick().then(() => focusFirstChild(bottomSheetEl));
135
+ });
136
+
137
+ const popupPosition = $derived<PopupPosition>(
138
+ variant === 'vertical' ? 'right-top' : variant === 'mobile' ? 'top-center' : 'bottom-left'
139
+ );
140
+
141
+ const isChildrenVisible = $derived(
142
+ !hasChildren
143
+ ? false
144
+ : childrenVariant === 'expanded'
145
+ ? true
146
+ : childrenVariant === 'bottom-sheet'
147
+ ? isChildrenOpen
148
+ : childrenVariant === 'popup'
149
+ ? isPopupOpen
150
+ : isChildrenExpanded
151
+ );
152
+
153
+ const showChevron = $derived(hasChildren && chevron && variant !== 'mobile');
154
+
155
+ // popup の方向に合わせたアイコン。それ以外は expand_more
156
+ const chevronIcon = $derived(
157
+ childrenVariant === 'popup' && variant === 'vertical'
158
+ ? 'arrow_right'
159
+ : childrenVariant === 'popup' && variant === 'horizontal'
160
+ ? 'arrow_drop_down'
161
+ : 'expand_more'
162
+ );
163
+
164
+ // popup 専用アイコンは方向固定なので展開時も回転しない
165
+ const chevronRotates = $derived(childrenVariant !== 'popup');
166
+
167
+ // =========================================================================
168
+ // Methods
169
+ // =========================================================================
170
+ const toggleOpen = () => {
171
+ isChildrenOpen = !isChildrenOpen;
172
+ };
173
+
174
+ const closeOpen = () => {
175
+ isChildrenOpen = false;
176
+ };
177
+
178
+ // popup の ArrowRight(vertical) / ArrowDown(horizontal) でサブメニューを開く
179
+ const handleTriggerKeyDown = (event: KeyboardEvent) => {
180
+ if (!hasChildren || childrenVariant !== 'popup' || isPopupOpen) return;
181
+ const openKey = variant === 'vertical' ? 'ArrowRight' : 'ArrowDown';
182
+ if (event.key !== openKey) return;
183
+ event.preventDefault();
184
+ popupMenuRef?.toggle();
185
+ };
186
+
187
+ const handleChildKeyDown = (event: KeyboardEvent, horizontal = false) => {
188
+ const container = event.currentTarget as HTMLElement;
189
+ const items = Array.from(
190
+ container.querySelectorAll<HTMLElement>('[data-nav-item-child]:not([tabindex="-1"])')
191
+ );
192
+ const currentIndex = items.indexOf(event.target as HTMLElement);
193
+ if (currentIndex === -1) return;
194
+
195
+ const prevKey = horizontal ? 'ArrowLeft' : 'ArrowUp';
196
+ const nextKey = horizontal ? 'ArrowRight' : 'ArrowDown';
197
+
198
+ switch (event.key) {
199
+ case prevKey:
200
+ event.preventDefault();
201
+ items[currentIndex > 0 ? currentIndex - 1 : items.length - 1]?.focus();
202
+ break;
203
+ case nextKey:
204
+ event.preventDefault();
205
+ items[currentIndex < items.length - 1 ? currentIndex + 1 : 0]?.focus();
206
+ break;
207
+ case 'Home':
208
+ event.preventDefault();
209
+ items[0]?.focus();
210
+ break;
211
+ case 'End':
212
+ event.preventDefault();
213
+ items[items.length - 1]?.focus();
214
+ break;
215
+ case 'Escape':
216
+ if (childrenVariant === 'bottom-sheet') {
217
+ event.preventDefault();
218
+ closeOpen();
219
+ anchorEl?.focus();
220
+ }
221
+ break;
222
+ }
223
+ };
224
+
225
+ const handleLinkClick = () => {
226
+ if (childrenVariant === 'popup') {
227
+ popupMenuRef?.toggle();
228
+ } else if (childrenVariant === 'bottom-sheet') {
229
+ toggleOpen();
230
+ } else {
231
+ onChildrenToggle?.(item);
232
+ }
233
+ };
234
+
235
+ const isChildSelected = (child: MenuItem) =>
236
+ !!child.href &&
237
+ matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher);
68
238
  </script>
69
239
 
70
240
  {#if isDisabled}
71
241
  <span
72
242
  class="nav-item nav-item--{variant} nav-item--disabled"
243
+ class:nav-item--child={isChild}
73
244
  class:nav-item--selected={isSelected}
74
245
  class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
75
246
  class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
76
247
  class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
77
- role={isTabVariant ? 'tab' : undefined}
78
- aria-selected={isTabVariant ? isSelected : undefined}
248
+ class:nav-item--style-underline={resolvedSelectedStyle === 'underline'}
79
249
  aria-disabled="true"
80
250
  tabindex="-1"
81
- data-nav-item
251
+ data-nav-item={!isChild ? '' : undefined}
252
+ data-nav-item-child={isChild ? '' : undefined}
82
253
  data-testid="nav-item"
83
254
  >
84
255
  {#if item.icon}
@@ -96,20 +267,158 @@
96
267
  <div class="nav-item__label">{item.label}</div>
97
268
  {/if}
98
269
  </span>
270
+ {:else if hasChildren}
271
+ <!-- ===================================================================
272
+ 親アイテム(子メニューあり)
273
+ =================================================================== -->
274
+ <div
275
+ class="nav-item__group nav-item__group--{variant}"
276
+ class:nav-item__group--open={isChildrenVisible}
277
+ >
278
+ <a
279
+ href={resolvedParentHref}
280
+ bind:this={anchorEl}
281
+ class="nav-item nav-item--{variant} nav-item--has-children"
282
+ class:nav-item--selected={isSelected}
283
+ class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
284
+ class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
285
+ class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
286
+ class:nav-item--style-underline={resolvedSelectedStyle === 'underline'}
287
+ aria-current={isSelected ? 'page' : undefined}
288
+ aria-expanded={childrenVariant === 'popup' ? isPopupOpen : isChildrenExpanded}
289
+ tabindex={0}
290
+ data-nav-item
291
+ data-testid="nav-item"
292
+ onclick={handleLinkClick}
293
+ onkeydown={handleTriggerKeyDown}
294
+ >
295
+ {#if item.icon}
296
+ <div class="nav-item__icon">
297
+ <Icon
298
+ filled={iconFilled || isSelected}
299
+ weight={iconWeight}
300
+ grade={iconGrade}
301
+ opticalSize={iconOpticalSize}
302
+ variant={iconVariant}>{item.icon}</Icon
303
+ >
304
+ </div>
305
+ {/if}
306
+ {#if item.label}
307
+ <div class="nav-item__label">{item.label}</div>
308
+ {/if}
309
+ {#if showChevron}
310
+ <div
311
+ class="nav-item__chevron"
312
+ class:nav-item__chevron--expanded={chevronRotates && isChildrenVisible}
313
+ >
314
+ <Icon
315
+ weight={iconWeight}
316
+ grade={iconGrade}
317
+ opticalSize={iconOpticalSize}
318
+ variant={iconVariant}>{chevronIcon}</Icon
319
+ >
320
+ </div>
321
+ {/if}
322
+ </a>
323
+
324
+ <!-- popup サブメニュー -->
325
+ {#if childrenVariant === 'popup'}
326
+ <PopupMenu
327
+ bind:this={popupMenuRef}
328
+ bind:isOpen={isPopupOpen}
329
+ anchorElement={anchorEl}
330
+ position={popupPosition}
331
+ menuItems={item.children!}
332
+ mobileFullscreen={false}
333
+ {iconFilled}
334
+ {iconWeight}
335
+ {iconGrade}
336
+ {iconOpticalSize}
337
+ {iconVariant}
338
+ />
339
+ {/if}
340
+
341
+ <!-- accordion / expanded サブメニュー -->
342
+ {#if (childrenVariant === 'accordion' || childrenVariant === 'expanded') && isChildrenVisible}
343
+ <div class="nav-item__children" role="presentation" transition:slide={{ duration: 200 }}>
344
+ {#each item.children! as child}
345
+ <NavItem
346
+ item={child}
347
+ {variant}
348
+ {pathPrefix}
349
+ {iconFilled}
350
+ {iconWeight}
351
+ {iconGrade}
352
+ {iconOpticalSize}
353
+ {iconVariant}
354
+ {selectedStyle}
355
+ isChild={true}
356
+ {resolvedCurrentPath}
357
+ {customPathMatcher}
358
+ isSelected={isChildSelected(child)}
359
+ isDisabled={child.disabled ?? false}
360
+ />
361
+ {/each}
362
+ </div>
363
+ {/if}
364
+
365
+ <!-- bottom-sheet オーバーレイ -->
366
+ {#if childrenVariant === 'bottom-sheet' && isChildrenOpen}
367
+ <div
368
+ class="nav-item__bottom-sheet-backdrop"
369
+ role="presentation"
370
+ onclick={closeOpen}
371
+ transition:fade={{ duration: 200 }}
372
+ ></div>
373
+ <div
374
+ class="nav-item__bottom-sheet"
375
+ role="menu"
376
+ tabindex="-1"
377
+ transition:fly={{ y: 100, duration: 250 }}
378
+ onkeydown={(e) => handleChildKeyDown(e, true)}
379
+ bind:this={bottomSheetEl}
380
+ >
381
+ {#each item.children! as child}
382
+ <NavItem
383
+ item={child}
384
+ {variant}
385
+ {pathPrefix}
386
+ {iconFilled}
387
+ {iconWeight}
388
+ {iconGrade}
389
+ {iconOpticalSize}
390
+ {iconVariant}
391
+ {selectedStyle}
392
+ isChild={true}
393
+ {resolvedCurrentPath}
394
+ {customPathMatcher}
395
+ isSelected={isChildSelected(child)}
396
+ isDisabled={child.disabled ?? false}
397
+ onChildrenClose={closeOpen}
398
+ />
399
+ {/each}
400
+ </div>
401
+ {/if}
402
+ </div>
99
403
  {:else}
404
+ <!-- ===================================================================
405
+ 通常アイテム(子メニューなし)
406
+ =================================================================== -->
100
407
  <a
101
408
  href={hrefWithPrefix}
102
409
  class="nav-item nav-item--{variant}"
410
+ class:nav-item--child={isChild}
103
411
  class:nav-item--selected={isSelected}
104
412
  class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
105
413
  class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
106
414
  class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
107
- role={isTabVariant ? 'tab' : undefined}
108
- aria-selected={isTabVariant ? isSelected : undefined}
109
- aria-current={!isTabVariant && isSelected ? 'page' : undefined}
415
+ class:nav-item--style-underline={resolvedSelectedStyle === 'underline'}
416
+ aria-current={isSelected ? 'page' : undefined}
110
417
  tabindex={0}
111
- data-nav-item
418
+ data-nav-item={!isChild ? '' : undefined}
419
+ data-nav-item-child={isChild ? '' : undefined}
112
420
  data-testid="nav-item"
421
+ onclick={onChildrenClose}
113
422
  >
114
423
  {#if item.icon}
115
424
  <div class="nav-item__icon">
@@ -171,40 +480,6 @@
171
480
  outline-offset: var(--svelte-ui-focus-outline-offset-inner);
172
481
  }
173
482
  }
174
- .nav-item--tab {
175
- justify-content: center;
176
- padding: var(--svelte-ui-tab-item-padding);
177
- min-height: var(--svelte-ui-nav-item-min-height);
178
- color: var(--svelte-ui-tab-item-text-color);
179
- }
180
-
181
- @media (hover: hover) {
182
- .nav-item--tab:hover {
183
- color: var(--svelte-ui-tab-item-selected-text-color);
184
- }
185
- .nav-item--tab:hover::before {
186
- opacity: 1;
187
- }
188
- }
189
- .nav-item--tab::before {
190
- content: "";
191
- display: block;
192
- position: absolute;
193
- left: calc(var(--svelte-ui-tab-item-padding-x) - var(--svelte-ui-tab-item-selected-bar-offset));
194
- bottom: 0;
195
- width: calc(100% - 2 * var(--svelte-ui-tab-item-padding-x) + 2 * var(--svelte-ui-tab-item-selected-bar-offset));
196
- height: var(--svelte-ui-tab-item-selected-bar-height);
197
- background-color: var(--svelte-ui-tab-item-selected-bar-color);
198
- border-radius: var(--svelte-ui-tab-item-selected-bar-radius);
199
- opacity: 0;
200
- transition-property: opacity;
201
- transition-duration: var(--svelte-ui-transition-duration);
202
- }
203
-
204
- .nav-item--tab.nav-item--selected::before {
205
- opacity: 1;
206
- }
207
-
208
483
  .nav-item--mobile {
209
484
  flex-direction: column;
210
485
  justify-content: center;
@@ -220,6 +495,9 @@
220
495
  padding: var(--svelte-ui-nav-item-padding);
221
496
  min-height: var(--svelte-ui-nav-item-min-height);
222
497
  border-radius: var(--svelte-ui-nav-item-border-radius);
498
+ white-space: normal;
499
+ word-break: break-word;
500
+ line-height: var(--svelte-ui-nav-item-label-line-height);
223
501
  }
224
502
 
225
503
  .nav-item--vertical::after {
@@ -237,12 +515,29 @@
237
515
  border-radius: var(--svelte-ui-nav-item-border-radius);
238
516
  }
239
517
 
518
+ .nav-item--horizontal.nav-item--style-underline {
519
+ padding: var(--svelte-ui-nav-item-padding-y) var(--svelte-ui-nav-item-padding-x);
520
+ justify-content: center;
521
+ border-radius: 0;
522
+ color: var(--svelte-ui-nav-item-underline-text-color);
523
+ }
524
+
240
525
  .nav-item--horizontal::after {
241
526
  border-radius: var(--svelte-ui-nav-item-border-radius);
242
527
  }
243
528
 
529
+ .nav-item--horizontal.nav-item--style-underline::after {
530
+ border-radius: 0;
531
+ }
532
+
244
533
  @media (hover: hover) {
245
- .nav-item--horizontal:hover::after {
534
+ .nav-item--horizontal:not(.nav-item--style-underline):hover::after {
535
+ opacity: 1;
536
+ }
537
+ .nav-item--horizontal.nav-item--style-underline:hover {
538
+ color: var(--svelte-ui-nav-item-underline-selected-text-color);
539
+ }
540
+ .nav-item--horizontal.nav-item--style-underline:hover::before {
246
541
  opacity: 1;
247
542
  }
248
543
  }
@@ -250,11 +545,6 @@
250
545
  color: var(--svelte-ui-nav-item-selected-text-color);
251
546
  }
252
547
 
253
- .nav-item--tab.nav-item--style-color {
254
- color: var(--svelte-ui-tab-item-selected-text-color);
255
- background-color: transparent;
256
- }
257
-
258
548
  .nav-item--style-filled {
259
549
  background-color: var(--svelte-ui-primary-color);
260
550
  color: var(--svelte-ui-nav-item-filled-text-color);
@@ -265,11 +555,100 @@
265
555
  color: var(--svelte-ui-nav-item-selected-text-color);
266
556
  }
267
557
 
558
+ .nav-item--horizontal.nav-item--style-underline.nav-item--selected {
559
+ color: var(--svelte-ui-nav-item-underline-selected-text-color);
560
+ }
561
+
562
+ .nav-item--horizontal.nav-item--style-underline::before {
563
+ content: "";
564
+ display: block;
565
+ position: absolute;
566
+ bottom: 0;
567
+ left: calc(var(--svelte-ui-nav-item-padding-x) - var(--svelte-ui-nav-item-underline-bar-offset));
568
+ width: calc(100% - 2 * var(--svelte-ui-nav-item-padding-x) + 2 * var(--svelte-ui-nav-item-underline-bar-offset));
569
+ height: var(--svelte-ui-nav-item-underline-bar-height);
570
+ background-color: var(--svelte-ui-nav-item-underline-bar-color);
571
+ border-radius: var(--svelte-ui-nav-item-underline-bar-radius);
572
+ opacity: 0;
573
+ transition-property: opacity;
574
+ transition-duration: var(--svelte-ui-transition-duration);
575
+ }
576
+
577
+ .nav-item--horizontal.nav-item--style-underline.nav-item--selected::before {
578
+ opacity: 1;
579
+ }
580
+
268
581
  .nav-item__label {
269
582
  text-box-trim: trim-both;
270
583
  text-box-edge: cap alphabetic;
584
+ line-height: var(--svelte-ui-nav-item-label-line-height);
271
585
  }
272
586
 
273
587
  .nav-item--mobile .nav-item__label {
274
588
  text-align: center;
589
+ }
590
+
591
+ .nav-item__chevron {
592
+ display: flex;
593
+ align-items: center;
594
+ margin: -12px -4px -12px auto;
595
+ flex-shrink: 0;
596
+ transition: transform var(--svelte-ui-transition-duration);
597
+ }
598
+
599
+ .nav-item--horizontal .nav-item__chevron,
600
+ .nav-item--mobile .nav-item__chevron {
601
+ margin-left: 2px;
602
+ }
603
+
604
+ .nav-item__chevron--expanded {
605
+ transform: rotate(180deg);
606
+ }
607
+
608
+ .nav-item__group {
609
+ position: relative;
610
+ display: flex;
611
+ flex-direction: column;
612
+ }
613
+
614
+ .nav-item__group--vertical {
615
+ width: 100%;
616
+ }
617
+
618
+ .nav-item__group--mobile {
619
+ flex: 1;
620
+ flex-direction: column;
621
+ }
622
+
623
+ .nav-item__children {
624
+ display: flex;
625
+ flex-direction: column;
626
+ padding-top: var(--internal-nav-gap, var(--svelte-ui-nav-vertical-item-gap));
627
+ gap: var(--internal-nav-gap, var(--svelte-ui-nav-vertical-item-gap));
628
+ overflow: hidden;
629
+ }
630
+
631
+ .nav-item--child.nav-item--vertical {
632
+ padding-left: calc(var(--svelte-ui-nav-item-padding-x) + var(--svelte-ui-nav-item-child-indent));
633
+ }
634
+
635
+ .nav-item__bottom-sheet-backdrop {
636
+ position: fixed;
637
+ inset: 0;
638
+ z-index: 1000;
639
+ background-color: var(--svelte-ui-nav-bottom-sheet-overlay-bg);
640
+ }
641
+
642
+ .nav-item__bottom-sheet {
643
+ position: fixed;
644
+ bottom: 0;
645
+ left: 0;
646
+ right: 0;
647
+ z-index: 1001;
648
+ background-color: var(--svelte-ui-surface-color);
649
+ border-radius: var(--svelte-ui-nav-bottom-sheet-border-radius);
650
+ padding: var(--svelte-ui-nav-bottom-sheet-padding);
651
+ display: flex;
652
+ flex-direction: row;
653
+ justify-content: space-around;
275
654
  }</style>
@@ -1,19 +1,36 @@
1
+ import NavItem from './NavItem.svelte';
1
2
  import type { MenuItem } from '../types/menuItem';
2
- import type { NavVariant } from '../types/propOptions';
3
+ import type { NavVariant, ChildrenVariant } from '../types/propOptions';
3
4
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
4
- export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal';
5
+ export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal' | 'underline';
5
6
  export type NavItemProps = {
6
7
  item: MenuItem;
7
8
  variant?: NavVariant;
8
9
  pathPrefix?: string;
10
+ /** Current URL path passed from Nav for computing child selected state. */
11
+ resolvedCurrentPath?: string;
12
+ /** Custom function to determine if a child item is active. */
13
+ customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
9
14
  iconFilled?: boolean;
10
15
  iconWeight?: IconWeight;
11
16
  iconGrade?: IconGrade;
12
17
  iconOpticalSize?: IconOpticalSize;
13
18
  iconVariant?: IconVariant;
19
+ /** Show chevron icon on parent items. @default true */
20
+ chevron?: boolean;
21
+ selectedStyle?: NavItemSelectedStyle;
22
+ /** How child items are displayed. Defaults to `accordion` (vertical), `bar` (horizontal), `bottom-sheet` (mobile). */
23
+ childrenVariant?: ChildrenVariant;
14
24
  isSelected?: boolean;
15
25
  isDisabled?: boolean;
16
- selectedStyle?: NavItemSelectedStyle;
26
+ /** When true, this item does not render its own children (prevents infinite recursion). */
27
+ isChild?: boolean;
28
+ /** For bar/accordion mode: whether this item's sub-menu is currently expanded. */
29
+ isChildrenExpanded?: boolean;
30
+ /** For bar/accordion mode: called when this parent is clicked. */
31
+ onChildrenToggle?: (item: MenuItem) => void;
32
+ /** Called when a leaf item (no children) is clicked — used to close any open sub-menu. */
33
+ onChildrenClose?: () => void;
17
34
  };
18
35
  declare const NavItem: import("svelte").Component<NavItemProps, {}, "">;
19
36
  type NavItem = ReturnType<typeof NavItem>;