@14ch/svelte-ui 0.0.6 → 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;
@@ -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
 
@@ -95,11 +95,6 @@
95
95
  // 入力イベント
96
96
  onchange = () => {}, // No params for type inference
97
97
  oninput = () => {}, // No params for type inference
98
- onsubmit = () => {}, // No params for type inference
99
-
100
- // IMEイベント
101
- oncompositionstart = () => {}, // No params for type inference
102
- oncompositionend = () => {}, // No params for type inference
103
98
 
104
99
  // アイコンイベント
105
100
  onRightIconClick,
@@ -190,11 +185,6 @@
190
185
  // 入力イベント
191
186
  onchange?: (value: any) => void;
192
187
  oninput?: (value: any) => void;
193
- onsubmit?: (value: any) => void;
194
-
195
- // IMEイベント
196
- oncompositionstart?: Function; // No params for type inference
197
- oncompositionend?: Function; // No params for type inference
198
188
 
199
189
  // アイコンイベント
200
190
  onRightIconClick?: Function; // No params for type inference
@@ -206,6 +196,7 @@
206
196
 
207
197
  let ref: HTMLInputElement | undefined = $state();
208
198
  let isFocused: boolean = $state(false);
199
+ let isComposing: boolean = $state(false);
209
200
 
210
201
  // =========================================================================
211
202
  // Methods
@@ -243,6 +234,10 @@
243
234
 
244
235
  // キーボードイベント
245
236
  const handleKeydown = (event: KeyboardEvent) => {
237
+ // Enterキーで入力確定(blur)する(IME変換中は除く)
238
+ if (event.key === 'Enter' && !disabled && !readonly && !isComposing) {
239
+ ref?.blur();
240
+ }
246
241
  onkeydown?.(event);
247
242
  };
248
243
 
@@ -251,12 +246,6 @@
251
246
  };
252
247
 
253
248
  // 入力イベント
254
- const handleSubmit = (event: SubmitEvent) => {
255
- if (disabled || readonly) return;
256
- event?.preventDefault?.();
257
- ref?.blur();
258
- onsubmit?.(value);
259
- };
260
249
 
261
250
  const handleChange = () => {
262
251
  if (disabled || readonly) return;
@@ -366,6 +355,15 @@
366
355
  onpointercancel?.(event);
367
356
  };
368
357
 
358
+ // IMEイベント
359
+ const handleCompositionStart = () => {
360
+ isComposing = true;
361
+ };
362
+
363
+ const handleCompositionEnd = () => {
364
+ isComposing = false;
365
+ };
366
+
369
367
  // =========================================================================
370
368
  // $derived
371
369
  // =========================================================================
@@ -405,8 +403,8 @@
405
403
  {getDisplayValue()}
406
404
  </div>
407
405
  {/if}
408
- <!-- 入力用フォーム -->
409
- <form onsubmit={handleSubmit}>
406
+ <!-- 入力用要素 -->
407
+ <div class="input__wrapper">
410
408
  <input
411
409
  {id}
412
410
  {name}
@@ -452,10 +450,12 @@
452
450
  onpointerleave={handlePointerLeave}
453
451
  onpointermove={handlePointerMove}
454
452
  onpointercancel={handlePointerCancel}
453
+ oncompositionstart={handleCompositionStart}
454
+ oncompositionend={handleCompositionEnd}
455
455
  {...inputAttributes}
456
456
  {...restProps}
457
457
  />
458
- </form>
458
+ </div>
459
459
  <!-- クリアボタン -->
460
460
  {#if clearable && !disabled && !readonly}
461
461
  <div class="input__clear-button">
@@ -546,7 +546,7 @@
546
546
  height: inherit;
547
547
  }
548
548
 
549
- form {
549
+ .input__wrapper {
550
550
  padding: inherit;
551
551
  border: none;
552
552
  font-size: inherit;
@@ -688,7 +688,7 @@
688
688
  * デザインバリアント:default
689
689
  * ============================================= */
690
690
  .input:not(.input--inline) {
691
- form {
691
+ .input__wrapper {
692
692
  position: static;
693
693
  opacity: 1;
694
694
  }
@@ -749,7 +749,7 @@
749
749
  text-align: right;
750
750
  }
751
751
 
752
- form {
752
+ .input__wrapper {
753
753
  position: absolute;
754
754
  top: 0;
755
755
  left: 0;
@@ -790,7 +790,7 @@
790
790
  opacity: 0;
791
791
  }
792
792
 
793
- form {
793
+ .input__wrapper {
794
794
  opacity: 1;
795
795
  }
796
796
  }
@@ -62,9 +62,6 @@ type $$ComponentProps = {
62
62
  onpointercancel?: Function;
63
63
  onchange?: (value: any) => void;
64
64
  oninput?: (value: any) => void;
65
- onsubmit?: (value: any) => void;
66
- oncompositionstart?: Function;
67
- oncompositionend?: Function;
68
65
  onRightIconClick?: Function;
69
66
  onLeftIconClick?: Function;
70
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;
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.6",
5
+ "version": "0.0.7",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",