@14ch/svelte-ui 0.0.1

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.
Files changed (109) hide show
  1. package/README.md +359 -0
  2. package/dist/assets/styles/README.md +144 -0
  3. package/dist/assets/styles/core.scss +61 -0
  4. package/dist/assets/styles/import.scss +11 -0
  5. package/dist/assets/styles/optional/fonts.scss +23 -0
  6. package/dist/assets/styles/optional/reset.scss +230 -0
  7. package/dist/assets/styles/variables.scss +805 -0
  8. package/dist/components/Button.svelte +574 -0
  9. package/dist/components/Button.svelte.d.ts +56 -0
  10. package/dist/components/COMPONENT_DESIGN_GUIDELINES.md +127 -0
  11. package/dist/components/Checkbox.svelte +523 -0
  12. package/dist/components/Checkbox.svelte.d.ts +42 -0
  13. package/dist/components/CheckboxGroup.svelte +82 -0
  14. package/dist/components/CheckboxGroup.svelte.d.ts +13 -0
  15. package/dist/components/ColorPicker.svelte +496 -0
  16. package/dist/components/ColorPicker.svelte.d.ts +45 -0
  17. package/dist/components/Combobox.svelte +576 -0
  18. package/dist/components/Combobox.svelte.d.ts +52 -0
  19. package/dist/components/ConfirmDialog.svelte +116 -0
  20. package/dist/components/ConfirmDialog.svelte.d.ts +20 -0
  21. package/dist/components/Datepicker.svelte +578 -0
  22. package/dist/components/Datepicker.svelte.d.ts +72 -0
  23. package/dist/components/DatepickerCalendar.svelte +925 -0
  24. package/dist/components/DatepickerCalendar.svelte.d.ts +31 -0
  25. package/dist/components/Dialog.svelte +245 -0
  26. package/dist/components/Dialog.svelte.d.ts +38 -0
  27. package/dist/components/Drawer.svelte +383 -0
  28. package/dist/components/Drawer.svelte.d.ts +39 -0
  29. package/dist/components/Fab.svelte +486 -0
  30. package/dist/components/Fab.svelte.d.ts +51 -0
  31. package/dist/components/FileUploader.svelte +456 -0
  32. package/dist/components/FileUploader.svelte.d.ts +36 -0
  33. package/dist/components/Icon.svelte +167 -0
  34. package/dist/components/Icon.svelte.d.ts +21 -0
  35. package/dist/components/IconButton.svelte +557 -0
  36. package/dist/components/IconButton.svelte.d.ts +60 -0
  37. package/dist/components/ImageUploader.svelte +516 -0
  38. package/dist/components/ImageUploader.svelte.d.ts +37 -0
  39. package/dist/components/ImageUploaderPreview.svelte +157 -0
  40. package/dist/components/ImageUploaderPreview.svelte.d.ts +13 -0
  41. package/dist/components/Input.svelte +885 -0
  42. package/dist/components/Input.svelte.d.ts +75 -0
  43. package/dist/components/LoadingSpinner.svelte +116 -0
  44. package/dist/components/LoadingSpinner.svelte.d.ts +10 -0
  45. package/dist/components/Modal.svelte +313 -0
  46. package/dist/components/Modal.svelte.d.ts +34 -0
  47. package/dist/components/Pagination.svelte +276 -0
  48. package/dist/components/Pagination.svelte.d.ts +14 -0
  49. package/dist/components/Popup.svelte +676 -0
  50. package/dist/components/Popup.svelte.d.ts +40 -0
  51. package/dist/components/PopupMenu.svelte +421 -0
  52. package/dist/components/PopupMenu.svelte.d.ts +24 -0
  53. package/dist/components/PopupMenuButton.svelte +365 -0
  54. package/dist/components/PopupMenuButton.svelte.d.ts +42 -0
  55. package/dist/components/Radio.svelte +548 -0
  56. package/dist/components/Radio.svelte.d.ts +42 -0
  57. package/dist/components/RadioGroup.svelte +74 -0
  58. package/dist/components/RadioGroup.svelte.d.ts +14 -0
  59. package/dist/components/Select.svelte +479 -0
  60. package/dist/components/Select.svelte.d.ts +47 -0
  61. package/dist/components/Slider.svelte +473 -0
  62. package/dist/components/Slider.svelte.d.ts +46 -0
  63. package/dist/components/Snackbar.svelte +124 -0
  64. package/dist/components/Snackbar.svelte.d.ts +9 -0
  65. package/dist/components/SnackbarItem.svelte +423 -0
  66. package/dist/components/SnackbarItem.svelte.d.ts +21 -0
  67. package/dist/components/Switch.svelte +454 -0
  68. package/dist/components/Switch.svelte.d.ts +40 -0
  69. package/dist/components/Tab.svelte +193 -0
  70. package/dist/components/Tab.svelte.d.ts +14 -0
  71. package/dist/components/TabItem.svelte +140 -0
  72. package/dist/components/TabItem.svelte.d.ts +17 -0
  73. package/dist/components/Textarea.svelte +702 -0
  74. package/dist/components/Textarea.svelte.d.ts +64 -0
  75. package/dist/components/skeleton/Skeleton.svelte +235 -0
  76. package/dist/components/skeleton/Skeleton.svelte.d.ts +13 -0
  77. package/dist/components/skeleton/SkeletonAvatar.svelte +97 -0
  78. package/dist/components/skeleton/SkeletonAvatar.svelte.d.ts +8 -0
  79. package/dist/components/skeleton/SkeletonBox.svelte +105 -0
  80. package/dist/components/skeleton/SkeletonBox.svelte.d.ts +12 -0
  81. package/dist/components/skeleton/SkeletonButton.svelte +71 -0
  82. package/dist/components/skeleton/SkeletonButton.svelte.d.ts +8 -0
  83. package/dist/components/skeleton/SkeletonHeading.svelte +49 -0
  84. package/dist/components/skeleton/SkeletonHeading.svelte.d.ts +8 -0
  85. package/dist/components/skeleton/SkeletonMedia.svelte +115 -0
  86. package/dist/components/skeleton/SkeletonMedia.svelte.d.ts +9 -0
  87. package/dist/components/skeleton/SkeletonText.svelte +75 -0
  88. package/dist/components/skeleton/SkeletonText.svelte.d.ts +8 -0
  89. package/dist/index.d.ts +42 -0
  90. package/dist/index.js +43 -0
  91. package/dist/types/icon.d.ts +4 -0
  92. package/dist/types/icon.js +2 -0
  93. package/dist/types/menuItem.d.ts +8 -0
  94. package/dist/types/menuItem.js +1 -0
  95. package/dist/types/options.d.ts +6 -0
  96. package/dist/types/options.js +4 -0
  97. package/dist/types/skeleton.d.ts +77 -0
  98. package/dist/types/skeleton.js +19 -0
  99. package/dist/utils/accessibility.d.ts +48 -0
  100. package/dist/utils/accessibility.js +87 -0
  101. package/dist/utils/formatText.d.ts +4 -0
  102. package/dist/utils/formatText.js +44 -0
  103. package/dist/utils/mobile.d.ts +9 -0
  104. package/dist/utils/mobile.js +47 -0
  105. package/dist/utils/snackbar.svelte.d.ts +51 -0
  106. package/dist/utils/snackbar.svelte.js +107 -0
  107. package/dist/utils/style.d.ts +17 -0
  108. package/dist/utils/style.js +22 -0
  109. package/package.json +102 -0
@@ -0,0 +1,456 @@
1
+ <!-- FileUploader.svelte -->
2
+
3
+ <script lang="ts">
4
+ import Icon from './Icon.svelte';
5
+ import IconButton from './IconButton.svelte';
6
+ import { announceToScreenReader } from '../utils/accessibility';
7
+ import { getStyleFromNumber } from '../utils/style';
8
+ import { t } from '../i18n';
9
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
10
+
11
+ // =========================================================================
12
+ // Props, States & Constants
13
+ // =========================================================================
14
+ let {
15
+ // 基本プロパティ
16
+ value = $bindable(),
17
+ multiple = false,
18
+ maxFileSize = 5 * 1024 * 1024,
19
+ placeholder = t('fileUploader.placeholder'),
20
+
21
+ // HTML属性系
22
+ id = `file-uploader-${Math.random().toString(36).substring(2, 15)}`,
23
+ accept = '',
24
+
25
+ // スタイル/レイアウト
26
+ width = undefined,
27
+ height = undefined,
28
+ rounded = false,
29
+
30
+ // アイコン系
31
+ icon = 'upload',
32
+ iconSize = 32,
33
+ iconFilled = false,
34
+ iconWeight = 300,
35
+ iconGrade = 0,
36
+ iconOpticalSize = iconSize,
37
+ iconVariant = 'outlined',
38
+ removeFileAriaLabel = t('fileUploader.removeFile'),
39
+
40
+ // 入力イベント
41
+ onchange = () => {}, // No params for type inference
42
+
43
+ // フォーカスイベント
44
+ onfocus = () => {}, // No params for type inference
45
+ onblur = () => {}, // No params for type inference
46
+
47
+ // キーボードイベント
48
+ onkeydown = () => {}, // No params for type inference
49
+ onkeyup = () => {}, // No params for type inference
50
+
51
+ // マウスイベント
52
+ onmouseenter = () => {}, // No params for type inference
53
+ onmouseleave = () => {}, // No params for type inference
54
+
55
+ // タッチイベント
56
+ ontouchstart = () => {}, // No params for type inference
57
+ ontouchend = () => {}, // No params for type inference
58
+
59
+ // ポインターイベント
60
+ onpointerenter = () => {}, // No params for type inference
61
+ onpointerleave = () => {} // No params for type inference
62
+ }: {
63
+ // 基本プロパティ
64
+ value: FileList | undefined;
65
+ multiple?: boolean;
66
+ maxFileSize?: number;
67
+ placeholder?: string;
68
+
69
+ // HTML属性系
70
+ id?: string;
71
+ accept?: string;
72
+
73
+ // スタイル/レイアウト
74
+ width?: string | number;
75
+ height?: string | number;
76
+ rounded?: boolean;
77
+
78
+ // アイコン系
79
+ icon?: string;
80
+ iconSize?: number;
81
+ iconFilled?: boolean;
82
+ iconWeight?: IconWeight;
83
+ iconGrade?: IconGrade;
84
+ iconOpticalSize?: IconOpticalSize;
85
+ iconVariant?: IconVariant;
86
+ removeFileAriaLabel?: string;
87
+
88
+ // 入力イベント
89
+ onchange?: (value: FileList | null) => void;
90
+
91
+ // フォーカスイベント
92
+ onfocus?: Function; // No params for type inference
93
+ onblur?: Function; // No params for type inference
94
+
95
+ // キーボードイベント
96
+ onkeydown?: Function; // No params for type inference
97
+ onkeyup?: Function; // No params for type inference
98
+
99
+ // マウスイベント
100
+ onmouseenter?: Function; // No params for type inference
101
+ onmouseleave?: Function; // No params for type inference
102
+
103
+ // タッチイベント
104
+ ontouchstart?: Function; // No params for type inference
105
+ ontouchend?: Function; // No params for type inference
106
+
107
+ // ポインターイベント
108
+ onpointerenter?: Function; // No params for type inference
109
+ onpointerleave?: Function; // No params for type inference
110
+ } = $props();
111
+
112
+ let dropAreaRef: HTMLButtonElement;
113
+ let fileInputRef: HTMLInputElement;
114
+ let isHover: boolean = $state(false);
115
+ let errorMessage: string = $state('');
116
+
117
+ // =========================================================================
118
+ // Effects
119
+ // =========================================================================
120
+ $effect(() => {
121
+ if (value && value.length > 0) {
122
+ const fileCount = value.length;
123
+ const fileNames = Array.from(value)
124
+ .map((file) => file.name)
125
+ .join(', ');
126
+ announceToScreenReader(`${fileCount} file${fileCount > 1 ? 's' : ''} selected: ${fileNames}`);
127
+ }
128
+ });
129
+
130
+ // =========================================================================
131
+ // Methods
132
+ // =========================================================================
133
+ const handleClick = () => {
134
+ fileInputRef?.click();
135
+ };
136
+
137
+ const handleFocus = (event: FocusEvent) => {
138
+ onfocus?.(event);
139
+ };
140
+
141
+ const handleBlur = (event: FocusEvent) => {
142
+ onblur?.(event);
143
+ };
144
+
145
+ const handleKeyDown = (event: KeyboardEvent) => {
146
+ onkeydown?.(event);
147
+ if (event.key === 'Enter' || event.key === ' ') {
148
+ event.preventDefault();
149
+ handleClick();
150
+ }
151
+ };
152
+
153
+ const handleKeyUp = (event: KeyboardEvent) => {
154
+ onkeyup?.(event);
155
+ };
156
+
157
+ const handleMouseEnter = (event: MouseEvent) => {
158
+ onmouseenter?.(event);
159
+ };
160
+
161
+ const handleMouseLeave = (event: MouseEvent) => {
162
+ onmouseleave?.(event);
163
+ };
164
+
165
+ const handleTouchStart = (event: TouchEvent) => {
166
+ ontouchstart?.(event);
167
+ };
168
+
169
+ const handleTouchEnd = (event: TouchEvent) => {
170
+ ontouchend?.(event);
171
+ };
172
+
173
+ const handlePointerEnter = (event: PointerEvent) => {
174
+ onpointerenter?.(event);
175
+ };
176
+
177
+ const handlePointerLeave = (event: PointerEvent) => {
178
+ onpointerleave?.(event);
179
+ };
180
+
181
+ const removeFile = (index: number) => {
182
+ if (!value) return;
183
+
184
+ const dt = new DataTransfer();
185
+ for (let i = 0; i < value.length; i++) {
186
+ if (i !== index) {
187
+ dt.items.add(value[i]);
188
+ }
189
+ }
190
+ value = dt.files.length > 0 ? dt.files : undefined;
191
+ };
192
+
193
+ const validateFile = (file: File): boolean => {
194
+ if (file.size > maxFileSize) {
195
+ errorMessage = t('fileUploader.maxFileSizeError', {
196
+ maxSize: (maxFileSize / 1024 / 1024).toFixed(1)
197
+ });
198
+ return false;
199
+ }
200
+
201
+ errorMessage = '';
202
+ return true;
203
+ };
204
+
205
+ const handleFileChange = (fileList: FileList | null) => {
206
+ if (!fileList) return;
207
+
208
+ const validFiles: File[] = [];
209
+ for (let i = 0; i < fileList.length; i++) {
210
+ const file = fileList[i];
211
+ if (validateFile(file)) {
212
+ validFiles.push(file);
213
+ }
214
+ // 無効なファイルは単純にスキップ
215
+ }
216
+
217
+ // 有効なファイルがある場合のみ更新
218
+ if (validFiles.length > 0) {
219
+ const dataTransfer = new DataTransfer();
220
+
221
+ // multipleの場合は既存のファイルを保持して追加
222
+ if (multiple && value) {
223
+ for (let i = 0; i < value.length; i++) {
224
+ dataTransfer.items.add(value[i]);
225
+ }
226
+ }
227
+
228
+ // 新しく選択されたファイルのみを追加
229
+ validFiles.forEach((file) => dataTransfer.items.add(file));
230
+ value = dataTransfer.files;
231
+ onchange(value);
232
+ }
233
+ };
234
+
235
+ export const reset = () => {
236
+ if (fileInputRef) {
237
+ fileInputRef.value = '';
238
+ value = undefined;
239
+ errorMessage = '';
240
+ }
241
+ };
242
+
243
+ // =========================================================================
244
+ // $derived
245
+ // =========================================================================
246
+ const widthStyle = $derived(getStyleFromNumber(width) || '100%');
247
+ </script>
248
+
249
+ <button
250
+ bind:this={dropAreaRef}
251
+ class="file-uploader"
252
+ class:file-uploader--hover={isHover}
253
+ class:rounded
254
+ style="
255
+ --file-uploader-width: {widthStyle};
256
+ --file-uploader-height: {height}px
257
+ "
258
+ data-testid="file-uploader"
259
+ onclick={handleClick}
260
+ onfocus={handleFocus}
261
+ onblur={handleBlur}
262
+ onkeydown={handleKeyDown}
263
+ onkeyup={handleKeyUp}
264
+ onmouseenter={handleMouseEnter}
265
+ onmouseleave={handleMouseLeave}
266
+ ontouchstart={handleTouchStart}
267
+ ontouchend={handleTouchEnd}
268
+ onpointerenter={handlePointerEnter}
269
+ onpointerleave={handlePointerLeave}
270
+ ondragover={(event) => {
271
+ event.stopPropagation();
272
+ event.preventDefault();
273
+ isHover = true;
274
+ }}
275
+ ondragleave={(event) => {
276
+ event.stopPropagation();
277
+ event.preventDefault();
278
+ isHover = false;
279
+ }}
280
+ ondrop={(event) => {
281
+ event.stopPropagation();
282
+ event.preventDefault();
283
+ isHover = false;
284
+ const fileList = event.dataTransfer?.files;
285
+ if (fileList) {
286
+ handleFileChange(fileList);
287
+ }
288
+ }}
289
+ aria-label={t('fileUploader.uploadFile')}
290
+ aria-describedby={`${id}-help`}
291
+ >
292
+ {#if value && value.length > 0}
293
+ <div class="file-uploader__description file-uploader__description--with-file">
294
+ <Icon
295
+ size={iconSize}
296
+ filled={iconFilled}
297
+ weight={iconWeight}
298
+ grade={iconGrade}
299
+ opticalSize={iconOpticalSize}
300
+ variant={iconVariant}>{icon}</Icon
301
+ >
302
+ </div>
303
+ <ul class="file-uploader__file-list">
304
+ {#each value as file, index}
305
+ <li class="file-uploader__file-list-item">
306
+ {file.name}
307
+ <IconButton
308
+ size={24}
309
+ iconFilled={true}
310
+ iconWeight={300}
311
+ color="var(--svelte-ui-text-color)"
312
+ onclick={(e) => {
313
+ e.stopPropagation();
314
+ removeFile(index);
315
+ }}
316
+ ariaLabel={removeFileAriaLabel}
317
+ tabindex={-1}
318
+ >
319
+ cancel
320
+ </IconButton>
321
+ </li>
322
+ {/each}
323
+ </ul>
324
+ {:else}
325
+ <div class="file-uploader__description">
326
+ <Icon
327
+ size={iconSize}
328
+ filled={iconFilled}
329
+ weight={iconWeight}
330
+ grade={iconGrade}
331
+ opticalSize={iconOpticalSize}
332
+ variant={iconVariant}>{icon}</Icon
333
+ >
334
+ {@html placeholder}
335
+ </div>
336
+ {/if}
337
+
338
+ {#if errorMessage}
339
+ <div class="file-uploader__error-message" role="alert" aria-live="polite">
340
+ {errorMessage}
341
+ </div>
342
+ {/if}
343
+
344
+ <input
345
+ bind:this={fileInputRef}
346
+ {accept}
347
+ {multiple}
348
+ class="file-uploader__input"
349
+ {id}
350
+ type="file"
351
+ onchange={(event) => {
352
+ const target = event.target as HTMLInputElement;
353
+ if (target.files && target.files.length > 0) {
354
+ handleFileChange(target.files);
355
+ }
356
+ }}
357
+ />
358
+ </button>
359
+
360
+ <style>
361
+ .file-uploader {
362
+ display: flex;
363
+ flex-direction: column;
364
+ justify-content: center;
365
+ align-items: center;
366
+ gap: 16px;
367
+ position: relative;
368
+ width: var(--file-uploader-width, 100%);
369
+ height: var(--file-uploader-height);
370
+ min-height: 100px;
371
+ padding: 16px;
372
+ background-color: var(--svelte-ui-file-uploader-bg);
373
+ border-radius: var(--svelte-ui-border-radius);
374
+ }
375
+
376
+ .file-uploader::before {
377
+ content: '';
378
+ display: block;
379
+ position: absolute;
380
+ top: 0;
381
+ left: 0;
382
+ width: 100%;
383
+ height: 100%;
384
+ background-color: transparent;
385
+ border: var(--svelte-ui-file-uploader-border-style) var(--svelte-ui-file-uploader-border-width)
386
+ var(--svelte-ui-file-uploader-border-color);
387
+ border-radius: var(--svelte-ui-border-radius);
388
+ transition-property: background-color border-color;
389
+ transition-duration: var(--svelte-ui-transition-duration);
390
+ }
391
+
392
+ .file-uploader.rounded {
393
+ border-radius: var(--svelte-ui-border-radius-rounded);
394
+ }
395
+
396
+ @media (hover: hover) {
397
+ .file-uploader:hover::before,
398
+ .file-uploader--hover::before {
399
+ background-color: var(--svelte-ui-file-uploader-hover-bg);
400
+ border-color: var(--svelte-ui-file-uploader-hover-border-color);
401
+ }
402
+ }
403
+
404
+ .file-uploader:focus-visible {
405
+ outline: var(--svelte-ui-focus-outline-inner);
406
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner);
407
+ }
408
+
409
+ .file-uploader__description {
410
+ display: flex;
411
+ flex-direction: column;
412
+ align-items: center;
413
+ gap: 16px;
414
+ color: var(--svelte-ui-text-subtle-color);
415
+ }
416
+
417
+ .file-uploader__input {
418
+ display: none;
419
+ }
420
+
421
+ @media (hover: hover) {
422
+ .file-uploader:hover .file-uploader__description,
423
+ .file-uploader--hover .file-uploader__description {
424
+ color: var(--svelte-ui-file-uploader-hover-color);
425
+ }
426
+ }
427
+
428
+ .file-uploader__error-message {
429
+ margin-top: 8px;
430
+ padding: 8px 12px;
431
+ background-color: var(--svelte-ui-error-container-color);
432
+ color: var(--svelte-ui-error-color);
433
+ border-radius: var(--svelte-ui-border-radius);
434
+ font-size: var(--svelte-ui-font-size-sm);
435
+ }
436
+
437
+ .file-uploader__file-list {
438
+ display: flex;
439
+ justify-content: center;
440
+ flex-wrap: wrap;
441
+ gap: 8px;
442
+ list-style: none;
443
+ padding: 0;
444
+ margin: 0;
445
+ width: 100%;
446
+ }
447
+
448
+ .file-uploader__file-list-item {
449
+ display: flex;
450
+ align-items: center;
451
+ gap: 8px;
452
+ padding: 4px 4px 4px 12px;
453
+ background-color: var(--svelte-ui-file-uploader-item-bg);
454
+ border-radius: var(--svelte-ui-border-radius-rounded);
455
+ }
456
+ </style>
@@ -0,0 +1,36 @@
1
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
2
+ type $$ComponentProps = {
3
+ value: FileList | undefined;
4
+ multiple?: boolean;
5
+ maxFileSize?: number;
6
+ placeholder?: string;
7
+ id?: string;
8
+ accept?: string;
9
+ width?: string | number;
10
+ height?: string | number;
11
+ rounded?: boolean;
12
+ icon?: string;
13
+ iconSize?: number;
14
+ iconFilled?: boolean;
15
+ iconWeight?: IconWeight;
16
+ iconGrade?: IconGrade;
17
+ iconOpticalSize?: IconOpticalSize;
18
+ iconVariant?: IconVariant;
19
+ removeFileAriaLabel?: string;
20
+ onchange?: (value: FileList | null) => void;
21
+ onfocus?: Function;
22
+ onblur?: Function;
23
+ onkeydown?: Function;
24
+ onkeyup?: Function;
25
+ onmouseenter?: Function;
26
+ onmouseleave?: Function;
27
+ ontouchstart?: Function;
28
+ ontouchend?: Function;
29
+ onpointerenter?: Function;
30
+ onpointerleave?: Function;
31
+ };
32
+ declare const FileUploader: import("svelte").Component<$$ComponentProps, {
33
+ reset: () => void;
34
+ }, "value">;
35
+ type FileUploader = ReturnType<typeof FileUploader>;
36
+ export default FileUploader;
@@ -0,0 +1,167 @@
1
+ <!-- Icon.svelte -->
2
+
3
+ <script lang="ts">
4
+ import type { Snippet } from 'svelte';
5
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
6
+ import { getStyleFromNumber } from '../utils/style';
7
+
8
+ // =========================================================================
9
+ // Props, States & Constants
10
+ // =========================================================================
11
+ let {
12
+ // Snippet
13
+ children,
14
+
15
+ // 基本プロパティ
16
+ title,
17
+ fallbackText,
18
+
19
+ // スタイル/レイアウト
20
+ size = 24,
21
+ color = '',
22
+ customStyle = '',
23
+
24
+ // アイコン関連
25
+ filled = false,
26
+ weight = 300,
27
+ grade = 0,
28
+ opticalSize = size,
29
+ variant = 'outlined',
30
+
31
+ // ARIA/アクセシビリティ
32
+ ariaLabel,
33
+ decorative = true,
34
+
35
+ // その他
36
+ ...restProps
37
+ }: {
38
+ // Snippet
39
+ children: Snippet;
40
+
41
+ // 基本プロパティ
42
+ title?: string;
43
+ fallbackText?: string;
44
+
45
+ // スタイル/レイアウト
46
+ size?: number;
47
+ color?: string;
48
+ customStyle?: string;
49
+
50
+ // アイコン関連
51
+ filled?: boolean;
52
+ weight?: IconWeight;
53
+ grade?: IconGrade;
54
+ opticalSize?: IconOpticalSize;
55
+ variant?: IconVariant;
56
+
57
+ // ARIA/アクセシビリティ
58
+ ariaLabel?: string;
59
+ decorative?: boolean;
60
+
61
+ // その他
62
+ [key: string]: any;
63
+ } = $props();
64
+
65
+ // =========================================================================
66
+ // $derived
67
+ // =========================================================================
68
+ const iconClasses = $derived(`material-symbols-${variant}`);
69
+
70
+ const fontVariationSettings = $derived(
71
+ `'FILL' ${filled ? 1 : 0}, 'wght' ${weight}, 'GRAD' ${grade}, 'opsz' ${opticalSize}`
72
+ );
73
+
74
+ const ariaAttributes = $derived({
75
+ 'aria-hidden': decorative && !ariaLabel ? true : undefined,
76
+ 'aria-label': ariaLabel || undefined,
77
+ role: !decorative && ariaLabel ? 'img' : undefined
78
+ });
79
+
80
+ const iconStyle = $derived(
81
+ `width: ${size}px; height: ${size}px; font-size: ${size}px;
82
+ color: ${color}; line-height: 1;
83
+ font-variation-settings: ${fontVariationSettings};
84
+ ${customStyle}`
85
+ );
86
+ </script>
87
+
88
+ <i
89
+ class={iconClasses}
90
+ style={iconStyle}
91
+ {title}
92
+ {...ariaAttributes}
93
+ {...restProps}
94
+ data-testid="icon"
95
+ >
96
+ {@render children()}
97
+ </i>
98
+
99
+ {#if fallbackText}
100
+ <!-- Unicode文字での代替表示 -->
101
+ <span
102
+ class="icon-fallback-text"
103
+ style="width: {size}px; height: {size}px; font-size: {size}px; {customStyle}"
104
+ {...ariaAttributes}
105
+ {...restProps}
106
+ data-testid="icon-fallback"
107
+ >
108
+ {fallbackText}
109
+ </span>
110
+ {/if}
111
+
112
+ <style>
113
+ .material-symbols-outlined,
114
+ .material-symbols-filled,
115
+ .material-symbols-rounded,
116
+ .material-symbols-sharp {
117
+ display: block;
118
+ font-family: 'Material Symbols Outlined';
119
+ font-size: inherit;
120
+ color: inherit;
121
+ line-height: inherit;
122
+ text-transform: none;
123
+ letter-spacing: normal;
124
+ word-wrap: normal;
125
+ white-space: nowrap;
126
+ direction: ltr;
127
+ transition-property: color, transform;
128
+ transition-duration: var(--svelte-ui-transition-duration);
129
+ transition-timing-function: ease;
130
+ }
131
+
132
+ /* font-variation-settings are controlled via inline styles for dynamic props */
133
+
134
+ .icon-fallback-text {
135
+ display: inline-flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ text-align: center;
139
+ line-height: 1;
140
+ color: inherit;
141
+ transition-property: color, transform;
142
+ transition-duration: var(--svelte-ui-transition-duration);
143
+ transition-timing-function: ease;
144
+ }
145
+
146
+ /* Prefers reduced motion */
147
+ @media (prefers-reduced-motion: reduce) {
148
+ .material-symbols-outlined,
149
+ .material-symbols-filled,
150
+ .material-symbols-rounded,
151
+ .material-symbols-sharp,
152
+ .icon-fallback-text {
153
+ transition: none;
154
+ }
155
+ }
156
+
157
+ /* Print styles */
158
+ @media print {
159
+ .material-symbols-outlined,
160
+ .material-symbols-filled,
161
+ .material-symbols-rounded,
162
+ .material-symbols-sharp,
163
+ .icon-fallback-text {
164
+ color: black !important;
165
+ }
166
+ }
167
+ </style>
@@ -0,0 +1,21 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
+ type $$ComponentProps = {
4
+ children: Snippet;
5
+ title?: string;
6
+ fallbackText?: string;
7
+ size?: number;
8
+ color?: string;
9
+ customStyle?: string;
10
+ filled?: boolean;
11
+ weight?: IconWeight;
12
+ grade?: IconGrade;
13
+ opticalSize?: IconOpticalSize;
14
+ variant?: IconVariant;
15
+ ariaLabel?: string;
16
+ decorative?: boolean;
17
+ [key: string]: any;
18
+ };
19
+ declare const Icon: import("svelte").Component<$$ComponentProps, {}, "">;
20
+ type Icon = ReturnType<typeof Icon>;
21
+ export default Icon;