@14ch/svelte-ui 0.0.28 → 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.
@@ -1,10 +1,9 @@
1
1
  <!-- Tab.svelte -->
2
2
 
3
3
  <script lang="ts">
4
- import TabItem from './TabItem.svelte';
4
+ import Nav from './Nav.svelte';
5
5
  import type { MenuItem } from '../types/menuItem';
6
- import { subscribeUrlChange } from '../utils/urlChange';
7
- import { getCurrentPath as resolveCurrentPath, matchPath as doMatchPath } from '../utils/navPath';
6
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
8
7
 
9
8
  // =========================================================================
10
9
  // Props, States & Constants
@@ -12,7 +11,7 @@
12
11
  export type TabProps = {
13
12
  // 基本プロパティ
14
13
  /** `{ label, href, icon?, disabled? }[]` */
15
- tabItems: MenuItem[];
14
+ tabItems?: MenuItem[];
16
15
  /** Prepended to each item's href for active-state matching. */
17
16
  pathPrefix?: string;
18
17
  /** Custom function to determine if an item is active. */
@@ -20,6 +19,16 @@
20
19
  /** Overrides the auto-detected current path. */
21
20
  currentPath?: string;
22
21
 
22
+ // HTML属性
23
+ id?: string;
24
+
25
+ // アイコン関連
26
+ iconFilled?: boolean;
27
+ iconWeight?: IconWeight;
28
+ iconGrade?: IconGrade;
29
+ iconOpticalSize?: IconOpticalSize;
30
+ iconVariant?: IconVariant;
31
+
23
32
  // スタイル/レイアウト
24
33
  /** Custom CSS color for tab labels. */
25
34
  textColor?: string;
@@ -40,7 +49,17 @@
40
49
  customPathMatcher,
41
50
  currentPath,
42
51
 
43
- // スタイル/レイアウト(未指定時は TabItem が variables の tab 用変数を直接参照)
52
+ // HTML属性
53
+ id,
54
+
55
+ // アイコン関連
56
+ iconFilled = false,
57
+ iconWeight = 300,
58
+ iconGrade = 0,
59
+ iconOpticalSize = 24,
60
+ iconVariant = 'outlined',
61
+
62
+ // スタイル/レイアウト
44
63
  textColor,
45
64
  selectedTextColor,
46
65
  selectedBarColor,
@@ -49,128 +68,35 @@
49
68
  ariaLabel = 'Tabs',
50
69
  ariaLabelledby
51
70
  }: TabProps = $props();
52
-
53
- let resolvedCurrentPath = $state('');
54
-
55
- // =========================================================================
56
- // Effects
57
- // =========================================================================
58
- $effect(() => {
59
- // props の currentPath が変更されたとき
60
- resolvedCurrentPath = resolveCurrentPath(currentPath);
61
- });
62
-
63
- $effect(() => {
64
- // URL の変更を subscribe
65
- return subscribeUrlChange(() => {
66
- resolvedCurrentPath = resolveCurrentPath(currentPath);
67
- });
68
- });
69
-
70
- // =========================================================================
71
- // Methods
72
- // =========================================================================
73
-
74
- // シンプルなキーボードナビゲーション(disabled タブはスキップ)
75
- const handleKeyDown = (event: KeyboardEvent) => {
76
- if (tabItems.length === 0 || enabledIndices.length === 0) return;
77
-
78
- const tabList = event.currentTarget as HTMLElement;
79
- const tabs = Array.from(tabList.querySelectorAll('[role="tab"]')) as HTMLElement[];
80
- const currentTab = event.target as HTMLElement;
81
- const currentIndex = tabs.indexOf(currentTab);
82
-
83
- if (currentIndex === -1) return;
84
-
85
- const currentEnabledPosition = enabledIndices.indexOf(currentIndex);
86
- let nextEnabledPosition = currentEnabledPosition;
87
-
88
- switch (event.key) {
89
- case 'ArrowLeft':
90
- event.preventDefault();
91
- nextEnabledPosition =
92
- currentEnabledPosition > 0 ? currentEnabledPosition - 1 : enabledIndices.length - 1;
93
- break;
94
- case 'ArrowRight':
95
- event.preventDefault();
96
- nextEnabledPosition =
97
- currentEnabledPosition < enabledIndices.length - 1 ? currentEnabledPosition + 1 : 0;
98
- break;
99
- case 'Home':
100
- event.preventDefault();
101
- nextEnabledPosition = 0;
102
- break;
103
- case 'End':
104
- event.preventDefault();
105
- nextEnabledPosition = enabledIndices.length - 1;
106
- break;
107
- default:
108
- return;
109
- }
110
-
111
- const nextIndex = enabledIndices[nextEnabledPosition];
112
- tabs[nextIndex]?.focus();
113
- };
114
-
115
- // =========================================================================
116
- // $derived
117
- // =========================================================================
118
-
119
- // アクティブなタブのインデックスを現在のパスに基づいて計算
120
- const selectedTabIndex = $derived.by(() => {
121
- for (let i = 0; i < tabItems.length; i++) {
122
- const item = tabItems[i];
123
- if (!item.href) continue;
124
-
125
- if (doMatchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
126
- return i;
127
- }
128
- }
129
- return -1;
130
- });
131
-
132
- // 有効なタブのインデックス一覧(disabled を除く)
133
- const enabledIndices = $derived(
134
- tabItems.map((item, i) => (item.disabled ? -1 : i)).filter((i) => i >= 0)
135
- );
136
71
  </script>
137
72
 
138
73
  <div
139
74
  class="tab"
140
- role="tablist"
141
- aria-label={ariaLabelledby ? undefined : ariaLabel}
142
- aria-labelledby={ariaLabelledby}
143
- tabindex="-1"
144
- onkeydown={handleKeyDown}
75
+ style:--svelte-ui-nav-item-underline-text-color={textColor}
76
+ style:--svelte-ui-nav-item-underline-selected-text-color={selectedTextColor}
77
+ style:--svelte-ui-nav-item-underline-bar-color={selectedBarColor}
145
78
  data-testid="tab"
146
79
  >
147
- {#each tabItems as tabItem, index}
148
- <TabItem
149
- {tabItem}
150
- {pathPrefix}
151
- isSelected={index === selectedTabIndex}
152
- isDisabled={tabItem.disabled ?? false}
153
- {textColor}
154
- {selectedTextColor}
155
- {selectedBarColor}
156
- />
157
- {/each}
80
+ <Nav
81
+ navItems={tabItems}
82
+ variant="horizontal"
83
+ selectedStyle="underline"
84
+ {pathPrefix}
85
+ {customPathMatcher}
86
+ {currentPath}
87
+ {id}
88
+ {iconFilled}
89
+ {iconWeight}
90
+ {iconGrade}
91
+ {iconOpticalSize}
92
+ {iconVariant}
93
+ {ariaLabel}
94
+ {ariaLabelledby}
95
+ />
158
96
  </div>
159
97
 
160
98
  <style>.tab {
161
- display: flex;
162
- justify-content: start;
163
- position: relative;
164
- width: 100%;
165
- height: 100%;
166
99
  min-height: var(--svelte-ui-tab-min-height);
167
- overflow-x: auto;
168
- overflow-y: visible;
169
- -ms-overflow-style: none;
170
- overscroll-behavior: contain;
100
+ width: 100%;
171
101
  box-sizing: border-box;
172
- }
173
-
174
- .tab::-webkit-scrollbar {
175
- display: none;
176
102
  }</style>
@@ -1,13 +1,20 @@
1
1
  import type { MenuItem } from '../types/menuItem';
2
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
2
3
  export type TabProps = {
3
4
  /** `{ label, href, icon?, disabled? }[]` */
4
- tabItems: MenuItem[];
5
+ tabItems?: MenuItem[];
5
6
  /** Prepended to each item's href for active-state matching. */
6
7
  pathPrefix?: string;
7
8
  /** Custom function to determine if an item is active. */
8
9
  customPathMatcher?: (currentPath: string, itemHref: string, item: MenuItem) => boolean;
9
10
  /** Overrides the auto-detected current path. */
10
11
  currentPath?: string;
12
+ id?: string;
13
+ iconFilled?: boolean;
14
+ iconWeight?: IconWeight;
15
+ iconGrade?: IconGrade;
16
+ iconOpticalSize?: IconOpticalSize;
17
+ iconVariant?: IconVariant;
11
18
  /** Custom CSS color for tab labels. */
12
19
  textColor?: string;
13
20
  /** Custom CSS color for the active tab label. */
package/dist/index.d.ts CHANGED
@@ -35,7 +35,6 @@ export { default as Switch } from './components/Switch.svelte';
35
35
  export { default as Nav } from './components/Nav.svelte';
36
36
  export { default as NavItem } from './components/NavItem.svelte';
37
37
  export { default as Tab } from './components/Tab.svelte';
38
- export { default as TabItem } from './components/TabItem.svelte';
39
38
  export { default as Textarea } from './components/Textarea.svelte';
40
39
  export type { ButtonProps } from './components/Button.svelte';
41
40
  export type { CheckboxProps } from './components/Checkbox.svelte';
@@ -78,9 +77,8 @@ export type { SwitchProps } from './components/Switch.svelte';
78
77
  export type { NavProps } from './components/Nav.svelte';
79
78
  export type { NavItemProps, NavItemSelectedStyle } from './components/NavItem.svelte';
80
79
  export type { TabProps } from './components/Tab.svelte';
81
- export type { TabItemProps } from './components/TabItem.svelte';
82
80
  export type { TextareaProps } from './components/Textarea.svelte';
83
- export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant } from './types/propOptions';
81
+ export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant, SubMenuMode } from './types/propOptions';
84
82
  export type { MenuItem } from './types/menuItem';
85
83
  export type { SegmentedControlItem } from './types/segmentedControlItem';
86
84
  export type { Option, OptionValue } from './types/options';
package/dist/index.js CHANGED
@@ -36,7 +36,6 @@ export { default as Switch } from './components/Switch.svelte';
36
36
  export { default as Nav } from './components/Nav.svelte';
37
37
  export { default as NavItem } from './components/NavItem.svelte';
38
38
  export { default as Tab } from './components/Tab.svelte';
39
- export { default as TabItem } from './components/TabItem.svelte';
40
39
  export { default as Textarea } from './components/Textarea.svelte';
41
40
  // Utils
42
41
  export * from './utils/accessibility';
@@ -7,4 +7,5 @@ export type MenuItem = {
7
7
  matchingPath?: string[];
8
8
  strictMatch?: boolean;
9
9
  disabled?: boolean;
10
+ children?: MenuItem[];
10
11
  };
@@ -56,4 +56,13 @@ export type FocusStyle = 'background' | 'outline' | 'none';
56
56
  * Nav variant type
57
57
  * Used by Nav component
58
58
  */
59
- export type NavVariant = 'vertical' | 'horizontal' | 'mobile' | 'tab';
59
+ export type NavVariant = 'vertical' | 'horizontal' | 'mobile';
60
+ /**
61
+ * Sub-menu display mode for Nav hierarchical menus.
62
+ * - `popup`: child items appear in a floating panel (all variants)
63
+ * - `accordion`: child items expand/collapse inline (vertical only)
64
+ * - `expanded`: child items are always visible inline (vertical only)
65
+ * - `bar`: child items appear in a secondary bar below the nav (horizontal only)
66
+ * - `bottom-sheet`: child items appear in a fixed bottom sheet overlay (mobile only)
67
+ */
68
+ export type SubMenuMode = 'popup' | 'accordion' | 'expanded' | 'bar' | 'bottom-sheet';
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.28",
5
+ "version": "0.0.29",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",
@@ -1,219 +0,0 @@
1
- <!-- TabItem.svelte -->
2
-
3
- <script lang="ts">
4
- import Icon from './Icon.svelte';
5
- import type { MenuItem } from '../types/menuItem';
6
- import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
7
-
8
- // =========================================================================
9
- // Props, States & Constants
10
- // =========================================================================
11
- export type TabItemProps = {
12
- // 基本プロパティ
13
- tabItem: MenuItem;
14
- pathPrefix?: string;
15
-
16
- // スタイル/レイアウト(未指定時は variables の tab 用変数を参照)
17
- textColor?: string;
18
- selectedTextColor?: string;
19
- selectedBarColor?: string;
20
-
21
- // アイコン関連
22
- iconFilled?: boolean;
23
- iconWeight?: IconWeight;
24
- iconGrade?: IconGrade;
25
- iconOpticalSize?: IconOpticalSize;
26
- iconVariant?: IconVariant;
27
-
28
- // 状態/動作
29
- isSelected?: boolean;
30
- isDisabled?: boolean;
31
- };
32
-
33
- let {
34
- // 基本プロパティ
35
- tabItem,
36
- pathPrefix = '',
37
-
38
- // スタイル/レイアウト
39
- textColor,
40
- selectedTextColor,
41
- selectedBarColor,
42
-
43
- // アイコン関連
44
- iconFilled = false,
45
- iconWeight = 300,
46
- iconGrade = 0,
47
- iconOpticalSize = 24,
48
- iconVariant = 'outlined',
49
-
50
- // 状態/動作
51
- isSelected = false,
52
- isDisabled = false
53
- }: TabItemProps = $props();
54
-
55
- // =========================================================================
56
- // $derived
57
- // =========================================================================
58
-
59
- // pathPrefixを付与したhrefを計算
60
- const hrefWithPrefix = $derived.by(() => {
61
- if (!tabItem.href) return undefined;
62
- if (!pathPrefix) return tabItem.href;
63
-
64
- // 既にpathPrefixが含まれている場合はそのまま
65
- // pathPrefixが完全一致、またはpathPrefix + '/'で始まる場合
66
- if (tabItem.href === pathPrefix || tabItem.href.startsWith(`${pathPrefix}/`)) {
67
- return tabItem.href;
68
- }
69
-
70
- // pathPrefixを付与
71
- return `${pathPrefix}${tabItem.href.startsWith('/') ? '' : '/'}${tabItem.href}`;
72
- });
73
-
74
- // 明示的に渡されたときだけ内部CSS変数で上書き。未渡しなら variables の tab 用変数をそのまま参照
75
- const internalTextColor = $derived(textColor);
76
- const internalSelectedTextColor = $derived(selectedTextColor);
77
- const internalSelectedBarColor = $derived(selectedBarColor);
78
- </script>
79
-
80
- {#if isDisabled}
81
- <span
82
- class="tab-item tab-item--disabled"
83
- class:tab-item--selected={isSelected}
84
- style:--internal-tab-item-text-color={internalTextColor}
85
- style:--internal-tab-item-selected-text-color={internalSelectedTextColor}
86
- style:--internal-tab-item-selected-bar-color={internalSelectedBarColor}
87
- role="tab"
88
- aria-selected={isSelected}
89
- aria-disabled="true"
90
- tabindex="-1"
91
- data-testid="tab-item"
92
- >
93
- {#if tabItem.icon}
94
- <div class="tab-item__icon">
95
- <Icon
96
- filled={iconFilled || isSelected}
97
- weight={iconWeight}
98
- grade={iconGrade}
99
- opticalSize={iconOpticalSize}
100
- variant={iconVariant}>{tabItem.icon}</Icon
101
- >
102
- </div>
103
- {/if}
104
- {#if tabItem.label}
105
- <div class="tab-item__label">
106
- {tabItem.label}
107
- </div>
108
- {/if}
109
- </span>
110
- {:else}
111
- <a
112
- href={hrefWithPrefix}
113
- class="tab-item"
114
- class:tab-item--selected={isSelected}
115
- style:--internal-tab-item-text-color={internalTextColor}
116
- style:--internal-tab-item-selected-text-color={internalSelectedTextColor}
117
- style:--internal-tab-item-selected-bar-color={internalSelectedBarColor}
118
- role="tab"
119
- aria-selected={isSelected}
120
- tabindex={0}
121
- data-testid="tab-item"
122
- >
123
- {#if tabItem.icon}
124
- <div class="tab-item__icon">
125
- <Icon
126
- filled={iconFilled || isSelected}
127
- weight={iconWeight}
128
- grade={iconGrade}
129
- opticalSize={iconOpticalSize}
130
- variant={iconVariant}>{tabItem.icon}</Icon
131
- >
132
- </div>
133
- {/if}
134
- {#if tabItem.label}
135
- <div class="tab-item__label">
136
- {tabItem.label}
137
- </div>
138
- {/if}
139
- </a>
140
- {/if}
141
-
142
- <style>@charset "UTF-8";
143
- .tab-item {
144
- display: flex;
145
- justify-content: center;
146
- align-items: center;
147
- gap: var(--svelte-ui-tab-item-icon-gap);
148
- position: relative;
149
- padding: var(--svelte-ui-tab-item-padding);
150
- color: var(--internal-tab-item-text-color, var(--svelte-ui-tab-item-text-color));
151
- white-space: nowrap;
152
- text-decoration: none;
153
- transition-property: background-color, color, outline;
154
- transition-duration: var(--svelte-ui-transition-duration);
155
- outline: none;
156
- }
157
-
158
- @media (hover: hover) {
159
- .tab-item:hover:not(.tab-item--selected) {
160
- color: var(--internal-tab-item-selected-text-color, var(--svelte-ui-tab-item-selected-text-color));
161
- }
162
- .tab-item:hover:not(.tab-item--selected)::before {
163
- opacity: 1;
164
- }
165
- }
166
- /* キーボードフォーカス時のみ枠線を表示 */
167
- .tab-item:focus-visible {
168
- outline: var(--svelte-ui-focus-outline-inner);
169
- outline-offset: var(--svelte-ui-focus-outline-offset-inner);
170
- }
171
-
172
- /* フォールバック: :focus-visible未対応ブラウザ用 */
173
- @supports not selector(:focus-visible) {
174
- .tab-item:focus {
175
- outline: var(--svelte-ui-focus-outline-inner);
176
- outline-offset: var(--svelte-ui-focus-outline-offset-inner);
177
- }
178
- .tab-item:focus:not(.tab-item--selected) {
179
- color: var(--internal-tab-item-selected-text-color, var(--svelte-ui-tab-item-selected-text-color));
180
- }
181
- .tab-item:focus:not(.tab-item--selected)::before {
182
- opacity: 1;
183
- }
184
- }
185
- /* 選択状態のインジケーター */
186
- .tab-item::before {
187
- content: "";
188
- display: block;
189
- position: absolute;
190
- left: calc(var(--svelte-ui-tab-item-padding-x) - var(--svelte-ui-tab-item-selected-bar-offset));
191
- bottom: 0;
192
- width: calc(100% - 2 * var(--svelte-ui-tab-item-padding-x) + 2 * var(--svelte-ui-tab-item-selected-bar-offset));
193
- height: var(--svelte-ui-tab-item-selected-bar-height);
194
- background-color: var(--internal-tab-item-selected-bar-color, var(--svelte-ui-tab-item-selected-bar-color));
195
- border-radius: var(--svelte-ui-tab-item-selected-bar-radius);
196
- opacity: 0;
197
- transition-property: opacity;
198
- transition-duration: var(--svelte-ui-transition-duration);
199
- }
200
-
201
- .tab-item--selected {
202
- color: var(--internal-tab-item-selected-text-color, var(--svelte-ui-tab-item-selected-text-color));
203
- background-color: transparent;
204
- }
205
-
206
- .tab-item--selected::before {
207
- opacity: 1;
208
- }
209
-
210
- .tab-item--disabled {
211
- opacity: var(--svelte-ui-tab-item-disabled-opacity);
212
- pointer-events: none;
213
- cursor: default;
214
- }
215
-
216
- .tab-item__label {
217
- text-box-trim: trim-both;
218
- text-box-edge: cap alphabetic;
219
- }</style>
@@ -1,19 +0,0 @@
1
- import type { MenuItem } from '../types/menuItem';
2
- import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
- export type TabItemProps = {
4
- tabItem: MenuItem;
5
- pathPrefix?: string;
6
- textColor?: string;
7
- selectedTextColor?: string;
8
- selectedBarColor?: string;
9
- iconFilled?: boolean;
10
- iconWeight?: IconWeight;
11
- iconGrade?: IconGrade;
12
- iconOpticalSize?: IconOpticalSize;
13
- iconVariant?: IconVariant;
14
- isSelected?: boolean;
15
- isDisabled?: boolean;
16
- };
17
- declare const TabItem: import("svelte").Component<TabItemProps, {}, "">;
18
- type TabItem = ReturnType<typeof TabItem>;
19
- export default TabItem;