@14ch/svelte-ui 0.0.27 → 0.0.29

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.
Files changed (72) hide show
  1. package/dist/assets/styles/variables.scss +28 -24
  2. package/dist/components/Button.svelte +8 -1
  3. package/dist/components/Button.svelte.d.ts +8 -1
  4. package/dist/components/Checkbox.svelte +7 -0
  5. package/dist/components/Checkbox.svelte.d.ts +7 -0
  6. package/dist/components/CheckboxGroup.svelte +5 -0
  7. package/dist/components/CheckboxGroup.svelte.d.ts +5 -0
  8. package/dist/components/ColorPicker.svelte +3 -0
  9. package/dist/components/ColorPicker.svelte.d.ts +3 -0
  10. package/dist/components/Combobox.svelte +6 -0
  11. package/dist/components/Combobox.svelte.d.ts +6 -0
  12. package/dist/components/ConfirmDialog.svelte +7 -0
  13. package/dist/components/ConfirmDialog.svelte.d.ts +7 -3
  14. package/dist/components/Datepicker.svelte +12 -0
  15. package/dist/components/Datepicker.svelte.d.ts +12 -3
  16. package/dist/components/Dialog.svelte +12 -0
  17. package/dist/components/Dialog.svelte.d.ts +12 -4
  18. package/dist/components/Drawer.svelte +13 -0
  19. package/dist/components/Drawer.svelte.d.ts +13 -4
  20. package/dist/components/Fab.svelte +11 -0
  21. package/dist/components/Fab.svelte.d.ts +11 -0
  22. package/dist/components/FileUploader.svelte +4 -0
  23. package/dist/components/FileUploader.svelte.d.ts +4 -0
  24. package/dist/components/Icon.svelte +8 -0
  25. package/dist/components/Icon.svelte.d.ts +8 -0
  26. package/dist/components/IconButton.svelte +13 -0
  27. package/dist/components/IconButton.svelte.d.ts +13 -0
  28. package/dist/components/ImageUploader.svelte +6 -0
  29. package/dist/components/ImageUploader.svelte.d.ts +6 -0
  30. package/dist/components/Input.svelte +13 -0
  31. package/dist/components/Input.svelte.d.ts +13 -0
  32. package/dist/components/LoadingSpinner.svelte +4 -0
  33. package/dist/components/LoadingSpinner.svelte.d.ts +4 -0
  34. package/dist/components/Modal.svelte +9 -0
  35. package/dist/components/Modal.svelte.d.ts +9 -4
  36. package/dist/components/Nav.svelte +172 -44
  37. package/dist/components/Nav.svelte.d.ts +11 -1
  38. package/dist/components/NavItem.svelte +438 -59
  39. package/dist/components/NavItem.svelte.d.ts +19 -2
  40. package/dist/components/Pagination.svelte +7 -0
  41. package/dist/components/Pagination.svelte.d.ts +7 -0
  42. package/dist/components/Popup.svelte +13 -0
  43. package/dist/components/Popup.svelte.d.ts +13 -4
  44. package/dist/components/PopupMenu.svelte +8 -0
  45. package/dist/components/PopupMenu.svelte.d.ts +8 -3
  46. package/dist/components/PopupMenuButton.svelte +7 -0
  47. package/dist/components/PopupMenuButton.svelte.d.ts +7 -0
  48. package/dist/components/Radio.svelte +7 -0
  49. package/dist/components/Radio.svelte.d.ts +7 -0
  50. package/dist/components/RadioGroup.svelte +5 -0
  51. package/dist/components/RadioGroup.svelte.d.ts +5 -0
  52. package/dist/components/SegmentedControl.svelte +5 -0
  53. package/dist/components/SegmentedControl.svelte.d.ts +5 -0
  54. package/dist/components/Select.svelte +4 -0
  55. package/dist/components/Select.svelte.d.ts +4 -0
  56. package/dist/components/Slider.svelte +5 -0
  57. package/dist/components/Slider.svelte.d.ts +5 -0
  58. package/dist/components/Snackbar.svelte +4 -0
  59. package/dist/components/Snackbar.svelte.d.ts +4 -0
  60. package/dist/components/Switch.svelte +3 -0
  61. package/dist/components/Switch.svelte.d.ts +3 -0
  62. package/dist/components/Tab.svelte +51 -118
  63. package/dist/components/Tab.svelte.d.ts +15 -1
  64. package/dist/components/Textarea.svelte +9 -0
  65. package/dist/components/Textarea.svelte.d.ts +9 -0
  66. package/dist/index.d.ts +5 -3
  67. package/dist/index.js +0 -1
  68. package/dist/types/menuItem.d.ts +1 -0
  69. package/dist/types/propOptions.d.ts +10 -1
  70. package/package.json +1 -1
  71. package/dist/components/TabItem.svelte +0 -219
  72. package/dist/components/TabItem.svelte.d.ts +0 -19
@@ -4,7 +4,7 @@
4
4
  import NavItem from './NavItem.svelte';
5
5
  import type { NavItemSelectedStyle } from './NavItem.svelte';
6
6
  import type { MenuItem } from '../types/menuItem';
7
- import type { NavVariant } from '../types/propOptions';
7
+ import type { NavVariant, SubMenuMode } from '../types/propOptions';
8
8
  import { subscribeUrlChange } from '../utils/urlChange';
9
9
  import { getCurrentPath, matchPath } from '../utils/navPath';
10
10
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
@@ -14,10 +14,15 @@
14
14
  // =========================================================================
15
15
  export type NavProps = {
16
16
  // 基本プロパティ
17
+ /** `{ label, href, icon?, children?, disabled? }[]` */
17
18
  navItems?: MenuItem[];
19
+ /** Layout variant. @default 'horizontal' */
18
20
  variant?: NavVariant;
21
+ /** Prepended to each item's href for active-state matching. */
19
22
  pathPrefix?: string;
23
+ /** Custom function to determine if an item is active. */
20
24
  customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
25
+ /** Overrides the auto-detected current path. */
21
26
  currentPath?: string;
22
27
 
23
28
  // HTML属性
@@ -31,8 +36,13 @@
31
36
  iconVariant?: IconVariant;
32
37
 
33
38
  // スタイル/レイアウト
39
+ /** Visual style for the selected item. */
34
40
  selectedStyle?: NavItemSelectedStyle;
35
41
  gap?: number | string;
42
+ /** Sub-menu display mode for items that have children. @default 'popup' */
43
+ subMenuMode?: SubMenuMode;
44
+ /** Show chevron icon on parent items. @default true */
45
+ chevron?: boolean;
36
46
 
37
47
  // ARIA/アクセシビリティ
38
48
  ariaLabel?: string;
@@ -42,7 +52,7 @@
42
52
  let {
43
53
  // 基本プロパティ
44
54
  navItems = [],
45
- variant = 'tab',
55
+ variant = 'horizontal',
46
56
  pathPrefix = '',
47
57
  customPathMatcher,
48
58
  currentPath,
@@ -60,6 +70,8 @@
60
70
  // スタイル/レイアウト
61
71
  selectedStyle,
62
72
  gap,
73
+ subMenuMode = 'popup',
74
+ chevron,
63
75
 
64
76
  // ARIA/アクセシビリティ
65
77
  ariaLabel,
@@ -67,6 +79,11 @@
67
79
  }: NavProps = $props();
68
80
 
69
81
  let resolvedCurrentPath = $state('');
82
+ let navEl: HTMLElement | undefined = $state();
83
+ let subBarEl: HTMLElement | undefined = $state();
84
+
85
+ // bar/accordion モード: 展開中の親アイテム($state.raw で Proxy ラップを避け === 比較を正常にする)
86
+ let expandedParent: MenuItem | null = $state.raw(null);
70
87
 
71
88
  // =========================================================================
72
89
  // Effects
@@ -78,25 +95,58 @@
78
95
  $effect(() => {
79
96
  return subscribeUrlChange(() => {
80
97
  resolvedCurrentPath = getCurrentPath(currentPath);
98
+ if (subMenuMode !== 'accordion') expandedParent = null;
81
99
  });
82
100
  });
83
101
 
102
+ // accordion モード: アクティブな子を持つ親を自動展開
103
+ $effect(() => {
104
+ if (subMenuMode !== 'accordion' || !resolvedCurrentPath) return;
105
+ const activeParent = navItems.find((item) =>
106
+ item.children?.some(
107
+ (child) =>
108
+ child.href &&
109
+ matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher)
110
+ )
111
+ );
112
+ if (activeParent) expandedParent = activeParent;
113
+ });
114
+
84
115
  // =========================================================================
85
116
  // Methods
86
117
  // =========================================================================
87
- const handleKeyDown = (event: KeyboardEvent) => {
88
- if (navItems.length === 0 || enabledIndices.length === 0) return;
118
+ const focusExpandedParent = () => {
119
+ if (!expandedParent || !navEl) return;
120
+ const idx = navItems.indexOf(expandedParent);
121
+ const parentEls = Array.from(navEl.querySelectorAll<HTMLElement>('[data-nav-item]'));
122
+ parentEls[idx]?.focus();
123
+ };
89
124
 
125
+ const handleKeyDown = (event: KeyboardEvent) => {
126
+ // accordion/expanded は子アイテムも DOM 順で含める
127
+ const includeChildren = subMenuMode === 'accordion' || subMenuMode === 'expanded';
128
+ const selector = includeChildren ? '[data-nav-item], [data-nav-item-child]' : '[data-nav-item]';
90
129
  const navItemEls = Array.from(
91
- (event.currentTarget as HTMLElement).querySelectorAll('[data-nav-item]')
92
- ) as HTMLElement[];
93
- const currentItem = event.target as HTMLElement;
94
- const currentIndex = navItemEls.indexOf(currentItem);
130
+ (event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>(selector)
131
+ ).filter(el => el.tabIndex !== -1);
132
+
133
+ if (navItemEls.length === 0) return;
95
134
 
135
+ const currentIndex = navItemEls.indexOf(event.target as HTMLElement);
96
136
  if (currentIndex === -1) return;
97
137
 
98
- const currentEnabledPosition = enabledIndices.indexOf(currentIndex);
99
- let nextEnabledPosition = currentEnabledPosition;
138
+ // bar モード: 展開中の親で ArrowDown → サブバーの最初の子へ
139
+ if (subMenuMode === 'bar' && event.key === 'ArrowDown' && showSubBar) {
140
+ const allNavItemEls = Array.from(
141
+ (event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>('[data-nav-item]')
142
+ );
143
+ const expandedEl = allNavItemEls[navItems.indexOf(expandedParent!)];
144
+ if (expandedEl && expandedEl.contains(event.target as Node)) {
145
+ event.preventDefault();
146
+ subBarEl?.querySelector<HTMLElement>('[data-nav-item-child]:not([tabindex="-1"])')?.focus();
147
+ return;
148
+ }
149
+ }
100
150
 
101
151
  const isVertical = variant === 'vertical';
102
152
  const prevKey = isVertical ? 'ArrowUp' : 'ArrowLeft';
@@ -105,28 +155,70 @@
105
155
  switch (event.key) {
106
156
  case prevKey:
107
157
  event.preventDefault();
108
- nextEnabledPosition =
109
- currentEnabledPosition > 0 ? currentEnabledPosition - 1 : enabledIndices.length - 1;
158
+ navItemEls[currentIndex > 0 ? currentIndex - 1 : navItemEls.length - 1]?.focus();
110
159
  break;
111
160
  case nextKey:
112
161
  event.preventDefault();
113
- nextEnabledPosition =
114
- currentEnabledPosition < enabledIndices.length - 1 ? currentEnabledPosition + 1 : 0;
162
+ navItemEls[currentIndex < navItemEls.length - 1 ? currentIndex + 1 : 0]?.focus();
115
163
  break;
116
164
  case 'Home':
117
165
  event.preventDefault();
118
- nextEnabledPosition = 0;
166
+ navItemEls[0]?.focus();
119
167
  break;
120
168
  case 'End':
121
169
  event.preventDefault();
122
- nextEnabledPosition = enabledIndices.length - 1;
170
+ navItemEls[navItemEls.length - 1]?.focus();
123
171
  break;
124
172
  default:
125
173
  return;
126
174
  }
175
+ };
176
+
177
+ const handleSubBarKeyDown = (event: KeyboardEvent) => {
178
+ const container = event.currentTarget as HTMLElement;
179
+ const items = Array.from(
180
+ container.querySelectorAll<HTMLElement>('[data-nav-item-child]:not([tabindex="-1"])')
181
+ );
182
+ const currentIndex = items.indexOf(event.target as HTMLElement);
183
+ if (currentIndex === -1) return;
127
184
 
128
- const nextIndex = enabledIndices[nextEnabledPosition];
129
- navItemEls[nextIndex]?.focus();
185
+ switch (event.key) {
186
+ case 'ArrowLeft':
187
+ event.preventDefault();
188
+ items[currentIndex > 0 ? currentIndex - 1 : items.length - 1]?.focus();
189
+ break;
190
+ case 'ArrowRight':
191
+ event.preventDefault();
192
+ items[currentIndex < items.length - 1 ? currentIndex + 1 : 0]?.focus();
193
+ break;
194
+ case 'ArrowUp':
195
+ event.preventDefault();
196
+ focusExpandedParent();
197
+ break;
198
+ case 'Home':
199
+ event.preventDefault();
200
+ items[0]?.focus();
201
+ break;
202
+ case 'End':
203
+ event.preventDefault();
204
+ items[items.length - 1]?.focus();
205
+ break;
206
+ case 'Escape':
207
+ event.preventDefault();
208
+ focusExpandedParent();
209
+ expandedParent = null;
210
+ break;
211
+ }
212
+ };
213
+
214
+ const handleSubMenuToggle = (item: MenuItem) => {
215
+ if (subMenuMode === 'accordion') {
216
+ // accordion: 開くのみ(再クリックで閉じない、他は自動的に閉じる)
217
+ expandedParent = item;
218
+ } else {
219
+ // bar: トグル
220
+ expandedParent = expandedParent === item ? null : item;
221
+ }
130
222
  };
131
223
 
132
224
  // =========================================================================
@@ -135,33 +227,37 @@
135
227
  const selectedIndex = $derived.by(() => {
136
228
  for (let i = 0; i < navItems.length; i++) {
137
229
  const item = navItems[i];
138
- if (!item.href) continue;
139
- if (matchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
230
+ if (item.href && matchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
231
+ return i;
232
+ }
233
+ // bar モード: 子が選択されていれば親も選択とみなす
234
+ if (subMenuMode === 'bar' && item.children?.some(
235
+ (child) => child.href && matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher)
236
+ )) {
140
237
  return i;
141
238
  }
142
239
  }
143
240
  return -1;
144
241
  });
145
242
 
146
- const enabledIndices = $derived(
147
- navItems.map((item, i) => (item.disabled ? -1 : i)).filter((i) => i >= 0)
243
+ const showSubBar = $derived(
244
+ variant === 'horizontal' && subMenuMode === 'bar' && expandedParent != null
148
245
  );
149
246
 
150
- const isTabVariant = $derived(variant === 'tab');
247
+ const isChildSelected = (child: MenuItem) =>
248
+ !!child.href && matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher);
151
249
  </script>
152
250
 
153
251
  <nav
154
252
  class="nav nav--{variant}"
155
- role={isTabVariant ? 'tablist' : undefined}
156
253
  aria-label={ariaLabelledby ? undefined : ariaLabel}
157
254
  aria-labelledby={ariaLabelledby}
158
- aria-orientation={variant === 'vertical' ? 'vertical' : 'horizontal'}
159
255
  style:--internal-nav-gap={gap != null ? (typeof gap === 'number' ? `${gap}px` : gap) : undefined}
160
- tabindex="-1"
161
256
  {id}
162
- onkeydown={handleKeyDown}
163
257
  data-testid="nav"
258
+ bind:this={navEl}
164
259
  >
260
+ <div style="display: contents" role="presentation" onkeydown={handleKeyDown}>
165
261
  {#each navItems as item, index}
166
262
  <NavItem
167
263
  {item}
@@ -175,32 +271,47 @@
175
271
  {iconOpticalSize}
176
272
  {iconVariant}
177
273
  {selectedStyle}
274
+ {subMenuMode}
275
+ {chevron}
276
+ {resolvedCurrentPath}
277
+ {customPathMatcher}
278
+ isSubMenuExpanded={(subMenuMode === 'bar' || subMenuMode === 'accordion') && expandedParent === item}
279
+ onSubMenuToggle={handleSubMenuToggle}
280
+ onClose={() => { expandedParent = null; }}
178
281
  />
179
282
  {/each}
283
+ </div>
180
284
  </nav>
181
285
 
286
+ <!-- bar モード: 選択中の親の子アイテムを横バーとして表示 -->
287
+ {#if showSubBar && expandedParent?.children}
288
+ <div class="nav__sub-bar" role="menu" tabindex="-1" onkeydown={handleSubBarKeyDown} bind:this={subBarEl}>
289
+ {#each expandedParent.children as child}
290
+ <NavItem
291
+ item={child}
292
+ variant="horizontal"
293
+ {pathPrefix}
294
+ {iconFilled}
295
+ {iconWeight}
296
+ {iconGrade}
297
+ {iconOpticalSize}
298
+ {iconVariant}
299
+ {selectedStyle}
300
+ isChild={true}
301
+ {resolvedCurrentPath}
302
+ {customPathMatcher}
303
+ isSelected={isChildSelected(child)}
304
+ isDisabled={child.disabled ?? false}
305
+ />
306
+ {/each}
307
+ </div>
308
+ {/if}
309
+
182
310
  <style>.nav {
183
311
  display: flex;
184
312
  box-sizing: border-box;
185
313
  }
186
314
 
187
- .nav--tab {
188
- flex-direction: row;
189
- justify-content: start;
190
- position: relative;
191
- width: 100%;
192
- height: 100%;
193
- min-height: var(--svelte-ui-tab-min-height);
194
- overflow-x: auto;
195
- overflow-y: visible;
196
- -ms-overflow-style: none;
197
- overscroll-behavior: contain;
198
- }
199
-
200
- .nav--tab::-webkit-scrollbar {
201
- display: none;
202
- }
203
-
204
315
  .nav--mobile {
205
316
  flex-direction: row;
206
317
  width: 100%;
@@ -215,7 +326,24 @@
215
326
  }
216
327
 
217
328
  .nav--horizontal {
329
+ flex-direction: row;
330
+ justify-content: start;
331
+ align-items: center;
332
+ gap: var(--internal-nav-gap, var(--svelte-ui-nav-horizontal-item-gap));
333
+ overflow-x: auto;
334
+ overflow-y: visible;
335
+ -ms-overflow-style: none;
336
+ overscroll-behavior: none;
337
+ }
338
+
339
+ .nav--horizontal::-webkit-scrollbar {
340
+ display: none;
341
+ }
342
+
343
+ .nav__sub-bar {
344
+ display: flex;
218
345
  flex-direction: row;
219
346
  gap: var(--internal-nav-gap, var(--svelte-ui-nav-horizontal-item-gap));
220
347
  align-items: center;
348
+ min-height: var(--svelte-ui-nav-sub-bar-min-height);
221
349
  }</style>
@@ -1,12 +1,17 @@
1
1
  import type { NavItemSelectedStyle } from './NavItem.svelte';
2
2
  import type { MenuItem } from '../types/menuItem';
3
- import type { NavVariant } from '../types/propOptions';
3
+ import type { NavVariant, SubMenuMode } from '../types/propOptions';
4
4
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
5
5
  export type NavProps = {
6
+ /** `{ label, href, icon?, children?, disabled? }[]` */
6
7
  navItems?: MenuItem[];
8
+ /** Layout variant. @default 'horizontal' */
7
9
  variant?: NavVariant;
10
+ /** Prepended to each item's href for active-state matching. */
8
11
  pathPrefix?: string;
12
+ /** Custom function to determine if an item is active. */
9
13
  customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
14
+ /** Overrides the auto-detected current path. */
10
15
  currentPath?: string;
11
16
  id?: string;
12
17
  iconFilled?: boolean;
@@ -14,8 +19,13 @@ export type NavProps = {
14
19
  iconGrade?: IconGrade;
15
20
  iconOpticalSize?: IconOpticalSize;
16
21
  iconVariant?: IconVariant;
22
+ /** Visual style for the selected item. */
17
23
  selectedStyle?: NavItemSelectedStyle;
18
24
  gap?: number | string;
25
+ /** Sub-menu display mode for items that have children. @default 'popup' */
26
+ subMenuMode?: SubMenuMode;
27
+ /** Show chevron icon on parent items. @default true */
28
+ chevron?: boolean;
19
29
  ariaLabel?: string;
20
30
  ariaLabelledby?: string;
21
31
  };