@14ch/svelte-ui 0.0.37 → 0.0.38

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.
@@ -498,6 +498,9 @@
498
498
  --svelte-ui-switch-thumb-size-lg: calc(
499
499
  var(--svelte-ui-switch-height-lg) - var(--svelte-ui-switch-thumb-margin) * 2
500
500
  );
501
+ --svelte-ui-switch-min-height: 1.8em;
502
+ --svelte-ui-switch-min-height-sm: 1.6em;
503
+ --svelte-ui-switch-min-height-lg: 2.2em;
501
504
  --svelte-ui-switch-gap: 8px;
502
505
  --svelte-ui-switch-line-height: 1.2em;
503
506
  --svelte-ui-switch-disabled-opacity: var(--svelte-ui-disabled-opacity);
@@ -629,6 +632,7 @@
629
632
  --svelte-ui-datepicker-selected-text-color: var(--svelte-ui-text-on-filled-color);
630
633
 
631
634
  /* Popup */
635
+ --svelte-ui-popup-border-radius: var(--svelte-ui-border-radius);
632
636
  --svelte-ui-popup-mobile-margin: 16px;
633
637
  --svelte-ui-popup-mobile-border-radius: 12px;
634
638
  --svelte-ui-popup-focus-color: var(--svelte-ui-primary-color);
@@ -28,13 +28,17 @@
28
28
 
29
29
  // HTML属性系
30
30
  id?: string;
31
+
31
32
  // スタイル/レイアウト
32
33
  /** Checkbox size. @default 'medium' */
33
34
  size?: 'small' | 'medium' | 'large';
35
+ customStyle?: string;
34
36
 
35
37
  // 状態/動作
36
38
  /** Disables the checkbox. @default false */
37
39
  disabled?: boolean;
40
+ /** Stretches the checkbox to fill its container width. @default false */
41
+ fullWidth?: boolean;
38
42
  required?: boolean;
39
43
 
40
44
  // ARIA/アクセシビリティ
@@ -95,9 +99,11 @@
95
99
 
96
100
  // スタイル/レイアウト
97
101
  size = 'medium',
102
+ customStyle = '',
98
103
 
99
104
  // 状態/動作
100
105
  disabled = false,
106
+ fullWidth = false,
101
107
  required = false,
102
108
 
103
109
  // ARIA/アクセシビリティ
@@ -281,6 +287,7 @@
281
287
  'checkbox',
282
288
  `checkbox--${size}`,
283
289
  disabled && 'checkbox--disabled',
290
+ fullWidth && 'checkbox--full-width',
284
291
  reducedMotion && 'checkbox--no-motion'
285
292
  ]
286
293
  .filter(Boolean)
@@ -288,7 +295,7 @@
288
295
  );
289
296
  </script>
290
297
 
291
- <div class={containerClasses} data-testid="checkbox">
298
+ <label class={containerClasses} style={customStyle} data-testid="checkbox">
292
299
  <input
293
300
  type="checkbox"
294
301
  bind:checked={value}
@@ -326,14 +333,14 @@
326
333
  onchange={handleChange}
327
334
  {...restProps}
328
335
  />
329
- <label for={id} class="checkbox__icon"></label>
336
+ <span class="checkbox__icon"></span>
330
337
 
331
338
  {#if children}
332
- <label for={id} class="checkbox__label">
339
+ <span class="checkbox__label">
333
340
  {@render children()}
334
- </label>
341
+ </span>
335
342
  {/if}
336
- </div>
343
+ </label>
337
344
 
338
345
  <style>
339
346
  /* =========================================================================
@@ -342,11 +349,12 @@
342
349
 
343
350
  .checkbox {
344
351
  display: inline-flex;
345
- align-items: center;
352
+ align-items: flex-start;
346
353
  width: fit-content;
347
354
  min-height: var(--svelte-ui-checkbox-min-height);
348
355
  vertical-align: top;
349
356
  contain: layout;
357
+ cursor: pointer;
350
358
  }
351
359
 
352
360
  .checkbox input[type='checkbox'] {
@@ -355,20 +363,18 @@
355
363
  height: 16px;
356
364
  margin: 0;
357
365
  opacity: 0;
358
- cursor: pointer;
359
366
  }
360
367
 
361
368
  /* Label */
362
369
  .checkbox__label {
363
370
  display: block;
364
371
  padding-left: var(--svelte-ui-checkbox-gap);
365
- white-space: nowrap;
366
372
  font-size: inherit;
367
373
  color: inherit;
368
374
  line-height: var(--svelte-ui-checkbox-line-height);
369
- cursor: pointer;
370
375
  text-box-trim: trim-both;
371
376
  text-box-edge: cap alphabetic;
377
+ margin-block: calc((var(--svelte-ui-checkbox-min-height) - 1cap) / 2);
372
378
  }
373
379
 
374
380
  /* Checkbox box */
@@ -382,7 +388,7 @@
382
388
  transition-property: background-color, border-color, opacity;
383
389
  transition-duration: var(--svelte-ui-transition-duration);
384
390
  flex-shrink: 0;
385
- cursor: pointer;
391
+ margin-block: calc((var(--svelte-ui-checkbox-min-height) - var(--svelte-ui-checkbox-size)) / 2);
386
392
  }
387
393
 
388
394
  /* Check mark */
@@ -420,13 +426,19 @@
420
426
  ========================================================================= */
421
427
 
422
428
  /* Disabled state */
429
+ .checkbox--full-width {
430
+ width: 100%;
431
+ }
432
+
433
+ .checkbox--full-width .checkbox__label {
434
+ flex: 1;
435
+ }
436
+
423
437
  .checkbox--disabled {
424
438
  opacity: var(--svelte-ui-button-disabled-opacity);
425
439
  }
426
440
 
427
- .checkbox--disabled input[type='checkbox'],
428
- .checkbox--disabled .checkbox__icon,
429
- .checkbox--disabled .checkbox__label {
441
+ .checkbox--disabled {
430
442
  cursor: not-allowed;
431
443
  }
432
444
 
@@ -480,12 +492,19 @@
480
492
  .checkbox--small .checkbox__icon {
481
493
  width: var(--svelte-ui-checkbox-size-sm);
482
494
  height: var(--svelte-ui-checkbox-size-sm);
495
+ margin-block: calc(
496
+ (var(--svelte-ui-checkbox-min-height-sm) - var(--svelte-ui-checkbox-size-sm)) / 2
497
+ );
483
498
  }
484
499
 
485
500
  .checkbox--small .checkbox__icon::after {
486
501
  font-size: var(--svelte-ui-checkbox-icon-size-sm);
487
502
  }
488
503
 
504
+ .checkbox--small .checkbox__label {
505
+ margin-block: calc((var(--svelte-ui-checkbox-min-height-sm) - 1cap) / 2);
506
+ }
507
+
489
508
  .checkbox--large {
490
509
  font-size: inherit;
491
510
  }
@@ -493,12 +512,19 @@
493
512
  .checkbox--large .checkbox__icon {
494
513
  width: var(--svelte-ui-checkbox-size-lg);
495
514
  height: var(--svelte-ui-checkbox-size-lg);
515
+ margin-block: calc(
516
+ (var(--svelte-ui-checkbox-min-height-lg) - var(--svelte-ui-checkbox-size-lg)) / 2
517
+ );
496
518
  }
497
519
 
498
520
  .checkbox--large .checkbox__icon::after {
499
521
  font-size: var(--svelte-ui-checkbox-icon-size-lg);
500
522
  }
501
523
 
524
+ .checkbox--large .checkbox__label {
525
+ margin-block: calc((var(--svelte-ui-checkbox-min-height-lg) - 1cap) / 2);
526
+ }
527
+
502
528
  /* =========================================================================
503
529
  * Motion & Media Queries
504
530
  * ========================================================================= */
@@ -12,8 +12,11 @@ export type CheckboxProps = {
12
12
  id?: string;
13
13
  /** Checkbox size. @default 'medium' */
14
14
  size?: 'small' | 'medium' | 'large';
15
+ customStyle?: string;
15
16
  /** Disables the checkbox. @default false */
16
17
  disabled?: boolean;
18
+ /** Stretches the checkbox to fill its container width. @default false */
19
+ fullWidth?: boolean;
17
20
  required?: boolean;
18
21
  /** Disables animations for users who prefer reduced motion. @default false */
19
22
  reducedMotion?: boolean;
@@ -562,7 +562,6 @@
562
562
  width: max-content;
563
563
  max-width: var(--svelte-ui-combobox-max-width);
564
564
  background: var(--svelte-ui-combobox-bg);
565
- border-radius: var(--svelte-ui-combobox-border-radius);
566
565
  max-height: var(--svelte-ui-combobox-options-max-height);
567
566
  overflow-y: auto;
568
567
  margin: 0;
@@ -919,15 +919,20 @@
919
919
  color: inherit;
920
920
  line-height: inherit;
921
921
  text-align: inherit;
922
- white-space: nowrap;
923
- overflow: hidden;
924
- text-overflow: ellipsis;
925
922
  opacity: 1;
926
923
  transition: none;
927
924
  }
928
925
 
929
926
  .input__display-text-content {
930
927
  width: 100%;
928
+ overflow: hidden;
929
+ white-space: nowrap;
930
+ }
931
+
932
+ .input__link-text-content {
933
+ width: 100%;
934
+ overflow: hidden;
935
+ white-space: nowrap;
931
936
  }
932
937
 
933
938
  .input__link-text {
@@ -0,0 +1,379 @@
1
+ <!-- MultiSelect.svelte -->
2
+
3
+ <script lang="ts">
4
+ import Popup from './Popup.svelte';
5
+ import Checkbox from './Checkbox.svelte';
6
+ import Icon from './Icon.svelte';
7
+ import { t } from '../i18n';
8
+ import type { Option, OptionValue } from '../types/options';
9
+ import type { BivariantValueHandler, FocusHandler } from '../types/callbackHandlers';
10
+ import type { PopupPosition } from '../types/propOptions';
11
+
12
+ // =========================================================================
13
+ // Props, States & Constants
14
+ // =========================================================================
15
+ export type MultiSelectProps = {
16
+ // 基本プロパティ
17
+ name?: string;
18
+ /** Selected values array. Supports `bind:values`. */
19
+ values: OptionValue[];
20
+ /** `{ label, value, disabled? }[]` */
21
+ options: Option[];
22
+
23
+ // HTML属性系
24
+ id?: string | null;
25
+ ariaLabel?: string;
26
+ tabindex?: number | null;
27
+ placeholder?: string;
28
+
29
+ // スタイル/レイアウト
30
+ /** Renders inline. @default false */
31
+ inline?: boolean;
32
+ /** @default 'outline' */
33
+ focusStyle?: 'background' | 'outline' | 'none';
34
+ fullWidth?: boolean;
35
+ rounded?: boolean;
36
+
37
+ // 状態/動作
38
+ disabled?: boolean;
39
+ required?: boolean;
40
+
41
+ // ポップアップ
42
+ /** @default 'bottom-left' */
43
+ position?: PopupPosition;
44
+
45
+ // イベントハンドラ
46
+ onfocus?: FocusHandler;
47
+ onblur?: FocusHandler;
48
+ onchange?: BivariantValueHandler<OptionValue[]>;
49
+ };
50
+
51
+ let {
52
+ name,
53
+ values = $bindable([]),
54
+ options = [],
55
+
56
+ id = `multi-select-${Math.random().toString(36).substring(2, 15)}`,
57
+ ariaLabel,
58
+ tabindex = null,
59
+ placeholder = '',
60
+
61
+ inline = false,
62
+ focusStyle = 'outline',
63
+ fullWidth = false,
64
+ rounded = false,
65
+
66
+ disabled = false,
67
+ required = false,
68
+
69
+ position = 'bottom-left',
70
+
71
+ onfocus = () => {},
72
+ onblur = () => {},
73
+ onchange = () => {}
74
+ }: MultiSelectProps = $props();
75
+
76
+ let popupRef = $state<any>();
77
+ let triggerEl = $state<HTMLButtonElement>();
78
+ let isPopupOpen = $state(false);
79
+ let triggerWidth = $state(0);
80
+ let isFocused = $state(false);
81
+
82
+ // =========================================================================
83
+ // $derived
84
+ // =========================================================================
85
+ const listboxId = $derived(`${id}-listbox`);
86
+
87
+ const selectedLabels = $derived(
88
+ options.filter((o) => values.includes(o.value)).map((o) => o.label)
89
+ );
90
+
91
+ // =========================================================================
92
+ // Methods
93
+ // =========================================================================
94
+ const toggleOption = (optionValue: OptionValue) => {
95
+ if (values.includes(optionValue)) {
96
+ values = values.filter((v) => v !== optionValue);
97
+ } else {
98
+ values = [...values, optionValue];
99
+ }
100
+ onchange(values);
101
+ };
102
+
103
+ const handleTriggerClick = () => {
104
+ if (disabled) return;
105
+ popupRef?.toggle();
106
+ };
107
+
108
+ const handleTriggerKeydown = (event: KeyboardEvent) => {
109
+ if (disabled) return;
110
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
111
+ event.preventDefault();
112
+ popupRef?.open();
113
+ }
114
+ };
115
+
116
+ const handleFocus = (event: FocusEvent) => {
117
+ if (disabled) return;
118
+ isFocused = true;
119
+ onfocus(event);
120
+ };
121
+
122
+ const handleBlur = (event: FocusEvent) => {
123
+ if (disabled) return;
124
+ isFocused = false;
125
+ onblur(event);
126
+ };
127
+
128
+ const handlePopupOpen = () => {
129
+ triggerWidth = triggerEl?.offsetWidth ?? 0;
130
+ };
131
+
132
+ const handlePopupClose = () => {
133
+ triggerEl?.focus();
134
+ };
135
+ </script>
136
+
137
+ <div
138
+ class="multi-select multi-select--focus-{focusStyle}"
139
+ class:multi-select--inline={inline}
140
+ class:multi-select--full-width={fullWidth}
141
+ class:multi-select--disabled={disabled}
142
+ class:multi-select--focused={isFocused}
143
+ class:multi-select--rounded={rounded}
144
+ data-testid="multi-select"
145
+ >
146
+ <button
147
+ bind:this={triggerEl}
148
+ {id}
149
+ type="button"
150
+ class="multi-select__trigger"
151
+ role="combobox"
152
+ aria-expanded={isPopupOpen}
153
+ aria-haspopup="listbox"
154
+ aria-controls={isPopupOpen ? listboxId : undefined}
155
+ aria-label={ariaLabel ?? t('multiSelect.accessibleName')}
156
+ aria-required={required ? 'true' : undefined}
157
+ {tabindex}
158
+ {disabled}
159
+ onfocus={handleFocus}
160
+ onblur={handleBlur}
161
+ onclick={handleTriggerClick}
162
+ onkeydown={handleTriggerKeydown}
163
+ >
164
+ {#if selectedLabels.length > 0}
165
+ <span class="multi-select__display-text">
166
+ {#each selectedLabels as label, i}
167
+ <span>{label}{#if i < selectedLabels.length - 1},{/if}</span>
168
+ {/each}
169
+ </span>
170
+ {:else if placeholder}
171
+ <span class="multi-select__placeholder">{placeholder}</span>
172
+ {/if}
173
+ </button>
174
+ <div class="multi-select__dropdown-icon" aria-hidden="true">
175
+ <Icon>arrow_drop_down</Icon>
176
+ </div>
177
+
178
+ <Popup
179
+ bind:this={popupRef}
180
+ bind:isOpen={isPopupOpen}
181
+ anchorElement={triggerEl}
182
+ {position}
183
+ mobileFullscreen={true}
184
+ onOpen={handlePopupOpen}
185
+ onClose={handlePopupClose}
186
+ margin={4}
187
+ >
188
+ <ul
189
+ id={listboxId}
190
+ class="multi-select__options"
191
+ role="listbox"
192
+ aria-multiselectable="true"
193
+ aria-label={ariaLabel ?? t('multiSelect.accessibleName')}
194
+ style:min-width="{triggerWidth}px"
195
+ >
196
+ {#each options as option, i (option.value)}
197
+ <li role="presentation" class="multi-select__item">
198
+ <Checkbox
199
+ value={values.includes(option.value)}
200
+ disabled={option.disabled}
201
+ fullWidth
202
+ customStyle="padding: 8px 12px"
203
+ onchange={() => toggleOption(option.value)}
204
+ >
205
+ {option.label}
206
+ </Checkbox>
207
+ </li>
208
+ {/each}
209
+ </ul>
210
+ </Popup>
211
+ </div>
212
+
213
+ <style>@charset "UTF-8";
214
+ /* =============================================
215
+ * 基本構造・レイアウト
216
+ * ============================================= */
217
+ .multi-select {
218
+ display: inline-block;
219
+ position: relative;
220
+ width: auto;
221
+ max-width: 100%;
222
+ vertical-align: top;
223
+ }
224
+
225
+ /* =============================================
226
+ * トリガーボタン
227
+ * ============================================= */
228
+ .multi-select__trigger {
229
+ width: 100%;
230
+ min-height: var(--svelte-ui-select-height);
231
+ padding: var(--svelte-ui-select-padding);
232
+ padding-right: var(--svelte-ui-select-icon-space);
233
+ background: transparent;
234
+ border: none;
235
+ font-family: inherit;
236
+ font-size: inherit;
237
+ font-weight: inherit;
238
+ color: inherit;
239
+ line-height: inherit;
240
+ text-align: left;
241
+ cursor: pointer;
242
+ display: flex;
243
+ align-items: center;
244
+ }
245
+ .multi-select__trigger:focus, .multi-select__trigger:focus-visible {
246
+ outline: var(--svelte-ui-focus-outline-inner);
247
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner);
248
+ }
249
+
250
+ .multi-select__display-text {
251
+ display: flex;
252
+ flex-wrap: wrap;
253
+ align-items: baseline;
254
+ gap: 0 0.5em;
255
+ flex: 1;
256
+ }
257
+
258
+ .multi-select__placeholder {
259
+ color: var(--svelte-ui-select-placeholder-color);
260
+ display: block;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ white-space: nowrap;
264
+ flex: 1;
265
+ }
266
+
267
+ .multi-select__dropdown-icon {
268
+ display: flex;
269
+ justify-content: center;
270
+ align-items: center;
271
+ position: absolute;
272
+ top: 50%;
273
+ right: 4px;
274
+ width: 32px;
275
+ height: 32px;
276
+ transform: translateY(-50%);
277
+ font-size: var(--svelte-ui-select-dropdown-icon-size);
278
+ color: var(--svelte-ui-select-dropdown-icon-color);
279
+ pointer-events: none;
280
+ }
281
+
282
+ /* =============================================
283
+ * レイアウトバリエーション
284
+ * ============================================= */
285
+ .multi-select--full-width {
286
+ width: 100%;
287
+ }
288
+
289
+ /* =============================================
290
+ * フォーカス効果バリエーション
291
+ * ============================================= */
292
+ .multi-select--focus-outline .multi-select__trigger:focus {
293
+ outline: var(--svelte-ui-focus-outline-inner);
294
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner);
295
+ }
296
+
297
+ .multi-select--focus-background .multi-select__trigger:focus {
298
+ background: var(--svelte-ui-hover-overlay);
299
+ }
300
+
301
+ /* =============================================
302
+ * 状態管理
303
+ * ============================================= */
304
+ .multi-select--disabled {
305
+ opacity: var(--svelte-ui-input-disabled-opacity);
306
+ cursor: not-allowed;
307
+ }
308
+ .multi-select--disabled .multi-select__trigger {
309
+ cursor: not-allowed;
310
+ pointer-events: none;
311
+ }
312
+
313
+ /* =============================================
314
+ * デザインバリアント:default
315
+ * ============================================= */
316
+ .multi-select:not(.multi-select--inline) .multi-select__trigger {
317
+ min-height: var(--svelte-ui-select-height);
318
+ background-color: var(--svelte-ui-select-bg);
319
+ box-shadow: 0 0 0 var(--svelte-ui-border-width) inset var(--svelte-ui-select-border-color);
320
+ border: none;
321
+ border-radius: var(--svelte-ui-select-border-radius);
322
+ font-size: 1rem;
323
+ }
324
+
325
+ /* =============================================
326
+ * デザインバリアント:inline
327
+ * ============================================= */
328
+ .multi-select.multi-select--inline .multi-select__trigger {
329
+ padding: inherit;
330
+ padding-right: var(--svelte-ui-input-icon-space-inline);
331
+ background: transparent;
332
+ border: none;
333
+ border-radius: 0;
334
+ color: inherit;
335
+ min-height: auto;
336
+ line-height: inherit;
337
+ }
338
+ .multi-select.multi-select--inline .multi-select__dropdown-icon {
339
+ right: 0;
340
+ }
341
+
342
+ /* =============================================
343
+ * デザインバリアント:rounded
344
+ * ============================================= */
345
+ .multi-select--rounded:not(.multi-select--inline) .multi-select__trigger {
346
+ border-radius: var(--svelte-ui-select-border-radius-rounded);
347
+ }
348
+
349
+ /* =============================================
350
+ * オプションリスト(ポップアップ内)
351
+ * ============================================= */
352
+ .multi-select__options {
353
+ list-style: none;
354
+ margin: 0;
355
+ padding: 0;
356
+ min-width: 160px;
357
+ max-height: var(--svelte-ui-combobox-options-max-height);
358
+ overflow-y: auto;
359
+ }
360
+
361
+ .multi-select__item {
362
+ position: relative;
363
+ padding: 0;
364
+ margin: 0;
365
+ }
366
+ @media (hover: hover) {
367
+ .multi-select__item:hover :global(.checkbox:not(.checkbox--disabled)) {
368
+ background-color: var(--svelte-ui-hover-overlay);
369
+ }
370
+ }
371
+
372
+ .multi-select__option-cover {
373
+ position: absolute;
374
+ inset: 0;
375
+ cursor: pointer;
376
+ }
377
+ .multi-select__option-cover--disabled {
378
+ cursor: not-allowed;
379
+ }</style>
@@ -0,0 +1,30 @@
1
+ import type { Option, OptionValue } from '../types/options';
2
+ import type { BivariantValueHandler, FocusHandler } from '../types/callbackHandlers';
3
+ import type { PopupPosition } from '../types/propOptions';
4
+ export type MultiSelectProps = {
5
+ name?: string;
6
+ /** Selected values array. Supports `bind:values`. */
7
+ values: OptionValue[];
8
+ /** `{ label, value, disabled? }[]` */
9
+ options: Option[];
10
+ id?: string | null;
11
+ ariaLabel?: string;
12
+ tabindex?: number | null;
13
+ placeholder?: string;
14
+ /** Renders inline. @default false */
15
+ inline?: boolean;
16
+ /** @default 'outline' */
17
+ focusStyle?: 'background' | 'outline' | 'none';
18
+ fullWidth?: boolean;
19
+ rounded?: boolean;
20
+ disabled?: boolean;
21
+ required?: boolean;
22
+ /** @default 'bottom-left' */
23
+ position?: PopupPosition;
24
+ onfocus?: FocusHandler;
25
+ onblur?: FocusHandler;
26
+ onchange?: BivariantValueHandler<OptionValue[]>;
27
+ };
28
+ declare const MultiSelect: import("svelte").Component<MultiSelectProps, {}, "values">;
29
+ type MultiSelect = ReturnType<typeof MultiSelect>;
30
+ export default MultiSelect;
@@ -553,9 +553,10 @@
553
553
  <style>@charset "UTF-8";
554
554
  :popover-open {
555
555
  border: solid 1px var(--svelte-ui-border-weak-color);
556
- border-radius: 4px;
556
+ border-radius: var(--svelte-ui-popup-border-radius);
557
557
  box-shadow: 0 11px 15px -7px rgba(0, 0, 0, 0.2), 0 24px 38px 3px rgba(0, 0, 0, 0.14), 0 9px 46px 8px rgba(0, 0, 0, 0.12);
558
558
  background: var(--svelte-ui-surface-color);
559
+ color: var(--svelte-ui-text-color);
559
560
  z-index: 1000; /* Popupを最前面に表示 */
560
561
  }
561
562
 
@@ -30,13 +30,17 @@
30
30
 
31
31
  // HTML属性系
32
32
  id?: string;
33
+
33
34
  // スタイル/レイアウト
34
35
  /** Radio button size. @default 'medium' */
35
36
  size?: 'small' | 'medium' | 'large';
37
+ customStyle?: string;
36
38
 
37
39
  // 状態/動作
38
40
  /** Disables this radio button. @default false */
39
41
  disabled?: boolean;
42
+ /** Stretches the radio to fill its container width. @default false */
43
+ fullWidth?: boolean;
40
44
  required?: boolean;
41
45
 
42
46
  // ARIA/アクセシビリティ
@@ -97,9 +101,11 @@
97
101
 
98
102
  // スタイル/レイアウト
99
103
  size = 'medium',
104
+ customStyle = '',
100
105
 
101
106
  // 状態/動作
102
107
  disabled = false,
108
+ fullWidth = false,
103
109
  required = false,
104
110
 
105
111
  // ARIA/アクセシビリティ
@@ -310,13 +316,19 @@
310
316
  const isChecked: boolean = $derived(currentValue === value);
311
317
 
312
318
  const containerClasses = $derived(
313
- ['radio', `radio--${size}`, disabled && 'radio--disabled', reducedMotion && 'radio--no-motion']
319
+ [
320
+ 'radio',
321
+ `radio--${size}`,
322
+ disabled && 'radio--disabled',
323
+ fullWidth && 'radio--full-width',
324
+ reducedMotion && 'radio--no-motion'
325
+ ]
314
326
  .filter(Boolean)
315
327
  .join(' ')
316
328
  );
317
329
  </script>
318
330
 
319
- <div class={containerClasses} data-testid="radio">
331
+ <label class={containerClasses} style={customStyle} data-testid="radio">
320
332
  <input
321
333
  type="radio"
322
334
  checked={isChecked}
@@ -352,14 +364,14 @@
352
364
  onchange={handleChange}
353
365
  {...restProps}
354
366
  />
355
- <label for={id} class="radio__icon"></label>
367
+ <span class="radio__icon"></span>
356
368
 
357
369
  {#if children}
358
- <label for={id} class="radio__label">
370
+ <span class="radio__label">
359
371
  {@render children()}
360
- </label>
372
+ </span>
361
373
  {/if}
362
- </div>
374
+ </label>
363
375
 
364
376
  <style>
365
377
  /* =============================================
@@ -368,11 +380,12 @@
368
380
 
369
381
  .radio {
370
382
  display: inline-flex;
371
- align-items: center;
383
+ align-items: flex-start;
372
384
  width: fit-content;
373
385
  min-height: var(--svelte-ui-radio-min-height);
374
386
  vertical-align: top;
375
387
  contain: layout;
388
+ cursor: pointer;
376
389
  }
377
390
 
378
391
  .radio input[type='radio'] {
@@ -382,20 +395,18 @@
382
395
  margin: 0;
383
396
  line-height: 1px;
384
397
  opacity: 0;
385
- cursor: pointer;
386
398
  }
387
399
 
388
400
  /* Label */
389
401
  .radio__label {
390
402
  display: block;
391
403
  padding-left: var(--svelte-ui-radio-gap);
392
- white-space: nowrap;
393
404
  font-size: inherit;
394
405
  color: inherit;
395
406
  line-height: var(--svelte-ui-radio-line-height);
396
- cursor: pointer;
397
407
  text-box-trim: trim-both;
398
408
  text-box-edge: cap alphabetic;
409
+ margin-block: calc((var(--svelte-ui-radio-min-height) - 1cap) / 2);
399
410
  }
400
411
 
401
412
  /* Icon */
@@ -404,11 +415,11 @@
404
415
  display: flex;
405
416
  align-items: center;
406
417
  width: var(--svelte-ui-radio-size);
407
- white-space: nowrap;
418
+ height: var(--svelte-ui-radio-size);
408
419
  font-size: inherit;
409
420
  color: inherit;
410
- cursor: pointer;
411
- min-height: var(--svelte-ui-radio-min-height);
421
+ flex-shrink: 0;
422
+ margin-block: calc((var(--svelte-ui-radio-min-height) - var(--svelte-ui-radio-size)) / 2);
412
423
  }
413
424
 
414
425
  .radio__icon::before,
@@ -453,13 +464,19 @@
453
464
  /* =============================================
454
465
  * Status
455
466
  * ============================================= */
467
+ .radio--full-width {
468
+ width: 100%;
469
+ }
470
+
471
+ .radio--full-width .radio__label {
472
+ flex: 1;
473
+ }
474
+
456
475
  .radio--disabled {
457
476
  opacity: var(--svelte-ui-button-disabled-opacity);
458
477
  }
459
478
 
460
- .radio--disabled input[type='radio'],
461
- .radio--disabled .radio__icon,
462
- .radio--disabled .radio__label {
479
+ .radio--disabled {
463
480
  cursor: not-allowed;
464
481
  }
465
482
 
@@ -493,7 +510,12 @@
493
510
 
494
511
  .radio--small .radio__icon {
495
512
  width: var(--svelte-ui-radio-size-sm);
496
- min-height: var(--svelte-ui-radio-min-height-sm);
513
+ height: var(--svelte-ui-radio-size-sm);
514
+ margin-block: calc((var(--svelte-ui-radio-min-height-sm) - var(--svelte-ui-radio-size-sm)) / 2);
515
+ }
516
+
517
+ .radio--small .radio__label {
518
+ margin-block: calc((var(--svelte-ui-radio-min-height-sm) - 1cap) / 2);
497
519
  }
498
520
 
499
521
  .radio--small .radio__icon::after {
@@ -517,7 +539,12 @@
517
539
 
518
540
  .radio--large .radio__icon {
519
541
  width: var(--svelte-ui-radio-size-lg);
520
- min-height: var(--svelte-ui-radio-min-height-lg);
542
+ height: var(--svelte-ui-radio-size-lg);
543
+ margin-block: calc((var(--svelte-ui-radio-min-height-lg) - var(--svelte-ui-radio-size-lg)) / 2);
544
+ }
545
+
546
+ .radio--large .radio__label {
547
+ margin-block: calc((var(--svelte-ui-radio-min-height-lg) - 1cap) / 2);
521
548
  }
522
549
 
523
550
  .radio--large .radio__icon::after {
@@ -13,8 +13,11 @@ export type RadioProps = {
13
13
  id?: string;
14
14
  /** Radio button size. @default 'medium' */
15
15
  size?: 'small' | 'medium' | 'large';
16
+ customStyle?: string;
16
17
  /** Disables this radio button. @default false */
17
18
  disabled?: boolean;
19
+ /** Stretches the radio to fill its container width. @default false */
20
+ fullWidth?: boolean;
18
21
  required?: boolean;
19
22
  /** Disables animations for users who prefer reduced motion. @default false */
20
23
  reducedMotion?: boolean;
@@ -26,9 +26,12 @@
26
26
  // スタイル/レイアウト
27
27
  /** @default 'medium' */
28
28
  size?: 'small' | 'medium' | 'large';
29
+ customStyle?: string;
29
30
 
30
31
  // 状態/動作
31
32
  disabled?: boolean;
33
+ /** Stretches the switch to fill its container width. @default false */
34
+ fullWidth?: boolean;
32
35
  required?: boolean;
33
36
 
34
37
  // ARIA/アクセシビリティ
@@ -87,9 +90,11 @@
87
90
 
88
91
  // スタイル/レイアウト
89
92
  size = 'medium',
93
+ customStyle = '',
90
94
 
91
95
  // 状態/動作
92
96
  disabled = false,
97
+ fullWidth = false,
93
98
  required = false,
94
99
 
95
100
  // ARIA/アクセシビリティ
@@ -271,14 +276,16 @@
271
276
  };
272
277
  </script>
273
278
 
274
- <div
279
+ <label
275
280
  class="switch"
276
281
  class:switch--small={size === 'small'}
277
282
  class:switch--medium={size === 'medium'}
278
283
  class:switch--large={size === 'large'}
279
284
  class:switch--disabled={disabled}
285
+ class:switch--full-width={fullWidth}
280
286
  class:switch--checked={value}
281
287
  class:switch--reduced-motion={reducedMotion}
288
+ style={customStyle}
282
289
  data-testid="switch"
283
290
  >
284
291
  <input
@@ -316,16 +323,16 @@
316
323
  {...restProps}
317
324
  />
318
325
 
319
- <label for={id} class="switch__track">
326
+ <span class="switch__track">
320
327
  <span class="switch-thumb"></span>
321
- </label>
328
+ </span>
322
329
 
323
- <label for={id} class="switch__label" class:switch__label--disabled={disabled}>
330
+ <span class="switch__label" class:switch__label--disabled={disabled}>
324
331
  {#if children}
325
332
  {@render children()}
326
333
  {/if}
327
- </label>
328
- </div>
334
+ </span>
335
+ </label>
329
336
 
330
337
  <style>@charset "UTF-8";
331
338
  /* =============================================
@@ -333,9 +340,19 @@
333
340
  * ============================================= */
334
341
  .switch {
335
342
  display: inline-flex;
336
- align-items: center;
343
+ align-items: flex-start;
337
344
  width: fit-content;
345
+ min-height: var(--svelte-ui-switch-min-height);
338
346
  contain: layout;
347
+ cursor: pointer;
348
+ }
349
+
350
+ .switch--full-width {
351
+ width: 100%;
352
+ }
353
+
354
+ .switch--full-width .switch__label {
355
+ flex: 1;
339
356
  }
340
357
 
341
358
  .switch--disabled {
@@ -358,15 +375,13 @@
358
375
  .switch__label {
359
376
  display: block;
360
377
  padding-left: var(--svelte-ui-switch-gap);
361
- white-space: nowrap;
362
378
  line-height: var(--svelte-ui-checkbox-line-height);
363
- cursor: pointer;
364
379
  text-box-trim: trim-both;
365
380
  text-box-edge: cap alphabetic;
366
381
  user-select: none;
382
+ margin-block: calc((var(--svelte-ui-switch-min-height) - 1cap) / 2);
367
383
  }
368
384
  .switch__label--disabled {
369
- cursor: not-allowed;
370
385
  opacity: 0.5;
371
386
  }
372
387
 
@@ -378,7 +393,7 @@
378
393
  border-radius: var(--switch-border-radius);
379
394
  transition: background-color var(--svelte-ui-transition-duration) ease, filter var(--svelte-ui-transition-duration) ease;
380
395
  flex-shrink: 0;
381
- cursor: pointer;
396
+ margin-block: calc((var(--svelte-ui-switch-min-height) - var(--switch-height, var(--svelte-ui-switch-height))) / 2);
382
397
  }
383
398
  .switch--checked .switch__track {
384
399
  background-color: var(--switch-active-color, var(--svelte-ui-switch-active-color));
@@ -412,6 +427,15 @@
412
427
  --switch-thumb-margin: var(--svelte-ui-switch-thumb-margin);
413
428
  --switch-border-radius: var(--svelte-ui-switch-border-radius);
414
429
  --switch-thumb-border-radius: var(--svelte-ui-switch-thumb-border-radius);
430
+ min-height: var(--svelte-ui-switch-min-height-sm);
431
+ }
432
+
433
+ .switch--small .switch__track {
434
+ margin-block: calc((var(--svelte-ui-switch-min-height-sm) - var(--svelte-ui-switch-height-sm)) / 2);
435
+ }
436
+
437
+ .switch--small .switch__label {
438
+ margin-block: calc((var(--svelte-ui-switch-min-height-sm) - 1cap) / 2);
415
439
  }
416
440
 
417
441
  .switch--medium {
@@ -430,6 +454,15 @@
430
454
  --switch-thumb-margin: var(--svelte-ui-switch-thumb-margin);
431
455
  --switch-border-radius: var(--svelte-ui-switch-border-radius);
432
456
  --switch-thumb-border-radius: var(--svelte-ui-switch-thumb-border-radius);
457
+ min-height: var(--svelte-ui-switch-min-height-lg);
458
+ }
459
+
460
+ .switch--large .switch__track {
461
+ margin-block: calc((var(--svelte-ui-switch-min-height-lg) - var(--svelte-ui-switch-height-lg)) / 2);
462
+ }
463
+
464
+ .switch--large .switch__label {
465
+ margin-block: calc((var(--svelte-ui-switch-min-height-lg) - 1cap) / 2);
433
466
  }
434
467
 
435
468
  /* =============================================
@@ -448,12 +481,30 @@
448
481
  .switch {
449
482
  min-height: var(--svelte-ui-touch-target);
450
483
  }
484
+ .switch__track {
485
+ margin-block: calc((var(--svelte-ui-touch-target) - var(--switch-height, var(--svelte-ui-switch-height))) / 2);
486
+ }
487
+ .switch__label {
488
+ margin-block: calc((var(--svelte-ui-touch-target) - 1cap) / 2);
489
+ }
451
490
  .switch--small {
452
491
  min-height: var(--svelte-ui-touch-target-sm);
453
492
  }
493
+ .switch--small .switch__track {
494
+ margin-block: calc((var(--svelte-ui-touch-target-sm) - var(--switch-height, var(--svelte-ui-switch-height-sm))) / 2);
495
+ }
496
+ .switch--small .switch__label {
497
+ margin-block: calc((var(--svelte-ui-touch-target-sm) - 1cap) / 2);
498
+ }
454
499
  .switch--large {
455
500
  min-height: var(--svelte-ui-touch-target-lg);
456
501
  }
502
+ .switch--large .switch__track {
503
+ margin-block: calc((var(--svelte-ui-touch-target-lg) - var(--switch-height, var(--svelte-ui-switch-height-lg))) / 2);
504
+ }
505
+ .switch--large .switch__label {
506
+ margin-block: calc((var(--svelte-ui-touch-target-lg) - 1cap) / 2);
507
+ }
457
508
  }
458
509
  .switch--reduced-motion * {
459
510
  transition: none !important;
@@ -7,7 +7,10 @@ export type SwitchProps = {
7
7
  id?: string;
8
8
  /** @default 'medium' */
9
9
  size?: 'small' | 'medium' | 'large';
10
+ customStyle?: string;
10
11
  disabled?: boolean;
12
+ /** Stretches the switch to fill its container width. @default false */
13
+ fullWidth?: boolean;
11
14
  required?: boolean;
12
15
  /** Disables animations for accessibility. @default false */
13
16
  reducedMotion?: boolean;
@@ -699,12 +699,6 @@
699
699
  transition: none;
700
700
  overflow-y: auto;
701
701
  overflow-x: hidden;
702
- scrollbar-width: none; /* Firefox */
703
- -ms-overflow-style: none; /* IE and Edge */
704
-
705
- &::-webkit-scrollbar {
706
- display: none; /* Chrome, Safari, Opera */
707
- }
708
702
 
709
703
  &::before {
710
704
  content: '';
@@ -715,6 +709,20 @@
715
709
  }
716
710
  }
717
711
 
712
+ /* display-text: スクロールバーの幅を確保しつつ透明にする(textarea と幅を一致させる) */
713
+ .textarea__display-text {
714
+ scrollbar-color: transparent transparent;
715
+ }
716
+
717
+ /* link-text: 絶対配置のオーバーレイなのでスクロールバーを完全に非表示 */
718
+ .textarea__link-text {
719
+ scrollbar-width: none;
720
+
721
+ &::-webkit-scrollbar {
722
+ display: none;
723
+ }
724
+ }
725
+
718
726
  /* クリック可能なリンク用オーバーレイ */
719
727
  .textarea__link-text {
720
728
  position: absolute;
@@ -33,6 +33,10 @@ export declare const TRANSLATIONS: {
33
33
  readonly select: {
34
34
  readonly accessibleName: "Select option";
35
35
  };
36
+ readonly multiSelect: {
37
+ readonly accessibleName: "Select options";
38
+ readonly placeholder: "Select options";
39
+ };
36
40
  readonly slider: {
37
41
  readonly accessibleName: "Slider";
38
42
  };
@@ -88,6 +92,10 @@ export declare const TRANSLATIONS: {
88
92
  readonly select: {
89
93
  readonly accessibleName: "選択";
90
94
  };
95
+ readonly multiSelect: {
96
+ readonly accessibleName: "複数選択";
97
+ readonly placeholder: "選択してください";
98
+ };
91
99
  readonly slider: {
92
100
  readonly accessibleName: "スライダー";
93
101
  };
@@ -143,6 +151,10 @@ export declare const TRANSLATIONS: {
143
151
  readonly select: {
144
152
  readonly accessibleName: "Choisir une option";
145
153
  };
154
+ readonly multiSelect: {
155
+ readonly accessibleName: "Sélectionner des options";
156
+ readonly placeholder: "Sélectionnez des options";
157
+ };
146
158
  readonly slider: {
147
159
  readonly accessibleName: "Curseur";
148
160
  };
@@ -198,6 +210,10 @@ export declare const TRANSLATIONS: {
198
210
  readonly select: {
199
211
  readonly accessibleName: "Option auswählen";
200
212
  };
213
+ readonly multiSelect: {
214
+ readonly accessibleName: "Optionen auswählen";
215
+ readonly placeholder: "Bitte wählen";
216
+ };
201
217
  readonly slider: {
202
218
  readonly accessibleName: "Schieberegler";
203
219
  };
@@ -253,6 +269,10 @@ export declare const TRANSLATIONS: {
253
269
  readonly select: {
254
270
  readonly accessibleName: "Seleccionar opción";
255
271
  };
272
+ readonly multiSelect: {
273
+ readonly accessibleName: "Seleccionar opciones";
274
+ readonly placeholder: "Seleccione opciones";
275
+ };
256
276
  readonly slider: {
257
277
  readonly accessibleName: "Control deslizante";
258
278
  };
@@ -308,6 +328,10 @@ export declare const TRANSLATIONS: {
308
328
  readonly select: {
309
329
  readonly accessibleName: "选择";
310
330
  };
331
+ readonly multiSelect: {
332
+ readonly accessibleName: "多选";
333
+ readonly placeholder: "请选择";
334
+ };
311
335
  readonly slider: {
312
336
  readonly accessibleName: "滑块";
313
337
  };
@@ -31,6 +31,10 @@ export declare const de: {
31
31
  readonly select: {
32
32
  readonly accessibleName: "Option auswählen";
33
33
  };
34
+ readonly multiSelect: {
35
+ readonly accessibleName: "Optionen auswählen";
36
+ readonly placeholder: "Bitte wählen";
37
+ };
34
38
  readonly slider: {
35
39
  readonly accessibleName: "Schieberegler";
36
40
  };
@@ -31,6 +31,10 @@ export const de = {
31
31
  select: {
32
32
  accessibleName: 'Option auswählen'
33
33
  },
34
+ multiSelect: {
35
+ accessibleName: 'Optionen auswählen',
36
+ placeholder: 'Bitte wählen'
37
+ },
34
38
  slider: {
35
39
  accessibleName: 'Schieberegler'
36
40
  },
@@ -31,6 +31,10 @@ export declare const en: {
31
31
  readonly select: {
32
32
  readonly accessibleName: "Select option";
33
33
  };
34
+ readonly multiSelect: {
35
+ readonly accessibleName: "Select options";
36
+ readonly placeholder: "Select options";
37
+ };
34
38
  readonly slider: {
35
39
  readonly accessibleName: "Slider";
36
40
  };
@@ -31,6 +31,10 @@ export const en = {
31
31
  select: {
32
32
  accessibleName: 'Select option'
33
33
  },
34
+ multiSelect: {
35
+ accessibleName: 'Select options',
36
+ placeholder: 'Select options'
37
+ },
34
38
  slider: {
35
39
  accessibleName: 'Slider'
36
40
  },
@@ -31,6 +31,10 @@ export declare const es: {
31
31
  readonly select: {
32
32
  readonly accessibleName: "Seleccionar opción";
33
33
  };
34
+ readonly multiSelect: {
35
+ readonly accessibleName: "Seleccionar opciones";
36
+ readonly placeholder: "Seleccione opciones";
37
+ };
34
38
  readonly slider: {
35
39
  readonly accessibleName: "Control deslizante";
36
40
  };
@@ -31,6 +31,10 @@ export const es = {
31
31
  select: {
32
32
  accessibleName: 'Seleccionar opción'
33
33
  },
34
+ multiSelect: {
35
+ accessibleName: 'Seleccionar opciones',
36
+ placeholder: 'Seleccione opciones'
37
+ },
34
38
  slider: {
35
39
  accessibleName: 'Control deslizante'
36
40
  },
@@ -31,6 +31,10 @@ export declare const fr: {
31
31
  readonly select: {
32
32
  readonly accessibleName: "Choisir une option";
33
33
  };
34
+ readonly multiSelect: {
35
+ readonly accessibleName: "Sélectionner des options";
36
+ readonly placeholder: "Sélectionnez des options";
37
+ };
34
38
  readonly slider: {
35
39
  readonly accessibleName: "Curseur";
36
40
  };
@@ -31,6 +31,10 @@ export const fr = {
31
31
  select: {
32
32
  accessibleName: 'Choisir une option'
33
33
  },
34
+ multiSelect: {
35
+ accessibleName: 'Sélectionner des options',
36
+ placeholder: 'Sélectionnez des options'
37
+ },
34
38
  slider: {
35
39
  accessibleName: 'Curseur'
36
40
  },
@@ -31,6 +31,10 @@ export declare const ja: {
31
31
  readonly select: {
32
32
  readonly accessibleName: "選択";
33
33
  };
34
+ readonly multiSelect: {
35
+ readonly accessibleName: "複数選択";
36
+ readonly placeholder: "選択してください";
37
+ };
34
38
  readonly slider: {
35
39
  readonly accessibleName: "スライダー";
36
40
  };
@@ -31,6 +31,10 @@ export const ja = {
31
31
  select: {
32
32
  accessibleName: '選択'
33
33
  },
34
+ multiSelect: {
35
+ accessibleName: '複数選択',
36
+ placeholder: '選択してください'
37
+ },
34
38
  slider: {
35
39
  accessibleName: 'スライダー'
36
40
  },
@@ -31,6 +31,10 @@ export declare const zhCn: {
31
31
  readonly select: {
32
32
  readonly accessibleName: "选择";
33
33
  };
34
+ readonly multiSelect: {
35
+ readonly accessibleName: "多选";
36
+ readonly placeholder: "请选择";
37
+ };
34
38
  readonly slider: {
35
39
  readonly accessibleName: "滑块";
36
40
  };
@@ -31,6 +31,10 @@ export const zhCn = {
31
31
  select: {
32
32
  accessibleName: '选择'
33
33
  },
34
+ multiSelect: {
35
+ accessibleName: '多选',
36
+ placeholder: '请选择'
37
+ },
34
38
  slider: {
35
39
  accessibleName: '滑块'
36
40
  },
package/dist/index.d.ts CHANGED
@@ -16,6 +16,7 @@ export { default as ImageUploader } from './components/ImageUploader.svelte';
16
16
  export { default as Input } from './components/Input.svelte';
17
17
  export { default as LoadingSpinner } from './components/LoadingSpinner.svelte';
18
18
  export { default as Modal } from './components/Modal.svelte';
19
+ export { default as MultiSelect } from './components/MultiSelect.svelte';
19
20
  export { default as Pagination } from './components/Pagination.svelte';
20
21
  export { default as Popup } from './components/Popup.svelte';
21
22
  export { default as PopupMenu } from './components/PopupMenu.svelte';
@@ -55,6 +56,7 @@ export type { ImageUploaderPreviewProps } from './components/ImageUploaderPrevie
55
56
  export type { InputProps } from './components/Input.svelte';
56
57
  export type { LoadingSpinnerProps } from './components/LoadingSpinner.svelte';
57
58
  export type { ModalProps } from './components/Modal.svelte';
59
+ export type { MultiSelectProps } from './components/MultiSelect.svelte';
58
60
  export type { PaginationProps } from './components/Pagination.svelte';
59
61
  export type { PopupProps } from './components/Popup.svelte';
60
62
  export type { PopupMenuProps } from './components/PopupMenu.svelte';
package/dist/index.js CHANGED
@@ -17,6 +17,7 @@ export { default as ImageUploader } from './components/ImageUploader.svelte';
17
17
  export { default as Input } from './components/Input.svelte';
18
18
  export { default as LoadingSpinner } from './components/LoadingSpinner.svelte';
19
19
  export { default as Modal } from './components/Modal.svelte';
20
+ export { default as MultiSelect } from './components/MultiSelect.svelte';
20
21
  export { default as Pagination } from './components/Pagination.svelte';
21
22
  export { default as Popup } from './components/Popup.svelte';
22
23
  export { default as PopupMenu } from './components/PopupMenu.svelte';
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.37",
5
+ "version": "0.0.38",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "svelte",