@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.
- package/dist/assets/styles/variables.scss +28 -24
- package/dist/components/Nav.svelte +168 -46
- package/dist/components/Nav.svelte.d.ts +7 -3
- package/dist/components/NavItem.svelte +439 -60
- package/dist/components/NavItem.svelte.d.ts +20 -3
- package/dist/components/Tab.svelte +44 -118
- package/dist/components/Tab.svelte.d.ts +8 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +0 -1
- package/dist/types/menuItem.d.ts +1 -0
- package/dist/types/propOptions.d.ts +10 -1
- package/package.json +1 -1
- package/dist/components/TabItem.svelte +0 -219
- package/dist/components/TabItem.svelte.d.ts +0 -19
|
@@ -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
|
-
|
|
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 = '
|
|
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
|
-
|
|
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
|
|
56
|
-
if (!
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
aria-
|
|
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
|
-
|
|
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>;
|