@14ch/svelte-ui 0.0.5 → 0.0.7

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.
@@ -341,6 +341,25 @@
341
341
  --svelte-ui-iconbutton-hover-overlay: var(--svelte-ui-hover-overlay);
342
342
  --svelte-ui-iconbutton-focus-color: var(--svelte-ui-focus-color);
343
343
 
344
+ /* SegmentedControl */
345
+ --svelte-ui-segmented-control-base-padding: 4px;
346
+ --svelte-ui-segmented-control-base-border-radius: var(--svelte-ui-border-radius);
347
+ --svelte-ui-segmented-control-gap: 4px;
348
+ --svelte-ui-segmented-control-button-radius: calc(var(--svelte-ui-border-radius) - 1px);
349
+ --svelte-ui-segmented-control-button-height-sm: 24px;
350
+ --svelte-ui-segmented-control-button-height: 32px;
351
+ --svelte-ui-segmented-control-button-height-lg: 40px;
352
+ --svelte-ui-segmented-control-font-size-sm: 12px;
353
+ --svelte-ui-segmented-control-font-size: 14px;
354
+ --svelte-ui-segmented-control-font-size-lg: 16px;
355
+ --svelte-ui-segmented-control-button-padding-sm: 4px 8px;
356
+ --svelte-ui-segmented-control-button-padding: 6px 12px;
357
+ --svelte-ui-segmented-control-button-padding-lg: 8px 16px;
358
+ --svelte-ui-segmented-control-base-bg: var(--svelte-ui-surface-accent-color);
359
+ --svelte-ui-segmented-control-selected-bg: var(--svelte-ui-primary-color);
360
+ --svelte-ui-segmented-control-selected-text-color: var(--svelte-ui-text-on-filled-color);
361
+ --svelte-ui-segmented-control-hover-overlay: var(--svelte-ui-hover-overlay);
362
+
344
363
  /* Input */
345
364
  --svelte-ui-input-height: var(--svelte-ui-form-height);
346
365
  --svelte-ui-input-padding: var(--svelte-ui-form-padding);
@@ -490,7 +509,7 @@
490
509
  /* ColorPicker */
491
510
  --svelte-ui-colorpicker-trigger-size: 28px;
492
511
  --svelte-ui-colorpicker-text-padding-left: 40px;
493
- --svelte-ui-colorpicker-trigger-border-radius: calc(var(--svelte-ui-border-radius) - 2px);
512
+ --svelte-ui-colorpicker-trigger-border-radius: calc(var(--svelte-ui-border-radius) - 1px);
494
513
  --svelte-ui-colorpicker-trigger-border-style: dashed;
495
514
  --svelte-ui-colorpicker-trigger-border-width: 1px;
496
515
  --svelte-ui-colorpicker-trigger-offset: 4px;
@@ -494,7 +494,7 @@
494
494
  }
495
495
 
496
496
  .button__icon {
497
- margin-left: -4px;
497
+ margin: -12px 0 -12px -4px;
498
498
  display: flex;
499
499
  align-items: center;
500
500
  justify-content: center;
@@ -72,7 +72,7 @@
72
72
  ...restProps
73
73
  }: {
74
74
  // 基本プロパティ
75
- value?: string;
75
+ value: string;
76
76
 
77
77
  // HTML属性系
78
78
  id?: string;
@@ -167,10 +167,6 @@
167
167
  // Methods
168
168
  // =========================================================================
169
169
 
170
- const handleSubmit = (event: SubmitEvent) => {
171
- event?.preventDefault?.();
172
- handleChange();
173
- };
174
170
 
175
171
  const handleChange = (event?: Event): void => {
176
172
  // 空文字列の場合はそのまま処理
@@ -207,6 +203,11 @@
207
203
  onclick?.(event);
208
204
  };
209
205
  const handleKeydown = (event: KeyboardEvent) => {
206
+ // Enterキーで色の変更を確定
207
+ if (event.key === 'Enter' && !disabled && !readonly) {
208
+ event.preventDefault();
209
+ handleChange();
210
+ }
210
211
  if (disabled) return;
211
212
  onkeydown(event);
212
213
  };
@@ -361,7 +362,6 @@
361
362
  onpointerleave={handlePointerLeave}
362
363
  onpointermove={handlePointerMove}
363
364
  onpointercancel={handlePointerCancel}
364
- onsubmit={handleSubmit}
365
365
  {...restProps}
366
366
  />
367
367
 
@@ -1,7 +1,7 @@
1
1
  import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  import type { IconVariant } from '../types/icon';
3
3
  type $$ComponentProps = {
4
- value?: string;
4
+ value: string;
5
5
  id?: string;
6
6
  inputAttributes?: HTMLInputAttributes | undefined;
7
7
  customStyle?: string;
@@ -61,7 +61,7 @@
61
61
  onpointerleave = () => {} // No params for type inference
62
62
  }: {
63
63
  // 基本プロパティ
64
- value: FileList | undefined;
64
+ value: FileList | null | undefined;
65
65
  multiple?: boolean;
66
66
  maxFileSize?: number;
67
67
  placeholder?: string;
@@ -86,7 +86,7 @@
86
86
  removeFileAriaLabel?: string;
87
87
 
88
88
  // 入力イベント
89
- onchange?: (value: FileList | null) => void;
89
+ onchange?: (value: FileList | null | undefined) => void;
90
90
 
91
91
  // フォーカスイベント
92
92
  onfocus?: Function; // No params for type inference
@@ -187,7 +187,7 @@
187
187
  dt.items.add(value[i]);
188
188
  }
189
189
  }
190
- value = dt.files.length > 0 ? dt.files : undefined;
190
+ value = dt.files.length > 0 ? dt.files : null;
191
191
  };
192
192
 
193
193
  const validateFile = (file: File): boolean => {
@@ -240,7 +240,7 @@
240
240
  export const reset = () => {
241
241
  if (fileInputRef) {
242
242
  fileInputRef.value = '';
243
- value = undefined;
243
+ value = null;
244
244
  errorMessage = '';
245
245
  }
246
246
  };
@@ -1,6 +1,6 @@
1
1
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
2
2
  type $$ComponentProps = {
3
- value: FileList | undefined;
3
+ value: FileList | null | undefined;
4
4
  multiple?: boolean;
5
5
  maxFileSize?: number;
6
6
  placeholder?: string;
@@ -17,7 +17,7 @@ type $$ComponentProps = {
17
17
  iconOpticalSize?: IconOpticalSize;
18
18
  iconVariant?: IconVariant;
19
19
  removeFileAriaLabel?: string;
20
- onchange?: (value: FileList | null) => void;
20
+ onchange?: (value: FileList | null | undefined) => void;
21
21
  onfocus?: Function;
22
22
  onblur?: Function;
23
23
  onkeydown?: Function;
@@ -66,7 +66,7 @@
66
66
  onpointerleave = () => {} // No params for type inference
67
67
  }: {
68
68
  // 基本プロパティ
69
- value: FileList | undefined;
69
+ value: FileList | null | undefined;
70
70
  multiple?: boolean;
71
71
  maxFileSize?: number;
72
72
  placeholder?: string;
@@ -93,7 +93,7 @@
93
93
  removeFileAriaLabel?: string;
94
94
 
95
95
  // 入力イベント
96
- onchange?: (value: FileList | null) => void;
96
+ onchange?: (value: FileList | null | undefined) => void;
97
97
 
98
98
  // フォーカスイベント
99
99
  onfocus?: Function; // No params for type inference
@@ -295,7 +295,7 @@
295
295
  export const reset = () => {
296
296
  if (fileInputRef) {
297
297
  fileInputRef.value = '';
298
- value = undefined;
298
+ value = null;
299
299
  errorMessage = '';
300
300
  cleanupObjectUrls();
301
301
  }
@@ -1,6 +1,6 @@
1
1
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
2
2
  type $$ComponentProps = {
3
- value: FileList | undefined;
3
+ value: FileList | null | undefined;
4
4
  multiple?: boolean;
5
5
  maxFileSize?: number;
6
6
  placeholder?: string;
@@ -19,7 +19,7 @@ type $$ComponentProps = {
19
19
  iconOpticalSize?: IconOpticalSize;
20
20
  iconVariant?: IconVariant;
21
21
  removeFileAriaLabel?: string;
22
- onchange?: (value: FileList | null) => void;
22
+ onchange?: (value: FileList | null | undefined) => void;
23
23
  onfocus?: Function;
24
24
  onblur?: Function;
25
25
  onkeydown?: Function;
@@ -28,6 +28,7 @@
28
28
  step = null,
29
29
  size = null,
30
30
  autocomplete = null,
31
+ spellcheck = null,
31
32
  inputAttributes,
32
33
 
33
34
  // スタイル/レイアウト
@@ -94,11 +95,6 @@
94
95
  // 入力イベント
95
96
  onchange = () => {}, // No params for type inference
96
97
  oninput = () => {}, // No params for type inference
97
- onsubmit = () => {}, // No params for type inference
98
-
99
- // IMEイベント
100
- oncompositionstart = () => {}, // No params for type inference
101
- oncompositionend = () => {}, // No params for type inference
102
98
 
103
99
  // アイコンイベント
104
100
  onRightIconClick,
@@ -109,7 +105,7 @@
109
105
  }: {
110
106
  // 基本プロパティ
111
107
  name?: string;
112
- value: string | number | undefined;
108
+ value: string | number;
113
109
 
114
110
  // HTML属性系
115
111
  id?: string | null;
@@ -122,6 +118,7 @@
122
118
  step?: number | null;
123
119
  size?: number | null;
124
120
  autocomplete?: HTMLInputElement['autocomplete'] | null;
121
+ spellcheck?: boolean | null;
125
122
  inputAttributes?: HTMLInputAttributes | undefined;
126
123
 
127
124
  // スタイル/レイアウト
@@ -188,11 +185,6 @@
188
185
  // 入力イベント
189
186
  onchange?: (value: any) => void;
190
187
  oninput?: (value: any) => void;
191
- onsubmit?: (value: any) => void;
192
-
193
- // IMEイベント
194
- oncompositionstart?: Function; // No params for type inference
195
- oncompositionend?: Function; // No params for type inference
196
188
 
197
189
  // アイコンイベント
198
190
  onRightIconClick?: Function; // No params for type inference
@@ -204,6 +196,7 @@
204
196
 
205
197
  let ref: HTMLInputElement | undefined = $state();
206
198
  let isFocused: boolean = $state(false);
199
+ let isComposing: boolean = $state(false);
207
200
 
208
201
  // =========================================================================
209
202
  // Methods
@@ -212,8 +205,8 @@
212
205
  const clear = (): void => {
213
206
  if (disabled || readonly) return;
214
207
  ref?.focus();
215
- onchange?.(undefined);
216
- value = undefined;
208
+ value = '';
209
+ onchange?.('');
217
210
  };
218
211
 
219
212
  export const focus = () => {
@@ -241,6 +234,10 @@
241
234
 
242
235
  // キーボードイベント
243
236
  const handleKeydown = (event: KeyboardEvent) => {
237
+ // Enterキーで入力確定(blur)する(IME変換中は除く)
238
+ if (event.key === 'Enter' && !disabled && !readonly && !isComposing) {
239
+ ref?.blur();
240
+ }
244
241
  onkeydown?.(event);
245
242
  };
246
243
 
@@ -249,12 +246,6 @@
249
246
  };
250
247
 
251
248
  // 入力イベント
252
- const handleSubmit = (event: SubmitEvent) => {
253
- if (disabled || readonly) return;
254
- event?.preventDefault?.();
255
- ref?.blur();
256
- onsubmit?.(value);
257
- };
258
249
 
259
250
  const handleChange = () => {
260
251
  if (disabled || readonly) return;
@@ -364,11 +355,19 @@
364
355
  onpointercancel?.(event);
365
356
  };
366
357
 
358
+ // IMEイベント
359
+ const handleCompositionStart = () => {
360
+ isComposing = true;
361
+ };
362
+
363
+ const handleCompositionEnd = () => {
364
+ isComposing = false;
365
+ };
366
+
367
367
  // =========================================================================
368
368
  // $derived
369
369
  // =========================================================================
370
370
  const getDisplayValue = (): string => {
371
- if (value === undefined) return '';
372
371
  if (type === 'number' && typeof value === 'number') {
373
372
  return value.toLocaleString();
374
373
  }
@@ -404,8 +403,8 @@
404
403
  {getDisplayValue()}
405
404
  </div>
406
405
  {/if}
407
- <!-- 入力用フォーム -->
408
- <form onsubmit={handleSubmit}>
406
+ <!-- 入力用要素 -->
407
+ <div class="input__wrapper">
409
408
  <input
410
409
  {id}
411
410
  {name}
@@ -425,6 +424,7 @@
425
424
  {required}
426
425
  {tabindex}
427
426
  {size}
427
+ {spellcheck}
428
428
  onchange={handleChange}
429
429
  oninput={handleInput}
430
430
  onfocus={handleFocus}
@@ -450,10 +450,12 @@
450
450
  onpointerleave={handlePointerLeave}
451
451
  onpointermove={handlePointerMove}
452
452
  onpointercancel={handlePointerCancel}
453
+ oncompositionstart={handleCompositionStart}
454
+ oncompositionend={handleCompositionEnd}
453
455
  {...inputAttributes}
454
456
  {...restProps}
455
457
  />
456
- </form>
458
+ </div>
457
459
  <!-- クリアボタン -->
458
460
  {#if clearable && !disabled && !readonly}
459
461
  <div class="input__clear-button">
@@ -544,7 +546,7 @@
544
546
  height: inherit;
545
547
  }
546
548
 
547
- form {
549
+ .input__wrapper {
548
550
  padding: inherit;
549
551
  border: none;
550
552
  font-size: inherit;
@@ -686,7 +688,7 @@
686
688
  * デザインバリアント:default
687
689
  * ============================================= */
688
690
  .input:not(.input--inline) {
689
- form {
691
+ .input__wrapper {
690
692
  position: static;
691
693
  opacity: 1;
692
694
  }
@@ -747,7 +749,7 @@
747
749
  text-align: right;
748
750
  }
749
751
 
750
- form {
752
+ .input__wrapper {
751
753
  position: absolute;
752
754
  top: 0;
753
755
  left: 0;
@@ -788,7 +790,7 @@
788
790
  opacity: 0;
789
791
  }
790
792
 
791
- form {
793
+ .input__wrapper {
792
794
  opacity: 1;
793
795
  }
794
796
  }
@@ -2,7 +2,7 @@ import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
3
  type $$ComponentProps = {
4
4
  name?: string;
5
- value: string | number | undefined;
5
+ value: string | number;
6
6
  id?: string | null;
7
7
  type?: 'text' | 'password' | 'email' | 'tel' | 'url' | 'number';
8
8
  tabindex?: number | null;
@@ -13,6 +13,7 @@ type $$ComponentProps = {
13
13
  step?: number | null;
14
14
  size?: number | null;
15
15
  autocomplete?: HTMLInputElement['autocomplete'] | null;
16
+ spellcheck?: boolean | null;
16
17
  inputAttributes?: HTMLInputAttributes | undefined;
17
18
  inline?: boolean;
18
19
  focusStyle?: 'background' | 'outline' | 'none';
@@ -61,9 +62,6 @@ type $$ComponentProps = {
61
62
  onpointercancel?: Function;
62
63
  onchange?: (value: any) => void;
63
64
  oninput?: (value: any) => void;
64
- onsubmit?: (value: any) => void;
65
- oncompositionstart?: Function;
66
- oncompositionend?: Function;
67
65
  onRightIconClick?: Function;
68
66
  onLeftIconClick?: Function;
69
67
  [key: string]: any;
@@ -0,0 +1,622 @@
1
+ <!-- SegmentedControl.svelte -->
2
+
3
+ <script lang="ts">
4
+ import Icon from './Icon.svelte';
5
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
6
+ import type { SegmentedControlItem } from '../types/segmentedControlItem';
7
+
8
+ // =========================================================================
9
+ // Props, States & Constants
10
+ // =========================================================================
11
+ let {
12
+ // 基本プロパティ
13
+ items = [],
14
+ value = $bindable(''),
15
+
16
+ // HTML属性系
17
+ id,
18
+ name = `segmented-control-${Math.random().toString(36).substring(2, 15)}`,
19
+
20
+ // スタイル/レイアウト
21
+ size = 'medium',
22
+ fullWidth = false,
23
+ color,
24
+ rounded = false,
25
+
26
+ // アイコン関連
27
+ iconFilled = false,
28
+ iconWeight = 300,
29
+ iconGrade = 0,
30
+ iconOpticalSize,
31
+ iconVariant = 'outlined',
32
+
33
+ // 状態/動作
34
+ disabled = false,
35
+
36
+ // ARIA/アクセシビリティ
37
+ ariaLabel,
38
+ ariaLabelledby,
39
+ reducedMotion = false,
40
+
41
+ // 入力イベント
42
+ onchange = () => {}, // No params for type inference
43
+
44
+ // フォーカスイベント
45
+ onfocus = () => {}, // No params for type inference
46
+ onblur = () => {}, // No params for type inference
47
+
48
+ // キーボードイベント
49
+ onkeydown = () => {}, // No params for type inference
50
+ onkeyup = () => {}, // No params for type inference
51
+
52
+ // マウスイベント
53
+ onclick = () => {}, // No params for type inference
54
+ onmousedown = () => {}, // No params for type inference
55
+ onmouseup = () => {}, // No params for type inference
56
+ onmouseenter = () => {}, // No params for type inference
57
+ onmouseleave = () => {}, // No params for type inference
58
+ onmouseover = () => {}, // No params for type inference
59
+ onmouseout = () => {}, // No params for type inference
60
+ oncontextmenu = () => {}, // No params for type inference
61
+ onauxclick = () => {}, // No params for type inference
62
+
63
+ // タッチイベント
64
+ ontouchstart = () => {}, // No params for type inference
65
+ ontouchend = () => {}, // No params for type inference
66
+ ontouchmove = () => {}, // No params for type inference
67
+ ontouchcancel = () => {}, // No params for type inference
68
+
69
+ // ポインターイベント
70
+ onpointerdown = () => {}, // No params for type inference
71
+ onpointerup = () => {}, // No params for type inference
72
+ onpointerenter = () => {}, // No params for type inference
73
+ onpointerleave = () => {}, // No params for type inference
74
+ onpointermove = () => {}, // No params for type inference
75
+ onpointercancel = () => {}, // No params for type inference
76
+
77
+ // その他
78
+ ...restProps
79
+ }: {
80
+ // 基本プロパティ
81
+ items: SegmentedControlItem[];
82
+ value: string;
83
+
84
+ // HTML属性系
85
+ id?: string;
86
+ name?: string;
87
+
88
+ // スタイル/レイアウト
89
+ size?: 'small' | 'medium' | 'large';
90
+ fullWidth?: boolean;
91
+ color?: string;
92
+ rounded?: boolean;
93
+
94
+ // アイコン関連
95
+ iconFilled?: boolean;
96
+ iconWeight?: IconWeight;
97
+ iconGrade?: IconGrade;
98
+ iconOpticalSize?: IconOpticalSize;
99
+ iconVariant?: IconVariant;
100
+
101
+ // 状態/動作
102
+ disabled?: boolean;
103
+
104
+ // ARIA/アクセシビリティ
105
+ ariaLabel?: string;
106
+ ariaLabelledby?: string;
107
+ reducedMotion?: boolean;
108
+
109
+ // 入力イベント
110
+ onchange?: (value: string) => void;
111
+
112
+ // フォーカスイベント
113
+ onfocus?: Function; // No params for type inference
114
+ onblur?: Function; // No params for type inference
115
+
116
+ // キーボードイベント
117
+ onkeydown?: Function; // No params for type inference
118
+ onkeyup?: Function; // No params for type inference
119
+
120
+ // マウスイベント
121
+ onclick?: Function; // No params for type inference
122
+ onmousedown?: Function; // No params for type inference
123
+ onmouseup?: Function; // No params for type inference
124
+ onmouseenter?: Function; // No params for type inference
125
+ onmouseleave?: Function; // No params for type inference
126
+ onmouseover?: Function; // No params for type inference
127
+ onmouseout?: Function; // No params for type inference
128
+ oncontextmenu?: Function; // No params for type inference
129
+ onauxclick?: Function; // No params for type inference
130
+
131
+ // タッチイベント
132
+ ontouchstart?: Function; // No params for type inference
133
+ ontouchend?: Function; // No params for type inference
134
+ ontouchmove?: Function; // No params for type inference
135
+ ontouchcancel?: Function; // No params for type inference
136
+
137
+ // ポインターイベント
138
+ onpointerdown?: Function; // No params for type inference
139
+ onpointerup?: Function; // No params for type inference
140
+ onpointerenter?: Function; // No params for type inference
141
+ onpointerleave?: Function; // No params for type inference
142
+ onpointermove?: Function; // No params for type inference
143
+ onpointercancel?: Function; // No params for type inference
144
+
145
+ // その他
146
+ [key: string]: any;
147
+ } = $props();
148
+
149
+ // =========================================================================
150
+ // Methods
151
+ // =========================================================================
152
+ const handleChange = (item: SegmentedControlItem, event: Event) => {
153
+ if (disabled || item.disabled) return;
154
+
155
+ const target = event.target as HTMLInputElement;
156
+ if (target.checked) {
157
+ value = item.value;
158
+ onchange(item.value);
159
+ }
160
+ };
161
+
162
+ const handleFocus = (event: FocusEvent) => {
163
+ onfocus(event);
164
+ };
165
+
166
+ const handleBlur = (event: FocusEvent) => {
167
+ onblur(event);
168
+ };
169
+
170
+ const handleKeydown = (event: KeyboardEvent) => {
171
+ if (disabled || items.length === 0) {
172
+ onkeydown(event);
173
+ return;
174
+ }
175
+
176
+ // Radioコンポーネントと同じキーボードナビゲーション
177
+ if (
178
+ event.key === 'ArrowUp' ||
179
+ event.key === 'ArrowDown' ||
180
+ event.key === 'ArrowLeft' ||
181
+ event.key === 'ArrowRight'
182
+ ) {
183
+ const radioInputs = document.querySelectorAll(
184
+ `input[type="radio"][name="${name}"]`
185
+ ) as NodeListOf<HTMLInputElement>;
186
+ const currentIndex = Array.from(radioInputs).findIndex((input) => input === event.target);
187
+
188
+ if (currentIndex !== -1) {
189
+ event.preventDefault();
190
+ let nextIndex;
191
+
192
+ if (event.key === 'ArrowUp' || event.key === 'ArrowLeft') {
193
+ // 前の有効なアイテムを探す
194
+ for (let i = currentIndex - 1; i >= 0; i--) {
195
+ if (!items[i]?.disabled && !radioInputs[i].disabled) {
196
+ nextIndex = i;
197
+ break;
198
+ }
199
+ }
200
+ // 見つからなければ最後の有効なアイテムへ
201
+ if (nextIndex === undefined) {
202
+ for (let i = items.length - 1; i > currentIndex; i--) {
203
+ if (!items[i]?.disabled && !radioInputs[i].disabled) {
204
+ nextIndex = i;
205
+ break;
206
+ }
207
+ }
208
+ }
209
+ } else {
210
+ // 次の有効なアイテムを探す
211
+ for (let i = currentIndex + 1; i < items.length; i++) {
212
+ if (!items[i]?.disabled && !radioInputs[i].disabled) {
213
+ nextIndex = i;
214
+ break;
215
+ }
216
+ }
217
+ // 見つからなければ最初の有効なアイテムへ
218
+ if (nextIndex === undefined) {
219
+ for (let i = 0; i < currentIndex; i++) {
220
+ if (!items[i]?.disabled && !radioInputs[i].disabled) {
221
+ nextIndex = i;
222
+ break;
223
+ }
224
+ }
225
+ }
226
+ }
227
+
228
+ if (nextIndex !== undefined) {
229
+ const nextInput = radioInputs[nextIndex];
230
+ if (nextInput && !nextInput.disabled) {
231
+ nextInput.focus();
232
+ nextInput.click();
233
+ }
234
+ }
235
+ }
236
+ } else if (event.key === 'Home') {
237
+ event.preventDefault();
238
+ const radioInputs = document.querySelectorAll(
239
+ `input[type="radio"][name="${name}"]`
240
+ ) as NodeListOf<HTMLInputElement>;
241
+ for (let i = 0; i < items.length; i++) {
242
+ if (!items[i]?.disabled && !radioInputs[i].disabled) {
243
+ radioInputs[i].focus();
244
+ radioInputs[i].click();
245
+ break;
246
+ }
247
+ }
248
+ } else if (event.key === 'End') {
249
+ event.preventDefault();
250
+ const radioInputs = document.querySelectorAll(
251
+ `input[type="radio"][name="${name}"]`
252
+ ) as NodeListOf<HTMLInputElement>;
253
+ for (let i = items.length - 1; i >= 0; i--) {
254
+ if (!items[i]?.disabled && !radioInputs[i].disabled) {
255
+ radioInputs[i].focus();
256
+ radioInputs[i].click();
257
+ break;
258
+ }
259
+ }
260
+ }
261
+
262
+ onkeydown(event);
263
+ };
264
+
265
+ const handleKeyup = (event: KeyboardEvent) => {
266
+ onkeyup(event);
267
+ };
268
+
269
+ // マウスイベント
270
+ const handleClick = (event: MouseEvent) => {
271
+ if (disabled) return;
272
+ onclick(event);
273
+ };
274
+
275
+ const handleMouseDown = (event: MouseEvent) => {
276
+ if (disabled) return;
277
+ onmousedown(event);
278
+ };
279
+
280
+ const handleMouseUp = (event: MouseEvent) => {
281
+ if (disabled) return;
282
+ onmouseup(event);
283
+ };
284
+
285
+ const handleMouseEnter = (event: MouseEvent) => {
286
+ if (disabled) return;
287
+ onmouseenter(event);
288
+ };
289
+
290
+ const handleMouseLeave = (event: MouseEvent) => {
291
+ if (disabled) return;
292
+ onmouseleave(event);
293
+ };
294
+
295
+ const handleMouseOver = (event: MouseEvent) => {
296
+ if (disabled) return;
297
+ onmouseover(event);
298
+ };
299
+
300
+ const handleMouseOut = (event: MouseEvent) => {
301
+ if (disabled) return;
302
+ onmouseout(event);
303
+ };
304
+
305
+ const handleContextMenu = (event: MouseEvent) => {
306
+ if (disabled) return;
307
+ oncontextmenu(event);
308
+ };
309
+
310
+ const handleAuxClick = (event: MouseEvent) => {
311
+ if (disabled) return;
312
+ onauxclick(event);
313
+ };
314
+
315
+ // タッチイベント
316
+ const handleTouchStart = (event: TouchEvent) => {
317
+ if (disabled) return;
318
+ ontouchstart(event);
319
+ };
320
+
321
+ const handleTouchEnd = (event: TouchEvent) => {
322
+ if (disabled) return;
323
+ ontouchend(event);
324
+ };
325
+
326
+ const handleTouchMove = (event: TouchEvent) => {
327
+ if (disabled) return;
328
+ ontouchmove(event);
329
+ };
330
+
331
+ const handleTouchCancel = (event: TouchEvent) => {
332
+ if (disabled) return;
333
+ ontouchcancel(event);
334
+ };
335
+
336
+ // ポインターイベント
337
+ const handlePointerDown = (event: PointerEvent) => {
338
+ if (disabled) return;
339
+ onpointerdown(event);
340
+ };
341
+
342
+ const handlePointerUp = (event: PointerEvent) => {
343
+ if (disabled) return;
344
+ onpointerup(event);
345
+ };
346
+
347
+ const handlePointerEnter = (event: PointerEvent) => {
348
+ if (disabled) return;
349
+ onpointerenter(event);
350
+ };
351
+
352
+ const handlePointerLeave = (event: PointerEvent) => {
353
+ if (disabled) return;
354
+ onpointerleave(event);
355
+ };
356
+
357
+ const handlePointerMove = (event: PointerEvent) => {
358
+ if (disabled) return;
359
+ onpointermove(event);
360
+ };
361
+
362
+ const handlePointerCancel = (event: PointerEvent) => {
363
+ if (disabled) return;
364
+ onpointercancel(event);
365
+ };
366
+
367
+ // =========================================================================
368
+ // $derived
369
+ // =========================================================================
370
+ const effectiveIconSize = $derived(
371
+ iconOpticalSize || (size === 'small' ? 16 : size === 'large' ? 24 : 20)
372
+ );
373
+
374
+ const containerClasses = $derived(
375
+ [
376
+ 'segmented-control',
377
+ `segmented-control--${size}`,
378
+ fullWidth && 'segmented-control--full-width',
379
+ rounded && 'segmented-control--rounded',
380
+ disabled && 'segmented-control--disabled',
381
+ reducedMotion && 'segmented-control--no-motion'
382
+ ]
383
+ .filter(Boolean)
384
+ .join(' ')
385
+ );
386
+ </script>
387
+
388
+ <div
389
+ class={containerClasses}
390
+ role="radiogroup"
391
+ aria-label={ariaLabelledby ? undefined : ariaLabel}
392
+ aria-labelledby={ariaLabelledby}
393
+ style="--svelte-ui-segmented-control-selected-bg: {color || 'var(--svelte-ui-primary-color)'};"
394
+ {id}
395
+ data-testid="segmented-control"
396
+ {...restProps}
397
+ >
398
+ {#each items as item, index}
399
+ {@const isSelected = value === item.value}
400
+ {@const isDisabled = disabled || item.disabled}
401
+ {@const inputId = `${name}-${index}`}
402
+ <div class="segmented-control__item">
403
+ <input
404
+ type="radio"
405
+ id={inputId}
406
+ {name}
407
+ value={item.value}
408
+ checked={isSelected}
409
+ disabled={isDisabled}
410
+ class="segmented-control__input"
411
+ onchange={(e) => handleChange(item, e)}
412
+ onfocus={handleFocus}
413
+ onblur={handleBlur}
414
+ onkeydown={handleKeydown}
415
+ onkeyup={handleKeyup}
416
+ onclick={handleClick}
417
+ onmousedown={handleMouseDown}
418
+ onmouseup={handleMouseUp}
419
+ onmouseenter={handleMouseEnter}
420
+ onmouseleave={handleMouseLeave}
421
+ onmouseover={handleMouseOver}
422
+ onmouseout={handleMouseOut}
423
+ oncontextmenu={handleContextMenu}
424
+ onauxclick={handleAuxClick}
425
+ ontouchstart={handleTouchStart}
426
+ ontouchend={handleTouchEnd}
427
+ ontouchmove={handleTouchMove}
428
+ ontouchcancel={handleTouchCancel}
429
+ onpointerdown={handlePointerDown}
430
+ onpointerup={handlePointerUp}
431
+ onpointerenter={handlePointerEnter}
432
+ onpointerleave={handlePointerLeave}
433
+ onpointermove={handlePointerMove}
434
+ onpointercancel={handlePointerCancel}
435
+ data-testid="segmented-control-input"
436
+ data-value={item.value}
437
+ />
438
+ <label
439
+ for={inputId}
440
+ class="segmented-control__label"
441
+ class:segmented-control__label--selected={isSelected}
442
+ class:segmented-control__label--first={index === 0}
443
+ class:segmented-control__label--last={index === items.length - 1}
444
+ aria-label={item.ariaLabel || item.label || undefined}
445
+ >
446
+ {#if item.icon}
447
+ <span class="segmented-control__icon">
448
+ <Icon
449
+ filled={iconFilled || isSelected}
450
+ weight={iconWeight}
451
+ grade={iconGrade}
452
+ opticalSize={effectiveIconSize}
453
+ variant={iconVariant}
454
+ >
455
+ {item.icon}
456
+ </Icon>
457
+ </span>
458
+ {/if}
459
+ {#if item.label}
460
+ <span class="segmented-control__label-text">{item.label}</span>
461
+ {/if}
462
+ </label>
463
+ </div>
464
+ {/each}
465
+ </div>
466
+
467
+ <style>.segmented-control {
468
+ display: inline-flex;
469
+ position: relative;
470
+ padding: var(--svelte-ui-segmented-control-base-padding);
471
+ background-color: var(--svelte-ui-segmented-control-base-bg);
472
+ border-radius: var(--svelte-ui-segmented-control-base-border-radius);
473
+ gap: var(--svelte-ui-segmented-control-gap);
474
+ box-sizing: border-box;
475
+ width: fit-content;
476
+ max-width: 100%;
477
+ overflow-x: auto;
478
+ overflow-y: visible;
479
+ -ms-overflow-style: none;
480
+ overscroll-behavior: contain;
481
+ }
482
+
483
+ .segmented-control::-webkit-scrollbar {
484
+ display: none;
485
+ }
486
+
487
+ .segmented-control--full-width {
488
+ width: 100%;
489
+ }
490
+
491
+ .segmented-control--full-width .segmented-control__item {
492
+ flex: 1;
493
+ }
494
+
495
+ .segmented-control--rounded {
496
+ border-radius: var(--svelte-ui-border-radius-rounded, 9999px);
497
+ }
498
+
499
+ .segmented-control--disabled {
500
+ opacity: var(--svelte-ui-button-disabled-opacity);
501
+ pointer-events: none;
502
+ }
503
+
504
+ .segmented-control__item {
505
+ position: relative;
506
+ flex: 0 1 auto;
507
+ display: flex;
508
+ align-items: stretch;
509
+ }
510
+
511
+ .segmented-control__input {
512
+ position: absolute;
513
+ width: 0;
514
+ height: 0;
515
+ margin: 0;
516
+ opacity: 0;
517
+ pointer-events: none;
518
+ }
519
+
520
+ .segmented-control__label {
521
+ display: flex;
522
+ align-items: center;
523
+ justify-content: center;
524
+ gap: 6px;
525
+ position: relative;
526
+ border: none;
527
+ background-color: transparent;
528
+ color: var(--svelte-ui-text-subtle-color, var(--svelte-ui-text-color));
529
+ white-space: nowrap;
530
+ cursor: pointer;
531
+ transition-property: background-color, color;
532
+ transition-duration: var(--svelte-ui-transition-duration, 0.2s);
533
+ transition-timing-function: ease;
534
+ outline: none;
535
+ font-family: inherit;
536
+ font-weight: 500;
537
+ line-height: 1;
538
+ box-sizing: border-box;
539
+ width: 100%;
540
+ text-align: center;
541
+ border-radius: var(--svelte-ui-segmented-control-button-radius);
542
+ }
543
+
544
+ .segmented-control--small .segmented-control__label {
545
+ height: var(--svelte-ui-segmented-control-button-height-sm);
546
+ font-size: var(--svelte-ui-segmented-control-font-size-sm);
547
+ padding: var(--svelte-ui-segmented-control-button-padding-sm);
548
+ }
549
+
550
+ .segmented-control--small .segmented-control__label:not(:has(.segmented-control__label-text)) {
551
+ width: var(--svelte-ui-segmented-control-button-height-sm);
552
+ }
553
+
554
+ .segmented-control--medium .segmented-control__label {
555
+ height: var(--svelte-ui-segmented-control-button-height);
556
+ font-size: var(--svelte-ui-segmented-control-font-size);
557
+ padding: var(--svelte-ui-segmented-control-button-padding);
558
+ }
559
+
560
+ .segmented-control--medium .segmented-control__label:not(:has(.segmented-control__label-text)) {
561
+ width: var(--svelte-ui-segmented-control-button-height);
562
+ }
563
+
564
+ .segmented-control--large .segmented-control__label {
565
+ height: var(--svelte-ui-segmented-control-button-height-lg);
566
+ font-size: var(--svelte-ui-segmented-control-font-size-lg);
567
+ padding: var(--svelte-ui-segmented-control-button-padding-lg);
568
+ }
569
+
570
+ .segmented-control--large .segmented-control__label:not(:has(.segmented-control__label-text)) {
571
+ width: var(--svelte-ui-segmented-control-button-height-lg);
572
+ }
573
+
574
+ .segmented-control--rounded .segmented-control__label {
575
+ border-radius: calc(var(--svelte-ui-border-radius-rounded, 9999px) - 2px);
576
+ }
577
+
578
+ @media (hover: hover) {
579
+ .segmented-control__input:not(:disabled) + .segmented-control__label:hover:not(.segmented-control__label--selected) {
580
+ background-color: var(--svelte-ui-segmented-control-hover-overlay);
581
+ color: var(--svelte-ui-text-color);
582
+ }
583
+ }
584
+ .segmented-control__input:focus-visible + .segmented-control__label {
585
+ outline: var(--svelte-ui-focus-outline-inner, 2px solid currentColor);
586
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner, 2px);
587
+ }
588
+
589
+ .segmented-control__input:checked + .segmented-control__label,
590
+ .segmented-control__label--selected {
591
+ background-color: var(--svelte-ui-segmented-control-selected-bg);
592
+ color: var(--svelte-ui-segmented-control-selected-text-color, white);
593
+ }
594
+
595
+ .segmented-control:not(.segmented-control--disabled) .segmented-control__input:disabled + .segmented-control__label {
596
+ opacity: var(--svelte-ui-button-disabled-opacity);
597
+ cursor: not-allowed;
598
+ }
599
+
600
+ .segmented-control__icon {
601
+ display: flex;
602
+ align-items: center;
603
+ justify-content: center;
604
+ }
605
+
606
+ .segmented-control__label-text {
607
+ text-box-trim: trim-both;
608
+ text-box-edge: cap alphabetic;
609
+ }
610
+
611
+ /* Reduced motion */
612
+ .segmented-control--no-motion,
613
+ .segmented-control--no-motion .segmented-control__label {
614
+ transition-duration: 0.01s;
615
+ }
616
+
617
+ @media (prefers-reduced-motion: reduce) {
618
+ .segmented-control,
619
+ .segmented-control__label {
620
+ transition-duration: 0.01s;
621
+ }
622
+ }</style>
@@ -0,0 +1,49 @@
1
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
2
+ import type { SegmentedControlItem } from '../types/segmentedControlItem';
3
+ type $$ComponentProps = {
4
+ items: SegmentedControlItem[];
5
+ value: string;
6
+ id?: string;
7
+ name?: string;
8
+ size?: 'small' | 'medium' | 'large';
9
+ fullWidth?: boolean;
10
+ color?: string;
11
+ rounded?: boolean;
12
+ iconFilled?: boolean;
13
+ iconWeight?: IconWeight;
14
+ iconGrade?: IconGrade;
15
+ iconOpticalSize?: IconOpticalSize;
16
+ iconVariant?: IconVariant;
17
+ disabled?: boolean;
18
+ ariaLabel?: string;
19
+ ariaLabelledby?: string;
20
+ reducedMotion?: boolean;
21
+ onchange?: (value: string) => void;
22
+ onfocus?: Function;
23
+ onblur?: Function;
24
+ onkeydown?: Function;
25
+ onkeyup?: Function;
26
+ onclick?: Function;
27
+ onmousedown?: Function;
28
+ onmouseup?: Function;
29
+ onmouseenter?: Function;
30
+ onmouseleave?: Function;
31
+ onmouseover?: Function;
32
+ onmouseout?: Function;
33
+ oncontextmenu?: Function;
34
+ onauxclick?: Function;
35
+ ontouchstart?: Function;
36
+ ontouchend?: Function;
37
+ ontouchmove?: Function;
38
+ ontouchcancel?: Function;
39
+ onpointerdown?: Function;
40
+ onpointerup?: Function;
41
+ onpointerenter?: Function;
42
+ onpointerleave?: Function;
43
+ onpointermove?: Function;
44
+ onpointercancel?: Function;
45
+ [key: string]: any;
46
+ };
47
+ declare const SegmentedControl: import("svelte").Component<$$ComponentProps, {}, "value">;
48
+ type SegmentedControl = ReturnType<typeof SegmentedControl>;
49
+ export default SegmentedControl;
@@ -71,7 +71,7 @@
71
71
  ...restProps
72
72
  }: {
73
73
  // 基本プロパティ
74
- value?: number;
74
+ value: number;
75
75
  name?: string;
76
76
  id?: string;
77
77
 
@@ -1,6 +1,6 @@
1
1
  import type { HTMLInputAttributes } from 'svelte/elements';
2
2
  type $$ComponentProps = {
3
- value?: number;
3
+ value: number;
4
4
  name?: string;
5
5
  id?: string;
6
6
  min?: number;
@@ -71,7 +71,7 @@
71
71
  children?: Snippet;
72
72
 
73
73
  // 基本プロパティ
74
- value?: boolean;
74
+ value: boolean;
75
75
 
76
76
  // HTML属性系
77
77
  id?: string;
@@ -2,7 +2,7 @@ import type { Snippet } from 'svelte';
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
3
  type $$ComponentProps = {
4
4
  children?: Snippet;
5
- value?: boolean;
5
+ value: boolean;
6
6
  id?: string;
7
7
  inputAttributes?: HTMLInputAttributes | undefined;
8
8
  size?: 'small' | 'medium' | 'large';
@@ -91,7 +91,7 @@
91
91
  }: {
92
92
  // 基本プロパティ
93
93
  name?: string;
94
- value: string | undefined;
94
+ value: string;
95
95
  placeholder?: string;
96
96
 
97
97
  // HTML属性系
@@ -177,9 +177,9 @@
177
177
 
178
178
  const clear = (): void => {
179
179
  if (disabled || readonly) return;
180
- value = undefined;
180
+ value = '';
181
181
  ref?.focus();
182
- onchange?.(value);
182
+ onchange?.('');
183
183
  };
184
184
 
185
185
  // 外部からフォーカスを当てる(キャレットを先頭に移動)
@@ -334,7 +334,7 @@
334
334
 
335
335
  // HTML表示用の値(autoResize時の高さ調整用)
336
336
  const htmlValue = $derived.by(() => {
337
- if (typeof value === 'string' && value !== '') {
337
+ if (value !== '') {
338
338
  let html = value
339
339
  .replace(/ +/g, (match) => '&nbsp;'.repeat(match.length))
340
340
  .replace(/\n/g, '<br />');
@@ -345,7 +345,7 @@
345
345
  }
346
346
  return html;
347
347
  } else {
348
- return value ?? '';
348
+ return '';
349
349
  }
350
350
  });
351
351
  </script>
@@ -2,7 +2,7 @@ import type { HTMLTextareaAttributes } from 'svelte/elements';
2
2
  import type { IconVariant } from '../types/icon';
3
3
  type $$ComponentProps = {
4
4
  name?: string;
5
- value: string | undefined;
5
+ value: string;
6
6
  placeholder?: string;
7
7
  id?: string | null;
8
8
  tabindex?: number | null;
package/dist/index.d.ts CHANGED
@@ -23,6 +23,7 @@ export { default as PopupMenuButton } from './components/PopupMenuButton.svelte'
23
23
  export { default as Radio } from './components/Radio.svelte';
24
24
  export { default as RadioGroup } from './components/RadioGroup.svelte';
25
25
  export { default as Select } from './components/Select.svelte';
26
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
26
27
  export { default as Slider } from './components/Slider.svelte';
27
28
  export { default as Snackbar } from './components/Snackbar.svelte';
28
29
  export { default as SnackbarItem } from './components/SnackbarItem.svelte';
@@ -40,3 +41,4 @@ export * from './utils/mobile';
40
41
  export * from './utils/snackbar.svelte';
41
42
  export * from './utils/style';
42
43
  export type { MenuItem } from './types/menuItem';
44
+ export type { SegmentedControlItem } from './types/segmentedControlItem';
package/dist/index.js CHANGED
@@ -24,6 +24,7 @@ export { default as PopupMenuButton } from './components/PopupMenuButton.svelte'
24
24
  export { default as Radio } from './components/Radio.svelte';
25
25
  export { default as RadioGroup } from './components/RadioGroup.svelte';
26
26
  export { default as Select } from './components/Select.svelte';
27
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
27
28
  export { default as Slider } from './components/Slider.svelte';
28
29
  export { default as Snackbar } from './components/Snackbar.svelte';
29
30
  export { default as SnackbarItem } from './components/SnackbarItem.svelte';
@@ -0,0 +1,13 @@
1
+ export type SegmentedControlItem = {
2
+ label: string;
3
+ value: string;
4
+ icon?: string;
5
+ disabled?: boolean;
6
+ ariaLabel?: string;
7
+ } | {
8
+ label?: never;
9
+ value: string;
10
+ icon: string;
11
+ disabled?: boolean;
12
+ ariaLabel: string;
13
+ };
@@ -0,0 +1 @@
1
+ export {};
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.5",
5
+ "version": "0.0.7",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",