@14ch/svelte-ui 0.0.31 → 0.0.33

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.
@@ -395,10 +395,11 @@
395
395
  --svelte-ui-nav-vertical-item-gap: 8px;
396
396
  /* Nav: horizontal */
397
397
  --svelte-ui-nav-horizontal-item-gap: 8px;
398
+ /* Nav: children spacing (parent item → children list) */
399
+ --svelte-ui-nav-children-offset: 8px;
398
400
  /* Nav: sub-menu */
399
401
  --svelte-ui-nav-sub-popup-min-width: 160px;
400
402
  --svelte-ui-nav-sub-popup-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
401
- --svelte-ui-nav-sub-bar-min-height: 48px;
402
403
  --svelte-ui-nav-item-child-indent: 32px;
403
404
  --svelte-ui-nav-bottom-sheet-overlay-bg: rgba(0, 0, 0, 0.4);
404
405
  --svelte-ui-nav-bottom-sheet-border-radius: 16px 16px 0 0;
@@ -2,7 +2,7 @@
2
2
 
3
3
  <script lang="ts">
4
4
  import NavItem from './NavItem.svelte';
5
- import type { NavItemSelectedStyle } from './NavItem.svelte';
5
+ import type { NavItemSelectedVariant } from './NavItem.svelte';
6
6
  import type { MenuItem } from '../types/menuItem';
7
7
  import type { NavVariant, ChildrenVariant } from '../types/propOptions';
8
8
  import { subscribeUrlChange } from '../utils/urlChange';
@@ -37,12 +37,20 @@
37
37
 
38
38
  // スタイル/レイアウト
39
39
  /** Visual style for the selected item. */
40
- selectedStyle?: NavItemSelectedStyle;
40
+ selectedVariant?: NavItemSelectedVariant;
41
41
  gap?: number | string;
42
42
  /** How child items are displayed. Defaults to `accordion` (vertical), `bar` (horizontal), `bottom-sheet` (mobile). */
43
43
  childrenVariant?: ChildrenVariant;
44
44
  /** Show chevron icon on parent items. @default true */
45
45
  chevron?: boolean;
46
+ /** Inline style applied to the nav container element. */
47
+ customContainerStyle?: string;
48
+ /** Inline style applied to each nav item element. */
49
+ customItemStyle?: string;
50
+ /** Inline style applied to each child nav item element. */
51
+ customChildrenItemStyle?: string;
52
+ /** Inline style applied to the children container (accordion/expanded list, bar row). */
53
+ customChildrenContainerStyle?: string;
46
54
 
47
55
  // ARIA/アクセシビリティ
48
56
  ariaLabel?: string;
@@ -68,10 +76,18 @@
68
76
  iconVariant = 'outlined',
69
77
 
70
78
  // スタイル/レイアウト
71
- selectedStyle,
79
+ selectedVariant,
72
80
  gap,
73
- childrenVariant = variant === 'mobile' ? 'bottom-sheet' : variant === 'vertical' ? 'accordion' : 'bar',
81
+ childrenVariant = variant === 'mobile'
82
+ ? 'bottom-sheet'
83
+ : variant === 'vertical'
84
+ ? 'accordion'
85
+ : 'bar',
74
86
  chevron,
87
+ customContainerStyle,
88
+ customItemStyle,
89
+ customChildrenItemStyle,
90
+ customChildrenContainerStyle,
75
91
 
76
92
  // ARIA/アクセシビリティ
77
93
  ariaLabel,
@@ -99,15 +115,19 @@
99
115
  });
100
116
  });
101
117
 
102
- // accordion モード: アクティブな子を持つ親を自動展開
118
+ // accordion / bar モード: アクティブな親を自動展開
103
119
  $effect(() => {
104
- if (childrenVariant !== '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
- )
120
+ if ((childrenVariant !== 'accordion' && childrenVariant !== 'bar') || !resolvedCurrentPath)
121
+ return;
122
+ const selected = navItems[selectedIndex];
123
+ const activeParent = navItems.find(
124
+ (item) =>
125
+ (item === selected && !!item.children?.length) ||
126
+ item.children?.some(
127
+ (child) =>
128
+ child.href &&
129
+ matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher)
130
+ )
111
131
  );
112
132
  if (activeParent) expandedParent = activeParent;
113
133
  });
@@ -128,7 +148,7 @@
128
148
  const selector = includeChildren ? '[data-nav-item], [data-nav-item-child]' : '[data-nav-item]';
129
149
  const navItemEls = Array.from(
130
150
  (event.currentTarget as HTMLElement).querySelectorAll<HTMLElement>(selector)
131
- ).filter(el => el.tabIndex !== -1);
151
+ ).filter((el) => el.tabIndex !== -1);
132
152
 
133
153
  if (navItemEls.length === 0) return;
134
154
 
@@ -227,13 +247,21 @@
227
247
  const selectedIndex = $derived.by(() => {
228
248
  for (let i = 0; i < navItems.length; i++) {
229
249
  const item = navItems[i];
230
- if (item.href && matchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)) {
250
+ if (
251
+ item.href &&
252
+ matchPath(resolvedCurrentPath, item.href, item, pathPrefix, customPathMatcher)
253
+ ) {
231
254
  return i;
232
255
  }
233
256
  // bar モード: 子が選択されていれば親も選択とみなす
234
- if (childrenVariant === 'bar' && item.children?.some(
235
- (child) => child.href && matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher)
236
- )) {
257
+ if (
258
+ childrenVariant === 'bar' &&
259
+ item.children?.some(
260
+ (child) =>
261
+ child.href &&
262
+ matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher)
263
+ )
264
+ ) {
237
265
  return i;
238
266
  }
239
267
  }
@@ -245,7 +273,8 @@
245
273
  );
246
274
 
247
275
  const isChildSelected = (child: MenuItem) =>
248
- !!child.href && matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher);
276
+ !!child.href &&
277
+ matchPath(resolvedCurrentPath, child.href, child, pathPrefix, customPathMatcher);
249
278
  </script>
250
279
 
251
280
  <nav
@@ -253,39 +282,53 @@
253
282
  aria-label={ariaLabelledby ? undefined : ariaLabel}
254
283
  aria-labelledby={ariaLabelledby}
255
284
  style:--internal-nav-gap={gap != null ? (typeof gap === 'number' ? `${gap}px` : gap) : undefined}
285
+ style={customContainerStyle}
256
286
  {id}
257
287
  data-testid="nav"
258
288
  bind:this={navEl}
259
289
  >
260
290
  <div style="display: contents" role="presentation" onkeydown={handleKeyDown}>
261
- {#each navItems as item, index}
262
- <NavItem
263
- {item}
264
- {variant}
265
- {pathPrefix}
266
- isSelected={index === selectedIndex}
267
- isDisabled={item.disabled ?? false}
268
- {iconFilled}
269
- {iconWeight}
270
- {iconGrade}
271
- {iconOpticalSize}
272
- {iconVariant}
273
- {selectedStyle}
274
- {childrenVariant}
275
- {chevron}
276
- {resolvedCurrentPath}
277
- {customPathMatcher}
278
- isChildrenVisible={(childrenVariant === 'bar' || childrenVariant === 'accordion') && expandedParent === item}
279
- onChildrenToggle={handleToggle}
280
- onChildrenClose={() => { expandedParent = null; }}
281
- />
282
- {/each}
291
+ {#each navItems as item, index}
292
+ <NavItem
293
+ {item}
294
+ {variant}
295
+ {pathPrefix}
296
+ isSelected={index === selectedIndex}
297
+ isDisabled={item.disabled ?? false}
298
+ {iconFilled}
299
+ {iconWeight}
300
+ {iconGrade}
301
+ {iconOpticalSize}
302
+ {iconVariant}
303
+ {selectedVariant}
304
+ {childrenVariant}
305
+ {chevron}
306
+ customStyle={customItemStyle}
307
+ customChildrenStyle={customChildrenItemStyle}
308
+ {customChildrenContainerStyle}
309
+ {resolvedCurrentPath}
310
+ {customPathMatcher}
311
+ isChildrenVisible={(childrenVariant === 'bar' || childrenVariant === 'accordion') &&
312
+ expandedParent === item}
313
+ onChildrenToggle={handleToggle}
314
+ onChildrenClose={() => {
315
+ expandedParent = null;
316
+ }}
317
+ />
318
+ {/each}
283
319
  </div>
284
320
  </nav>
285
321
 
286
322
  <!-- bar モード: 選択中の親の子アイテムを横バーとして表示 -->
287
323
  {#if showSubBar && expandedParent?.children}
288
- <div class="nav__sub-bar" role="menu" tabindex="-1" onkeydown={handleSubBarKeyDown} bind:this={subBarEl}>
324
+ <div
325
+ class="nav__children-bar"
326
+ role="menu"
327
+ tabindex="-1"
328
+ style={customChildrenContainerStyle}
329
+ onkeydown={handleSubBarKeyDown}
330
+ bind:this={subBarEl}
331
+ >
289
332
  {#each expandedParent.children as child}
290
333
  <NavItem
291
334
  item={child}
@@ -296,7 +339,8 @@
296
339
  {iconGrade}
297
340
  {iconOpticalSize}
298
341
  {iconVariant}
299
- {selectedStyle}
342
+ {selectedVariant}
343
+ customStyle={customChildrenItemStyle}
300
344
  isChild={true}
301
345
  {resolvedCurrentPath}
302
346
  {customPathMatcher}
@@ -340,10 +384,10 @@
340
384
  display: none;
341
385
  }
342
386
 
343
- .nav__sub-bar {
387
+ .nav__children-bar {
344
388
  display: flex;
345
389
  flex-direction: row;
346
390
  gap: var(--internal-nav-gap, var(--svelte-ui-nav-horizontal-item-gap));
347
391
  align-items: center;
348
- min-height: var(--svelte-ui-nav-sub-bar-min-height);
392
+ padding-top: var(--svelte-ui-nav-children-offset);
349
393
  }</style>
@@ -1,4 +1,4 @@
1
- import type { NavItemSelectedStyle } from './NavItem.svelte';
1
+ import type { NavItemSelectedVariant } from './NavItem.svelte';
2
2
  import type { MenuItem } from '../types/menuItem';
3
3
  import type { NavVariant, ChildrenVariant } from '../types/propOptions';
4
4
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
@@ -20,12 +20,20 @@ export type NavProps = {
20
20
  iconOpticalSize?: IconOpticalSize;
21
21
  iconVariant?: IconVariant;
22
22
  /** Visual style for the selected item. */
23
- selectedStyle?: NavItemSelectedStyle;
23
+ selectedVariant?: NavItemSelectedVariant;
24
24
  gap?: number | string;
25
25
  /** How child items are displayed. Defaults to `accordion` (vertical), `bar` (horizontal), `bottom-sheet` (mobile). */
26
26
  childrenVariant?: ChildrenVariant;
27
27
  /** Show chevron icon on parent items. @default true */
28
28
  chevron?: boolean;
29
+ /** Inline style applied to the nav container element. */
30
+ customContainerStyle?: string;
31
+ /** Inline style applied to each nav item element. */
32
+ customItemStyle?: string;
33
+ /** Inline style applied to each child nav item element. */
34
+ customChildrenItemStyle?: string;
35
+ /** Inline style applied to the children container (accordion/expanded list, bar row). */
36
+ customChildrenContainerStyle?: string;
29
37
  ariaLabel?: string;
30
38
  ariaLabelledby?: string;
31
39
  };
@@ -15,7 +15,7 @@
15
15
  // =========================================================================
16
16
  // Props, States & Constants
17
17
  // =========================================================================
18
- export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal' | 'underline';
18
+ export type NavItemSelectedVariant = 'color' | 'filled' | 'tonal' | 'underline';
19
19
 
20
20
  export type NavItemProps = {
21
21
  // 基本プロパティ
@@ -37,9 +37,15 @@
37
37
  // スタイル/レイアウト
38
38
  /** Show chevron icon on parent items. @default true */
39
39
  chevron?: boolean;
40
- selectedStyle?: NavItemSelectedStyle;
40
+ selectedVariant?: NavItemSelectedVariant;
41
41
  /** How child items are displayed. Defaults to `accordion` (vertical), `bar` (horizontal), `bottom-sheet` (mobile). */
42
42
  childrenVariant?: ChildrenVariant;
43
+ /** Inline style applied to this nav item element. */
44
+ customStyle?: string;
45
+ /** Inline style applied to each child nav item element. */
46
+ customChildrenStyle?: string;
47
+ /** Inline style applied to the children container (accordion/expanded list). */
48
+ customChildrenContainerStyle?: string;
43
49
 
44
50
  // 状態/動作
45
51
  isSelected?: boolean;
@@ -73,8 +79,11 @@
73
79
 
74
80
  // スタイル/レイアウト
75
81
  chevron = true,
76
- selectedStyle,
82
+ selectedVariant,
77
83
  childrenVariant = variant === 'mobile' ? 'bottom-sheet' : variant === 'vertical' ? 'accordion' : 'bar',
84
+ customStyle,
85
+ customChildrenStyle,
86
+ customChildrenContainerStyle,
78
87
 
79
88
  // 状態/動作
80
89
  isSelected = false,
@@ -99,7 +108,7 @@
99
108
  const hrefWithPrefix = $derived(item.href ? withPrefix(item.href) : undefined);
100
109
 
101
110
  const resolvedSelectedStyle = $derived(
102
- selectedStyle ?? (variant === 'vertical' || variant === 'horizontal' ? 'tonal' : 'color')
111
+ selectedVariant ?? (variant === 'vertical' || variant === 'horizontal' ? 'tonal' : 'color')
103
112
  );
104
113
 
105
114
  // isChild=true のアイテムでは children を展開しない(無限再帰防止)
@@ -232,6 +241,7 @@
232
241
  class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
233
242
  class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
234
243
  class:nav-item--style-underline={resolvedSelectedStyle === 'underline'}
244
+ style={customStyle}
235
245
  aria-disabled="true"
236
246
  tabindex="-1"
237
247
  data-nav-item={!isChild ? '' : undefined}
@@ -270,6 +280,7 @@
270
280
  class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
271
281
  class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
272
282
  class:nav-item--style-underline={resolvedSelectedStyle === 'underline'}
283
+ style={customStyle}
273
284
  aria-current={isSelected ? 'page' : undefined}
274
285
  aria-expanded={childrenVariant === 'expanded' || isChildrenVisible}
275
286
  tabindex={0}
@@ -326,7 +337,7 @@
326
337
 
327
338
  <!-- accordion / expanded サブメニュー -->
328
339
  {#if childrenVariant === 'expanded' || (childrenVariant === 'accordion' && isChildrenVisible)}
329
- <div class="nav-item__children" role="presentation" transition:slide={{ duration: 200 }}>
340
+ <div class="nav-item__children" role="presentation" style={customChildrenContainerStyle} transition:slide={{ duration: 200 }}>
330
341
  {#each item.children! as child}
331
342
  <NavItem
332
343
  item={child}
@@ -337,7 +348,8 @@
337
348
  {iconGrade}
338
349
  {iconOpticalSize}
339
350
  {iconVariant}
340
- {selectedStyle}
351
+ {selectedVariant}
352
+ customStyle={customChildrenStyle}
341
353
  isChild={true}
342
354
  {resolvedCurrentPath}
343
355
  {customPathMatcher}
@@ -374,7 +386,8 @@
374
386
  {iconGrade}
375
387
  {iconOpticalSize}
376
388
  {iconVariant}
377
- {selectedStyle}
389
+ {selectedVariant}
390
+ customStyle={customChildrenStyle}
378
391
  isChild={true}
379
392
  {resolvedCurrentPath}
380
393
  {customPathMatcher}
@@ -399,6 +412,7 @@
399
412
  class:nav-item--style-filled={isSelected && resolvedSelectedStyle === 'filled'}
400
413
  class:nav-item--style-tonal={isSelected && resolvedSelectedStyle === 'tonal'}
401
414
  class:nav-item--style-underline={resolvedSelectedStyle === 'underline'}
415
+ style={customStyle}
402
416
  aria-current={isSelected ? 'page' : undefined}
403
417
  tabindex={0}
404
418
  data-nav-item={!isChild ? '' : undefined}
@@ -609,7 +623,7 @@
609
623
  .nav-item__children {
610
624
  display: flex;
611
625
  flex-direction: column;
612
- padding-top: var(--internal-nav-gap, var(--svelte-ui-nav-vertical-item-gap));
626
+ padding-top: var(--svelte-ui-nav-children-offset);
613
627
  gap: var(--internal-nav-gap, var(--svelte-ui-nav-vertical-item-gap));
614
628
  overflow: hidden;
615
629
  }
@@ -2,7 +2,7 @@ import NavItem from './NavItem.svelte';
2
2
  import type { MenuItem } from '../types/menuItem';
3
3
  import type { NavVariant, ChildrenVariant } from '../types/propOptions';
4
4
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
5
- export type NavItemSelectedStyle = 'color' | 'filled' | 'tonal' | 'underline';
5
+ export type NavItemSelectedVariant = 'color' | 'filled' | 'tonal' | 'underline';
6
6
  export type NavItemProps = {
7
7
  item: MenuItem;
8
8
  variant?: NavVariant;
@@ -18,9 +18,15 @@ export type NavItemProps = {
18
18
  iconVariant?: IconVariant;
19
19
  /** Show chevron icon on parent items. @default true */
20
20
  chevron?: boolean;
21
- selectedStyle?: NavItemSelectedStyle;
21
+ selectedVariant?: NavItemSelectedVariant;
22
22
  /** How child items are displayed. Defaults to `accordion` (vertical), `bar` (horizontal), `bottom-sheet` (mobile). */
23
23
  childrenVariant?: ChildrenVariant;
24
+ /** Inline style applied to this nav item element. */
25
+ customStyle?: string;
26
+ /** Inline style applied to each child nav item element. */
27
+ customChildrenStyle?: string;
28
+ /** Inline style applied to the children container (accordion/expanded list). */
29
+ customChildrenContainerStyle?: string;
24
30
  isSelected?: boolean;
25
31
  isDisabled?: boolean;
26
32
  /** When true, this item does not render its own children (prevents infinite recursion). */
@@ -36,6 +36,14 @@
36
36
  selectedTextColor?: string;
37
37
  /** Custom CSS color for the active tab indicator bar. */
38
38
  selectedBarColor?: string;
39
+ /** Inline style applied to the tab container element. */
40
+ customContainerStyle?: string;
41
+ /** Inline style applied to each tab item element. */
42
+ customItemStyle?: string;
43
+ /** Inline style applied to the children container. */
44
+ customChildrenContainerStyle?: string;
45
+ /** Inline style applied to each child item element. */
46
+ customChildrenItemStyle?: string;
39
47
 
40
48
  // ARIA/アクセシビリティ
41
49
  ariaLabel?: string;
@@ -63,6 +71,10 @@
63
71
  textColor,
64
72
  selectedTextColor,
65
73
  selectedBarColor,
74
+ customContainerStyle,
75
+ customItemStyle,
76
+ customChildrenContainerStyle,
77
+ customChildrenItemStyle,
66
78
 
67
79
  // ARIA/アクセシビリティ
68
80
  ariaLabel = 'Tabs',
@@ -80,7 +92,7 @@
80
92
  <Nav
81
93
  navItems={tabItems}
82
94
  variant="horizontal"
83
- selectedStyle="underline"
95
+ selectedVariant="underline"
84
96
  {pathPrefix}
85
97
  {customPathMatcher}
86
98
  {currentPath}
@@ -90,6 +102,10 @@
90
102
  {iconGrade}
91
103
  {iconOpticalSize}
92
104
  {iconVariant}
105
+ {customContainerStyle}
106
+ {customItemStyle}
107
+ {customChildrenContainerStyle}
108
+ {customChildrenItemStyle}
93
109
  {ariaLabel}
94
110
  {ariaLabelledby}
95
111
  />
@@ -21,6 +21,14 @@ export type TabProps = {
21
21
  selectedTextColor?: string;
22
22
  /** Custom CSS color for the active tab indicator bar. */
23
23
  selectedBarColor?: string;
24
+ /** Inline style applied to the tab container element. */
25
+ customContainerStyle?: string;
26
+ /** Inline style applied to each tab item element. */
27
+ customItemStyle?: string;
28
+ /** Inline style applied to the children container. */
29
+ customChildrenContainerStyle?: string;
30
+ /** Inline style applied to each child item element. */
31
+ customChildrenItemStyle?: string;
24
32
  ariaLabel?: string;
25
33
  ariaLabelledby?: string;
26
34
  };
package/dist/index.d.ts CHANGED
@@ -75,7 +75,7 @@ export type { SkeletonHeadingProps } from './components/skeleton/SkeletonHeading
75
75
  export type { SkeletonMediaProps } from './components/skeleton/SkeletonMedia.svelte';
76
76
  export type { SwitchProps } from './components/Switch.svelte';
77
77
  export type { NavProps } from './components/Nav.svelte';
78
- export type { NavItemProps, NavItemSelectedStyle } from './components/NavItem.svelte';
78
+ export type { NavItemProps, NavItemSelectedVariant } from './components/NavItem.svelte';
79
79
  export type { TabProps } from './components/Tab.svelte';
80
80
  export type { TextareaProps } from './components/Textarea.svelte';
81
81
  export type { PopupPosition, SnackbarPosition, FabPosition, ButtonVariant, ButtonSize, SnackbarType, SnackbarVariant, BadgeVariant, DatepickerMode, FocusStyle, NavVariant, ChildrenVariant } from './types/propOptions';
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.31",
5
+ "version": "0.0.33",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",