@14ch/svelte-ui 0.0.24 → 0.0.25

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.
@@ -373,6 +373,36 @@
373
373
  --svelte-ui-tab-item-selected-bar-radius: 3px 3px 0 0;
374
374
  --svelte-ui-tab-item-disabled-opacity: var(--svelte-ui-disabled-opacity);
375
375
 
376
+ /* Nav */
377
+ --svelte-ui-nav-item-text-color: var(--svelte-ui-text-color);
378
+ --svelte-ui-nav-item-selected-text-color: var(--svelte-ui-primary-color);
379
+ --svelte-ui-nav-item-selected-bg-color: var(--svelte-ui-hover-overlay);
380
+ --svelte-ui-nav-item-filled-text-color: #ffffff;
381
+ --svelte-ui-nav-item-tonal-bg-color: color-mix(
382
+ in srgb,
383
+ var(--svelte-ui-primary-color) 16%,
384
+ transparent
385
+ );
386
+ --svelte-ui-nav-item-selected-bar-color: var(--svelte-ui-primary-color);
387
+ --svelte-ui-nav-item-selected-bar-height: 3px;
388
+ --svelte-ui-nav-item-selected-bar-radius: 2px;
389
+ --svelte-ui-nav-item-min-height: 40px;
390
+ --svelte-ui-nav-item-icon-gap: 8px;
391
+ --svelte-ui-nav-item-disabled-opacity: var(--svelte-ui-disabled-opacity);
392
+ /* Nav: mobile-tab */
393
+ --svelte-ui-nav-mobile-min-height: 56px;
394
+ --svelte-ui-nav-mobile-item-padding: 8px;
395
+ --svelte-ui-nav-mobile-item-icon-gap: 8px;
396
+ --svelte-ui-nav-mobile-item-font-size: 0.75rem;
397
+ --svelte-ui-nav-mobile-item-bar-width: 40px;
398
+ --svelte-ui-nav-item-padding: 8px 12px;
399
+ --svelte-ui-nav-item-border-radius: var(--svelte-ui-border-radius);
400
+ /* Nav: vertical */
401
+ --svelte-ui-nav-vertical-padding: 8px;
402
+ --svelte-ui-nav-vertical-item-gap: 8px;
403
+ /* Nav: horizontal */
404
+ --svelte-ui-nav-horizontal-item-gap: 8px;
405
+
376
406
  /* Input */
377
407
  --svelte-ui-input-height: var(--svelte-ui-form-height);
378
408
  --svelte-ui-input-padding: var(--svelte-ui-form-padding);
@@ -0,0 +1,218 @@
1
+ <!-- Nav.svelte -->
2
+
3
+ <script lang="ts">
4
+ import NavItem from './NavItem.svelte';
5
+ import type { NavItemSelectedStyle } from './NavItem.svelte';
6
+ import type { MenuItem } from '../types/menuItem';
7
+ import type { NavVariant } from '../types/propOptions';
8
+ import { subscribeUrlChange } from '../utils/urlChange';
9
+ import { getCurrentPath, matchPath } from '../utils/navPath';
10
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
11
+
12
+ // =========================================================================
13
+ // Props, States & Constants
14
+ // =========================================================================
15
+ export type NavProps = {
16
+ // 基本プロパティ
17
+ items?: MenuItem[];
18
+ variant?: NavVariant;
19
+ pathPrefix?: string;
20
+ customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
21
+ currentPath?: string;
22
+
23
+ // HTML属性
24
+ id?: string;
25
+
26
+ // アイコン関連
27
+ iconFilled?: boolean;
28
+ iconWeight?: IconWeight;
29
+ iconGrade?: IconGrade;
30
+ iconOpticalSize?: IconOpticalSize;
31
+ iconVariant?: IconVariant;
32
+
33
+ // スタイル/レイアウト
34
+ selectedStyle?: NavItemSelectedStyle;
35
+
36
+ // ARIA/アクセシビリティ
37
+ ariaLabel?: string;
38
+ ariaLabelledby?: string;
39
+ };
40
+
41
+ let {
42
+ // 基本プロパティ
43
+ items = [],
44
+ variant = 'tab',
45
+ pathPrefix = '',
46
+ customPathMatcher,
47
+ currentPath,
48
+
49
+ // HTML属性
50
+ id,
51
+
52
+ // アイコン関連
53
+ iconFilled = false,
54
+ iconWeight = 300,
55
+ iconGrade = 0,
56
+ iconOpticalSize = 24,
57
+ iconVariant = 'outlined',
58
+
59
+ // スタイル/レイアウト
60
+ selectedStyle,
61
+
62
+ // ARIA/アクセシビリティ
63
+ ariaLabel,
64
+ ariaLabelledby
65
+ }: NavProps = $props();
66
+
67
+ let resolvedCurrentPath = $state('');
68
+
69
+ // =========================================================================
70
+ // Effects
71
+ // =========================================================================
72
+ $effect(() => {
73
+ resolvedCurrentPath = getCurrentPath(currentPath);
74
+ });
75
+
76
+ $effect(() => {
77
+ return subscribeUrlChange(() => {
78
+ resolvedCurrentPath = getCurrentPath(currentPath);
79
+ });
80
+ });
81
+
82
+ // =========================================================================
83
+ // Methods
84
+ // =========================================================================
85
+ const handleKeyDown = (event: KeyboardEvent) => {
86
+ if (items.length === 0 || enabledIndices.length === 0) return;
87
+
88
+ const navEl = event.currentTarget as HTMLElement;
89
+ const navItems = Array.from(navEl.querySelectorAll('[data-nav-item]')) as HTMLElement[];
90
+ const currentItem = event.target as HTMLElement;
91
+ const currentIndex = navItems.indexOf(currentItem);
92
+
93
+ if (currentIndex === -1) return;
94
+
95
+ const currentEnabledPosition = enabledIndices.indexOf(currentIndex);
96
+ let nextEnabledPosition = currentEnabledPosition;
97
+
98
+ const isVertical = variant === 'vertical';
99
+ const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
100
+ const nextKey = isVertical ? 'ArrowDown' : 'ArrowRight';
101
+
102
+ switch (event.key) {
103
+ case prevKey:
104
+ event.preventDefault();
105
+ nextEnabledPosition =
106
+ currentEnabledPosition > 0 ? currentEnabledPosition - 1 : enabledIndices.length - 1;
107
+ break;
108
+ case nextKey:
109
+ event.preventDefault();
110
+ nextEnabledPosition =
111
+ currentEnabledPosition < enabledIndices.length - 1 ? currentEnabledPosition + 1 : 0;
112
+ break;
113
+ case 'Home':
114
+ event.preventDefault();
115
+ nextEnabledPosition = 0;
116
+ break;
117
+ case 'End':
118
+ event.preventDefault();
119
+ nextEnabledPosition = enabledIndices.length - 1;
120
+ break;
121
+ default:
122
+ return;
123
+ }
124
+
125
+ const nextIndex = enabledIndices[nextEnabledPosition];
126
+ navItems[nextIndex]?.focus();
127
+ };
128
+
129
+ // =========================================================================
130
+ // $derived
131
+ // =========================================================================
132
+ const selectedIndex = $derived.by(() => {
133
+ for (let i = 0; i < items.length; i++) {
134
+ const item = items[i];
135
+ if (!item.href) continue;
136
+ if (matchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
137
+ return i;
138
+ }
139
+ }
140
+ return -1;
141
+ });
142
+
143
+ const enabledIndices = $derived(
144
+ items.map((item, i) => (item.disabled ? -1 : i)).filter((i) => i >= 0)
145
+ );
146
+
147
+ const isTabVariant = $derived(variant === 'tab');
148
+ </script>
149
+
150
+ <nav
151
+ class="nav nav--{variant}"
152
+ role={isTabVariant ? 'tablist' : undefined}
153
+ aria-label={ariaLabelledby ? undefined : ariaLabel}
154
+ aria-labelledby={ariaLabelledby}
155
+ aria-orientation={variant === 'vertical' ? 'vertical' : 'horizontal'}
156
+ tabindex="-1"
157
+ {id}
158
+ onkeydown={handleKeyDown}
159
+ data-testid="nav"
160
+ >
161
+ {#each items as item, index}
162
+ <NavItem
163
+ {item}
164
+ {variant}
165
+ {pathPrefix}
166
+ isSelected={index === selectedIndex}
167
+ isDisabled={item.disabled ?? false}
168
+ {iconFilled}
169
+ {iconWeight}
170
+ {iconGrade}
171
+ {iconOpticalSize}
172
+ {iconVariant}
173
+ {selectedStyle}
174
+ />
175
+ {/each}
176
+ </nav>
177
+
178
+ <style>.nav {
179
+ display: flex;
180
+ box-sizing: border-box;
181
+ }
182
+
183
+ .nav--tab {
184
+ flex-direction: row;
185
+ justify-content: start;
186
+ position: relative;
187
+ width: 100%;
188
+ height: 100%;
189
+ min-height: var(--svelte-ui-tab-min-height);
190
+ overflow-x: auto;
191
+ overflow-y: visible;
192
+ -ms-overflow-style: none;
193
+ overscroll-behavior: contain;
194
+ }
195
+
196
+ .nav--tab::-webkit-scrollbar {
197
+ display: none;
198
+ }
199
+
200
+ .nav--mobile {
201
+ flex-direction: row;
202
+ width: 100%;
203
+ min-height: var(--svelte-ui-nav-mobile-min-height);
204
+ overflow: hidden;
205
+ }
206
+
207
+ .nav--vertical {
208
+ flex-direction: column;
209
+ gap: var(--svelte-ui-nav-vertical-item-gap);
210
+ padding: var(--svelte-ui-nav-vertical-padding);
211
+ width: 100%;
212
+ }
213
+
214
+ .nav--horizontal {
215
+ flex-direction: row;
216
+ gap: var(--svelte-ui-nav-horizontal-item-gap);
217
+ align-items: center;
218
+ }</style>
@@ -0,0 +1,23 @@
1
+ import type { NavItemSelectedStyle } from './NavItem.svelte';
2
+ import type { MenuItem } from '../types/menuItem';
3
+ import type { NavVariant } from '../types/propOptions';
4
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
5
+ export type NavProps = {
6
+ items?: MenuItem[];
7
+ variant?: NavVariant;
8
+ pathPrefix?: string;
9
+ customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
10
+ currentPath?: string;
11
+ id?: string;
12
+ iconFilled?: boolean;
13
+ iconWeight?: IconWeight;
14
+ iconGrade?: IconGrade;
15
+ iconOpticalSize?: IconOpticalSize;
16
+ iconVariant?: IconVariant;
17
+ selectedStyle?: NavItemSelectedStyle;
18
+ ariaLabel?: string;
19
+ ariaLabelledby?: string;
20
+ };
21
+ declare const Nav: import("svelte").Component<NavProps, {}, "">;
22
+ type Nav = ReturnType<typeof Nav>;
23
+ export default Nav;
@@ -0,0 +1,289 @@
1
+ <!-- NavItem.svelte -->
2
+
3
+ <script lang="ts">
4
+ import Icon from './Icon.svelte';
5
+ import type { MenuItem } from '../types/menuItem';
6
+ import type { NavVariant } from '../types/propOptions';
7
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
8
+
9
+ // =========================================================================
10
+ // Props, States & Constants
11
+ // =========================================================================
12
+ export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal';
13
+
14
+ export type NavItemProps = {
15
+ // 基本プロパティ
16
+ item: MenuItem;
17
+ variant?: NavVariant;
18
+ pathPrefix?: string;
19
+
20
+ // アイコン関連
21
+ iconFilled?: boolean;
22
+ iconWeight?: IconWeight;
23
+ iconGrade?: IconGrade;
24
+ iconOpticalSize?: IconOpticalSize;
25
+ iconVariant?: IconVariant;
26
+
27
+ // 状態/動作
28
+ isSelected?: boolean;
29
+ isDisabled?: boolean;
30
+ selectedStyle?: NavItemSelectedStyle;
31
+ };
32
+
33
+ let {
34
+ // 基本プロパティ
35
+ item,
36
+ variant = 'tab',
37
+ pathPrefix = '',
38
+
39
+ // アイコン関連
40
+ iconFilled = false,
41
+ iconWeight = 300,
42
+ iconGrade = 0,
43
+ iconOpticalSize = 24,
44
+ iconVariant = 'outlined',
45
+
46
+ // 状態/動作
47
+ isSelected = false,
48
+ isDisabled = false,
49
+ selectedStyle
50
+ }: NavItemProps = $props();
51
+
52
+ // =========================================================================
53
+ // $derived
54
+ // =========================================================================
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
+ });
61
+
62
+ const isTabVariant = $derived(variant === 'tab');
63
+
64
+ // selectedStyle 未指定時のバリアント別デフォルト
65
+ const resolvedSelectedStyle = $derived(
66
+ selectedStyle ?? (variant === 'vertical' || variant === 'horizontal' ? 'tonal' : 'color')
67
+ );
68
+ </script>
69
+
70
+ {#if isDisabled}
71
+ <span
72
+ class="nav-item nav-item--{variant} nav-item--disabled"
73
+ class:nav-item--selected={isSelected}
74
+ class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
75
+ class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
76
+ class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
77
+ role={isTabVariant ? 'tab' : undefined}
78
+ aria-selected={isTabVariant ? isSelected : undefined}
79
+ aria-disabled="true"
80
+ tabindex="-1"
81
+ data-nav-item
82
+ data-testid="nav-item"
83
+ >
84
+ {#if item.icon}
85
+ <div class="nav-item__icon">
86
+ <Icon
87
+ filled={iconFilled || isSelected}
88
+ weight={iconWeight}
89
+ grade={iconGrade}
90
+ opticalSize={iconOpticalSize}
91
+ variant={iconVariant}>{item.icon}</Icon
92
+ >
93
+ </div>
94
+ {/if}
95
+ {#if item.label}
96
+ <div class="nav-item__label">{item.label}</div>
97
+ {/if}
98
+ </span>
99
+ {:else}
100
+ <a
101
+ href={hrefWithPrefix}
102
+ class="nav-item nav-item--{variant}"
103
+ class:nav-item--selected={isSelected}
104
+ class:nav-item--style-color={isSelected && resolvedSelectedStyle === 'color'}
105
+ class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
106
+ 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}
110
+ tabindex={0}
111
+ data-nav-item
112
+ data-testid="nav-item"
113
+ >
114
+ {#if item.icon}
115
+ <div class="nav-item__icon">
116
+ <Icon
117
+ filled={iconFilled || isSelected}
118
+ weight={iconWeight}
119
+ grade={iconGrade}
120
+ opticalSize={iconOpticalSize}
121
+ variant={iconVariant}>{item.icon}</Icon
122
+ >
123
+ </div>
124
+ {/if}
125
+ {#if item.label}
126
+ <div class="nav-item__label">{item.label}</div>
127
+ {/if}
128
+ </a>
129
+ {/if}
130
+
131
+ <style>.nav-item {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: var(--svelte-ui-nav-item-icon-gap);
135
+ position: relative;
136
+ color: var(--svelte-ui-nav-item-text-color);
137
+ white-space: nowrap;
138
+ text-decoration: none;
139
+ cursor: pointer;
140
+ outline: none;
141
+ transition-property: background-color, color, outline;
142
+ transition-duration: var(--svelte-ui-transition-duration);
143
+ box-sizing: border-box;
144
+ }
145
+
146
+ .nav-item::after {
147
+ content: "";
148
+ position: absolute;
149
+ inset: 0;
150
+ background-color: var(--svelte-ui-hover-overlay);
151
+ opacity: 0;
152
+ pointer-events: none;
153
+ transition-property: opacity;
154
+ transition-duration: var(--svelte-ui-transition-duration);
155
+ }
156
+
157
+ .nav-item--disabled {
158
+ opacity: var(--svelte-ui-nav-item-disabled-opacity);
159
+ pointer-events: none;
160
+ cursor: default;
161
+ }
162
+
163
+ .nav-item:focus-visible {
164
+ outline: var(--svelte-ui-focus-outline-inner);
165
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner);
166
+ }
167
+
168
+ @supports not selector(:focus-visible) {
169
+ .nav-item:focus {
170
+ outline: var(--svelte-ui-focus-outline-inner);
171
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner);
172
+ }
173
+ }
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
+ .nav-item--mobile {
209
+ flex-direction: column;
210
+ justify-content: center;
211
+ flex: 1;
212
+ padding: var(--svelte-ui-nav-mobile-item-padding);
213
+ min-height: var(--svelte-ui-nav-mobile-min-height);
214
+ color: var(--svelte-ui-nav-item-text-color);
215
+ font-size: var(--svelte-ui-nav-mobile-item-font-size);
216
+ gap: var(--svelte-ui-nav-mobile-item-icon-gap);
217
+ }
218
+
219
+ @media (hover: hover) {
220
+ .nav-item--mobile:hover {
221
+ color: var(--svelte-ui-nav-item-selected-text-color);
222
+ }
223
+ }
224
+ .nav-item--vertical {
225
+ width: 100%;
226
+ padding: var(--svelte-ui-nav-item-padding);
227
+ min-height: var(--svelte-ui-nav-item-min-height);
228
+ border-radius: var(--svelte-ui-nav-item-border-radius);
229
+ color: var(--svelte-ui-nav-item-text-color);
230
+ }
231
+
232
+ .nav-item--vertical::after {
233
+ border-radius: var(--svelte-ui-nav-item-border-radius);
234
+ }
235
+
236
+ @media (hover: hover) {
237
+ .nav-item--vertical:hover {
238
+ color: var(--svelte-ui-nav-item-selected-text-color);
239
+ }
240
+ .nav-item--vertical:hover::after {
241
+ opacity: 1;
242
+ }
243
+ }
244
+ .nav-item--horizontal {
245
+ padding: var(--svelte-ui-nav-item-padding);
246
+ min-height: var(--svelte-ui-nav-item-min-height);
247
+ border-radius: var(--svelte-ui-nav-item-border-radius);
248
+ color: var(--svelte-ui-nav-item-text-color);
249
+ }
250
+
251
+ .nav-item--horizontal::after {
252
+ border-radius: var(--svelte-ui-nav-item-border-radius);
253
+ }
254
+
255
+ @media (hover: hover) {
256
+ .nav-item--horizontal:hover {
257
+ color: var(--svelte-ui-nav-item-selected-text-color);
258
+ }
259
+ .nav-item--horizontal:hover::after {
260
+ opacity: 1;
261
+ }
262
+ }
263
+ .nav-item--style-color {
264
+ color: var(--svelte-ui-nav-item-selected-text-color);
265
+ }
266
+
267
+ .nav-item--tab.nav-item--style-color {
268
+ color: var(--svelte-ui-tab-item-selected-text-color);
269
+ background-color: transparent;
270
+ }
271
+
272
+ .nav-item--style-filled {
273
+ background-color: var(--svelte-ui-primary-color);
274
+ color: var(--svelte-ui-nav-item-filled-text-color);
275
+ }
276
+
277
+ .nav-item--style-tonal {
278
+ background-color: var(--svelte-ui-nav-item-tonal-bg-color);
279
+ color: var(--svelte-ui-nav-item-selected-text-color);
280
+ }
281
+
282
+ .nav-item__label {
283
+ text-box-trim: trim-both;
284
+ text-box-edge: cap alphabetic;
285
+ }
286
+
287
+ .nav-item--mobile .nav-item__label {
288
+ text-align: center;
289
+ }</style>
@@ -0,0 +1,20 @@
1
+ import type { MenuItem } from '../types/menuItem';
2
+ import type { NavVariant } from '../types/propOptions';
3
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
4
+ export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal';
5
+ export type NavItemProps = {
6
+ item: MenuItem;
7
+ variant?: NavVariant;
8
+ pathPrefix?: string;
9
+ iconFilled?: boolean;
10
+ iconWeight?: IconWeight;
11
+ iconGrade?: IconGrade;
12
+ iconOpticalSize?: IconOpticalSize;
13
+ iconVariant?: IconVariant;
14
+ isSelected?: boolean;
15
+ isDisabled?: boolean;
16
+ selectedStyle?: NavItemSelectedStyle;
17
+ };
18
+ declare const NavItem: import("svelte").Component<NavItemProps, {}, "">;
19
+ type NavItem = ReturnType<typeof NavItem>;
20
+ export default NavItem;
@@ -4,6 +4,7 @@
4
4
  import TabItem from './TabItem.svelte';
5
5
  import type { MenuItem } from '../types/menuItem';
6
6
  import { subscribeUrlChange } from '../utils/urlChange';
7
+ import { getCurrentPath as resolveCurrentPath, matchPath as doMatchPath } from '../utils/navPath';
7
8
 
8
9
  // =========================================================================
9
10
  // Props, States & Constants
@@ -49,72 +50,19 @@
49
50
  // =========================================================================
50
51
  $effect(() => {
51
52
  // props の currentPath が変更されたとき
52
- resolvedCurrentPath = getCurrentPath();
53
+ resolvedCurrentPath = resolveCurrentPath(currentPath);
53
54
  });
54
55
 
55
56
  $effect(() => {
56
57
  // URL の変更を subscribe
57
58
  return subscribeUrlChange(() => {
58
- resolvedCurrentPath = getCurrentPath();
59
+ resolvedCurrentPath = resolveCurrentPath(currentPath);
59
60
  });
60
61
  });
61
62
 
62
63
  // =========================================================================
63
64
  // Methods
64
65
  // =========================================================================
65
- const getCurrentPath = () => {
66
- // アプリ側から現在パスが明示的に渡されていればそれを優先(SSR対応)
67
- if (currentPath && currentPath !== '') {
68
- return currentPath;
69
- }
70
-
71
- // それ以外の場合はクライアント実行時のみ window.location から取得
72
- if (typeof window !== 'undefined') {
73
- return window.location.pathname;
74
- }
75
-
76
- return '';
77
- };
78
-
79
- // パスの正規化処理
80
- const normalizePath = (path: string): string => {
81
- if (!pathPrefix) return path;
82
-
83
- // pathPrefixが設定されている場合、それを除去
84
- if (path.startsWith(pathPrefix)) {
85
- const normalized = path.substring(pathPrefix.length);
86
- return normalized.startsWith('/') ? normalized : '/' + normalized;
87
- }
88
-
89
- return path;
90
- };
91
-
92
- // パスマッチング関数
93
- const matchPath = (currentPath: string, itemHref: string, item: MenuItem): boolean => {
94
- if (customPathMatcher) {
95
- return customPathMatcher(currentPath, itemHref, item);
96
- }
97
-
98
- const normalizedCurrentPath = normalizePath(currentPath);
99
-
100
- // matchingPathのチェック
101
- if (item.matchingPath?.some((href) => normalizedCurrentPath.startsWith(href))) {
102
- return true;
103
- }
104
-
105
- // strictMatchの場合
106
- if (item.strictMatch) {
107
- return normalizedCurrentPath === itemHref;
108
- }
109
-
110
- // ルートパス (/) の特別な処理
111
- if (itemHref === '/') {
112
- return normalizedCurrentPath === '/';
113
- }
114
-
115
- // その他のパス
116
- return normalizedCurrentPath !== '' && normalizedCurrentPath.startsWith(itemHref);
117
- };
118
66
 
119
67
  // シンプルなキーボードナビゲーション(disabled タブはスキップ)
120
68
  const handleKeyDown = (event: KeyboardEvent) => {
@@ -167,7 +115,7 @@
167
115
  const item = tabItems[i];
168
116
  if (!item.href) continue;
169
117
 
170
- if (matchPath(resolvedCurrentPath, item.href, item)) {
118
+ if (doMatchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
171
119
  return i;
172
120
  }
173
121
  }
package/dist/index.d.ts CHANGED
@@ -32,6 +32,8 @@ export { default as SkeletonText } from './components/skeleton/SkeletonText.svel
32
32
  export { default as SkeletonBox } from './components/skeleton/SkeletonBox.svelte';
33
33
  export { default as SkeletonAvatar } from './components/skeleton/SkeletonAvatar.svelte';
34
34
  export { default as Switch } from './components/Switch.svelte';
35
+ export { default as Nav } from './components/Nav.svelte';
36
+ export { default as NavItem } from './components/NavItem.svelte';
35
37
  export { default as Tab } from './components/Tab.svelte';
36
38
  export { default as TabItem } from './components/TabItem.svelte';
37
39
  export { default as Textarea } from './components/Textarea.svelte';
@@ -73,10 +75,12 @@ export type { SkeletonButtonProps } from './components/skeleton/SkeletonButton.s
73
75
  export type { SkeletonHeadingProps } from './components/skeleton/SkeletonHeading.svelte';
74
76
  export type { SkeletonMediaProps } from './components/skeleton/SkeletonMedia.svelte';
75
77
  export type { SwitchProps } from './components/Switch.svelte';
78
+ export type { NavProps } from './components/Nav.svelte';
79
+ export type { NavItemProps, NavItemSelectedStyle } from './components/NavItem.svelte';
76
80
  export type { TabProps } from './components/Tab.svelte';
77
81
  export type { TabItemProps } from './components/TabItem.svelte';
78
82
  export type { TextareaProps } from './components/Textarea.svelte';
79
- export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle } from './types/propOptions';
83
+ export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant } from './types/propOptions';
80
84
  export type { MenuItem } from './types/menuItem';
81
85
  export type { SegmentedControlItem } from './types/segmentedControlItem';
82
86
  export * from './utils/accessibility';
package/dist/index.js CHANGED
@@ -33,6 +33,8 @@ export { default as SkeletonText } from './components/skeleton/SkeletonText.svel
33
33
  export { default as SkeletonBox } from './components/skeleton/SkeletonBox.svelte';
34
34
  export { default as SkeletonAvatar } from './components/skeleton/SkeletonAvatar.svelte';
35
35
  export { default as Switch } from './components/Switch.svelte';
36
+ export { default as Nav } from './components/Nav.svelte';
37
+ export { default as NavItem } from './components/NavItem.svelte';
36
38
  export { default as Tab } from './components/Tab.svelte';
37
39
  export { default as TabItem } from './components/TabItem.svelte';
38
40
  export { default as Textarea } from './components/Textarea.svelte';
@@ -52,3 +52,8 @@ export type DatepickerMode = 'single' | 'range';
52
52
  * Used by Input, Textarea, Datepicker components
53
53
  */
54
54
  export type FocusStyle = 'background' | 'outline' | 'none';
55
+ /**
56
+ * Nav variant type
57
+ * Used by Nav component
58
+ */
59
+ export type NavVariant = 'vertical' | 'horizontal' | 'mobile' | 'tab';
@@ -0,0 +1,4 @@
1
+ import type { MenuItem } from '../types/menuItem';
2
+ export declare const getCurrentPath: (currentPath?: string) => string;
3
+ export declare const normalizePath: (path: string, pathPrefix: string) => string;
4
+ export declare const matchPath: (currentPath: string, itemHref: string, item: MenuItem, pathPrefix: string, customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean) => boolean;
@@ -0,0 +1,28 @@
1
+ export const getCurrentPath = (currentPath) => {
2
+ if (currentPath && currentPath !== '')
3
+ return currentPath;
4
+ if (typeof window !== 'undefined')
5
+ return window.location.pathname;
6
+ return '';
7
+ };
8
+ export const normalizePath = (path, pathPrefix) => {
9
+ if (!pathPrefix)
10
+ return path;
11
+ if (path.startsWith(pathPrefix)) {
12
+ const normalized = path.substring(pathPrefix.length);
13
+ return normalized.startsWith('/') ? normalized : '/' + normalized;
14
+ }
15
+ return path;
16
+ };
17
+ export const matchPath = (currentPath, itemHref, item, pathPrefix, customPathMatcher) => {
18
+ if (customPathMatcher)
19
+ return customPathMatcher(currentPath, itemHref, item);
20
+ const normalizedCurrentPath = normalizePath(currentPath, pathPrefix);
21
+ if (item.matchingPath?.some((href) => normalizedCurrentPath.startsWith(href)))
22
+ return true;
23
+ if (item.strictMatch)
24
+ return normalizedCurrentPath === itemHref;
25
+ if (itemHref === '/')
26
+ return normalizedCurrentPath === '/';
27
+ return normalizedCurrentPath !== '' && normalizedCurrentPath.startsWith(itemHref);
28
+ };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@14ch/svelte-ui",
3
3
  "description": "Modern Svelte UI components library with TypeScript support",
4
4
  "private": false,
5
- "version": "0.0.24",
5
+ "version": "0.0.25",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",