@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,516 @@
1
+ <!-- ImageUploader.svelte -->
2
+
3
+ <script lang="ts">
4
+ import Icon from './Icon.svelte';
5
+ import { t } from '../i18n';
6
+ import { announceToScreenReader } from '../utils/accessibility';
7
+ import { getStyleFromNumber } from '../utils/style';
8
+ import { onDestroy } from 'svelte';
9
+ import ImageUploaderPreview from './ImageUploaderPreview.svelte';
10
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
11
+
12
+ // =========================================================================
13
+ // Props, States & Constants
14
+ // =========================================================================
15
+ let {
16
+ // 基本プロパティ
17
+ value = $bindable(),
18
+ multiple = false,
19
+ maxFileSize = 5 * 1024 * 1024,
20
+ placeholder = '',
21
+
22
+ // HTML属性系
23
+ id = `image-uploader-${Math.random().toString(36).substring(2, 15)}`,
24
+ accept = '.jpg,.jpeg,.png,.gif,.webp,.svg',
25
+
26
+ // スタイル/レイアウト
27
+ width = '120px',
28
+ height = '120px',
29
+ rounded = false,
30
+
31
+ // アイコン系
32
+ icon = 'add_photo_alternate',
33
+ iconSize = 32,
34
+ iconFilled = false,
35
+ iconWeight = 300,
36
+ iconGrade = 0,
37
+ iconOpticalSize = iconSize,
38
+ iconVariant = 'outlined',
39
+ removeFileAriaLabel = t('imageUploader.removeFile'),
40
+
41
+ // 状態/動作
42
+ adaptiveSize = false,
43
+
44
+ // 入力イベント
45
+ onchange = () => {}, // No params for type inference
46
+
47
+ // フォーカスイベント
48
+ onfocus = () => {}, // No params for type inference
49
+ onblur = () => {}, // No params for type inference
50
+
51
+ // キーボードイベント
52
+ onkeydown = () => {}, // No params for type inference
53
+ onkeyup = () => {}, // No params for type inference
54
+
55
+ // マウスイベント
56
+ onmouseenter = () => {}, // No params for type inference
57
+ onmouseleave = () => {}, // No params for type inference
58
+
59
+ // タッチイベント
60
+ ontouchstart = () => {}, // No params for type inference
61
+ ontouchend = () => {}, // No params for type inference
62
+
63
+ // ポインターイベント
64
+ onpointerenter = () => {}, // No params for type inference
65
+ onpointerleave = () => {} // No params for type inference
66
+ }: {
67
+ // 基本プロパティ
68
+ value: FileList | undefined;
69
+ multiple?: boolean;
70
+ maxFileSize?: number;
71
+ placeholder?: string;
72
+
73
+ // HTML属性系
74
+ id?: string;
75
+ accept?: string;
76
+
77
+ // スタイル/レイアウト
78
+
79
+ width?: string | number;
80
+ height?: string | number;
81
+ rounded?: boolean;
82
+ adaptiveSize?: boolean;
83
+
84
+ // アイコン系
85
+ icon?: string;
86
+ iconSize?: number;
87
+ iconFilled?: boolean;
88
+ iconWeight?: IconWeight;
89
+ iconGrade?: IconGrade;
90
+ iconOpticalSize?: IconOpticalSize;
91
+ iconVariant?: IconVariant;
92
+ removeFileAriaLabel?: string;
93
+
94
+ // 入力イベント
95
+ onchange?: (value: FileList | null) => void;
96
+
97
+ // フォーカスイベント
98
+ onfocus?: Function; // No params for type inference
99
+ onblur?: Function; // No params for type inference
100
+
101
+ // キーボードイベント
102
+ onkeydown?: Function; // No params for type inference
103
+ onkeyup?: Function; // No params for type inference
104
+
105
+ // マウスイベント
106
+ onmouseenter?: Function; // No params for type inference
107
+ onmouseleave?: Function; // No params for type inference
108
+
109
+ // タッチイベント
110
+ ontouchstart?: Function; // No params for type inference
111
+ ontouchend?: Function; // No params for type inference
112
+
113
+ // ポインターイベント
114
+ onpointerenter?: Function; // No params for type inference
115
+ onpointerleave?: Function; // No params for type inference
116
+ } = $props();
117
+
118
+ let dropAreaRef: HTMLButtonElement;
119
+ let fileInputRef: HTMLInputElement;
120
+ let isHover: boolean = $state(false);
121
+ let errorMessage: string = $state('');
122
+ let urlCache = new Map<File, string>();
123
+
124
+ // =========================================================================
125
+ // Lifecycle
126
+ // =========================================================================
127
+
128
+ onDestroy(() => {
129
+ cleanupObjectUrls();
130
+ });
131
+
132
+ // =========================================================================
133
+ // Effects
134
+ // =========================================================================
135
+ $effect(() => {
136
+ if (value && value.length > 0) {
137
+ const fileCount = value.length;
138
+ const fileNames = Array.from(value)
139
+ .map((file) => file.name)
140
+ .join(', ');
141
+ announceToScreenReader(
142
+ `${fileCount} image${fileCount > 1 ? 's' : ''} selected: ${fileNames}`
143
+ );
144
+ }
145
+ });
146
+
147
+ // =========================================================================
148
+ // Methods
149
+ // =========================================================================
150
+ const handleClick = () => {
151
+ fileInputRef?.click();
152
+ };
153
+
154
+ const handleFocus = (event: FocusEvent) => {
155
+ onfocus?.(event);
156
+ };
157
+
158
+ const handleBlur = (event: FocusEvent) => {
159
+ onblur?.(event);
160
+ };
161
+
162
+ const handleKeyDown = (event: KeyboardEvent) => {
163
+ onkeydown?.(event);
164
+ if (event.key === 'Enter' || event.key === ' ') {
165
+ event.preventDefault();
166
+ handleClick();
167
+ }
168
+ };
169
+
170
+ const handleKeyUp = (event: KeyboardEvent) => {
171
+ onkeyup?.(event);
172
+ };
173
+
174
+ const handleMouseEnter = (event: MouseEvent) => {
175
+ onmouseenter?.(event);
176
+ };
177
+
178
+ const handleMouseLeave = (event: MouseEvent) => {
179
+ onmouseleave?.(event);
180
+ };
181
+
182
+ const handleTouchStart = (event: TouchEvent) => {
183
+ ontouchstart?.(event);
184
+ };
185
+
186
+ const handleTouchEnd = (event: TouchEvent) => {
187
+ ontouchend?.(event);
188
+ };
189
+
190
+ const handlePointerEnter = (event: PointerEvent) => {
191
+ onpointerenter?.(event);
192
+ };
193
+
194
+ const handlePointerLeave = (event: PointerEvent) => {
195
+ onpointerleave?.(event);
196
+ };
197
+
198
+ const validateFile = (file: File): boolean => {
199
+ // サポートされる画像タイプ
200
+ const supportedTypes = [
201
+ 'image/jpeg',
202
+ 'image/jpg',
203
+ 'image/png',
204
+ 'image/gif',
205
+ 'image/webp',
206
+ 'image/svg+xml'
207
+ ];
208
+
209
+ if (!supportedTypes.includes(file.type)) {
210
+ errorMessage = t('imageUploader.unsupportedFileFormat');
211
+ return false;
212
+ }
213
+
214
+ if (file.size > maxFileSize) {
215
+ errorMessage = t('imageUploader.fileSizeExceeded', {
216
+ maxSize: (maxFileSize / 1024 / 1024).toFixed(1)
217
+ });
218
+ return false;
219
+ }
220
+
221
+ errorMessage = '';
222
+ return true;
223
+ };
224
+
225
+ const handleFileChange = (fileList: FileList | null) => {
226
+ if (!fileList) return;
227
+
228
+ const validFiles: File[] = [];
229
+ for (let i = 0; i < fileList.length; i++) {
230
+ const file = fileList[i];
231
+ if (validateFile(file)) {
232
+ validFiles.push(file);
233
+ }
234
+ // 無効なファイルは単純にスキップ
235
+ }
236
+
237
+ // 有効なファイルがある場合のみ更新
238
+ if (validFiles.length > 0) {
239
+ const dataTransfer = new DataTransfer();
240
+
241
+ // multipleの場合は既存のファイルを保持して追加
242
+ if (multiple && value) {
243
+ for (let i = 0; i < value.length; i++) {
244
+ dataTransfer.items.add(value[i]);
245
+ }
246
+ }
247
+
248
+ // 新しく選択されたファイルのみを追加
249
+ validFiles.forEach((file) => dataTransfer.items.add(file));
250
+ value = dataTransfer.files;
251
+ onchange(value);
252
+ }
253
+ };
254
+
255
+ const removeFile = (index: number) => {
256
+ if (!value) return;
257
+
258
+ // 削除されるファイルのURLをクリーンアップ
259
+ const fileToRemove = value[index];
260
+ if (fileToRemove && urlCache.has(fileToRemove)) {
261
+ const url = urlCache.get(fileToRemove)!;
262
+ URL.revokeObjectURL(url);
263
+ urlCache.delete(fileToRemove);
264
+ }
265
+
266
+ const dt = new DataTransfer();
267
+ for (let i = 0; i < value.length; i++) {
268
+ if (i !== index) {
269
+ dt.items.add(value[i]);
270
+ }
271
+ }
272
+ if (dt.files.length > 0) {
273
+ value = dt.files;
274
+ } else {
275
+ // 空の場合は空のFileListを作成
276
+ const emptyDt = new DataTransfer();
277
+ value = emptyDt.files;
278
+ }
279
+ };
280
+
281
+ const cleanupObjectUrls = () => {
282
+ // キャッシュされたすべてのURLを解放
283
+ for (const url of urlCache.values()) {
284
+ URL.revokeObjectURL(url);
285
+ }
286
+ urlCache.clear();
287
+ };
288
+
289
+ export const reset = () => {
290
+ if (fileInputRef) {
291
+ fileInputRef.value = '';
292
+ value = undefined;
293
+ errorMessage = '';
294
+ cleanupObjectUrls();
295
+ }
296
+ };
297
+
298
+ // =========================================================================
299
+ // $derived
300
+ // =========================================================================
301
+ const previewWidthStyle = $derived(getStyleFromNumber(width));
302
+ const previewHeightStyle = $derived(getStyleFromNumber(height));
303
+ </script>
304
+
305
+ {#snippet preview(file: File, index: number)}
306
+ <ImageUploaderPreview
307
+ {file}
308
+ {width}
309
+ {height}
310
+ {adaptiveSize}
311
+ {rounded}
312
+ id={id ? `${id}-preview-${index}` : undefined}
313
+ {removeFileAriaLabel}
314
+ onRemove={() => removeFile(index)}
315
+ />
316
+ {/snippet}
317
+
318
+ <div
319
+ class="image-uploader"
320
+ class:image-uploader--multiple={multiple}
321
+ class:image-uploader--rounded={rounded}
322
+ class:image-uploader--adaptive={adaptiveSize}
323
+ style="
324
+ --image-uploader-button-width: {previewWidthStyle};
325
+ --image-uploader-button-height: {previewHeightStyle};
326
+ "
327
+ data-testid="image-uploader"
328
+ >
329
+ {#if multiple}
330
+ {#each value as file, index}
331
+ {@render preview(file, index)}
332
+ {/each}
333
+ {/if}
334
+ <button
335
+ bind:this={dropAreaRef}
336
+ class="image-uploader__button"
337
+ class:image-uploader__button--hover={isHover}
338
+ class:image-uploader__button--has-images={!multiple && value && value.length > 0}
339
+ class:image-uploader__button--rounded={rounded}
340
+ onclick={handleClick}
341
+ onfocus={handleFocus}
342
+ onblur={handleBlur}
343
+ onkeydown={handleKeyDown}
344
+ onkeyup={handleKeyUp}
345
+ onmouseenter={handleMouseEnter}
346
+ onmouseleave={handleMouseLeave}
347
+ ontouchstart={handleTouchStart}
348
+ ontouchend={handleTouchEnd}
349
+ onpointerenter={handlePointerEnter}
350
+ onpointerleave={handlePointerLeave}
351
+ ondragover={(event) => {
352
+ event.stopPropagation();
353
+ event.preventDefault();
354
+ isHover = true;
355
+ }}
356
+ ondragleave={(event) => {
357
+ event.stopPropagation();
358
+ event.preventDefault();
359
+ isHover = false;
360
+ }}
361
+ ondrop={(event) => {
362
+ event.stopPropagation();
363
+ event.preventDefault();
364
+ isHover = false;
365
+ const fileList = event.dataTransfer?.files;
366
+ if (fileList) {
367
+ handleFileChange(fileList);
368
+ }
369
+ }}
370
+ aria-label={t('imageUploader.uploadImage')}
371
+ >
372
+ {#if !multiple && value && value.length > 0}
373
+ {@render preview(value[0], 0)}
374
+ {:else}
375
+ <div class="image-uploader__description">
376
+ <Icon
377
+ size={iconSize}
378
+ filled={iconFilled}
379
+ weight={iconWeight}
380
+ grade={iconGrade}
381
+ opticalSize={iconOpticalSize}
382
+ variant={iconVariant}>{icon}</Icon
383
+ >
384
+ {@html placeholder}
385
+ </div>
386
+ {/if}
387
+ </button>
388
+
389
+ {#if errorMessage}
390
+ <div class="image-uploader__error-message" role="alert" aria-live="polite">
391
+ {errorMessage}
392
+ </div>
393
+ {/if}
394
+
395
+ <input
396
+ bind:this={fileInputRef}
397
+ {accept}
398
+ {multiple}
399
+ class="image-uploader__input"
400
+ {id}
401
+ type="file"
402
+ onchange={(event) => {
403
+ const target = event.target as HTMLInputElement;
404
+ if (target.files && target.files.length > 0) {
405
+ handleFileChange(target.files);
406
+ }
407
+ }}
408
+ />
409
+ </div>
410
+
411
+ <style>
412
+ .image-uploader {
413
+ position: relative;
414
+ width: 100%;
415
+ }
416
+
417
+ .image-uploader--multiple {
418
+ display: flex;
419
+ flex-wrap: wrap;
420
+ gap: var(--svelte-ui-image-uploader-preview-gap);
421
+ }
422
+
423
+ .image-uploader__button {
424
+ display: flex;
425
+ flex-direction: column;
426
+ justify-content: center;
427
+ align-items: center;
428
+ position: relative;
429
+ max-width: 100%;
430
+ min-width: var(--image-uploader-button-width);
431
+ min-height: var(--image-uploader-button-height);
432
+ padding: 16px;
433
+ background-color: var(--svelte-ui-form-bg);
434
+ border-radius: var(--svelte-ui-border-radius);
435
+ cursor: pointer;
436
+ transition: background-color var(--svelte-ui-transition-duration);
437
+ }
438
+
439
+ .image-uploader__button::before {
440
+ content: '';
441
+ display: block;
442
+ position: absolute;
443
+ top: 0;
444
+ left: 0;
445
+ width: 100%;
446
+ height: 100%;
447
+ background-color: transparent;
448
+ border: var(--svelte-ui-image-uploader-border-style)
449
+ var(--svelte-ui-image-uploader-border-width) var(--svelte-ui-image-uploader-border-color);
450
+ border-radius: var(--svelte-ui-border-radius);
451
+ transition-property: background-color border-color;
452
+ transition-duration: var(--svelte-ui-transition-duration);
453
+ }
454
+
455
+ .image-uploader__button--rounded,
456
+ .image-uploader__button--rounded::before {
457
+ border-radius: var(--svelte-ui-border-radius-rounded);
458
+ }
459
+
460
+ @media (hover: hover) {
461
+ .image-uploader__button:hover::before,
462
+ .image-uploader__button--hover::before {
463
+ background-color: var(--svelte-ui-image-uploader-hover-bg);
464
+ border-color: var(--svelte-ui-image-uploader-hover-border-color);
465
+ }
466
+ }
467
+
468
+ .image-uploader:focus-visible {
469
+ outline: var(--svelte-ui-focus-outline-inner);
470
+ outline-offset: var(--svelte-ui-focus-outline-offset-inner);
471
+ }
472
+
473
+ .image-uploader__button--hover {
474
+ background-color: var(--svelte-ui-image-uploader-hover-bg);
475
+ border-color: var(--svelte-ui-primary-color);
476
+ }
477
+
478
+ .image-uploader__button--has-images {
479
+ padding: 0;
480
+ background: transparent;
481
+
482
+ &::before {
483
+ background: transparent;
484
+ border: none;
485
+ }
486
+ }
487
+
488
+ .image-uploader__description {
489
+ display: flex;
490
+ flex-direction: column;
491
+ align-items: center;
492
+ gap: 16px;
493
+ color: var(--svelte-ui-text-subtle-color);
494
+ text-align: center;
495
+ }
496
+
497
+ @media (hover: hover) {
498
+ .image-uploader__button:hover .image-uploader__description,
499
+ .image-uploader__button--hover .image-uploader__description {
500
+ color: var(--svelte-ui-image-uploader-hover-color);
501
+ }
502
+ }
503
+
504
+ .image-uploader__error-message {
505
+ margin-top: 8px;
506
+ padding: 8px 12px;
507
+ background-color: var(--svelte-ui-error-container-color);
508
+ color: var(--svelte-ui-error-color);
509
+ border-radius: var(--svelte-ui-border-radius);
510
+ font-size: var(--svelte-ui-font-size-sm);
511
+ }
512
+
513
+ .image-uploader__input {
514
+ display: none;
515
+ }
516
+ </style>
@@ -0,0 +1,37 @@
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
+ adaptiveSize?: boolean;
13
+ icon?: string;
14
+ iconSize?: number;
15
+ iconFilled?: boolean;
16
+ iconWeight?: IconWeight;
17
+ iconGrade?: IconGrade;
18
+ iconOpticalSize?: IconOpticalSize;
19
+ iconVariant?: IconVariant;
20
+ removeFileAriaLabel?: string;
21
+ onchange?: (value: FileList | null) => void;
22
+ onfocus?: Function;
23
+ onblur?: Function;
24
+ onkeydown?: Function;
25
+ onkeyup?: Function;
26
+ onmouseenter?: Function;
27
+ onmouseleave?: Function;
28
+ ontouchstart?: Function;
29
+ ontouchend?: Function;
30
+ onpointerenter?: Function;
31
+ onpointerleave?: Function;
32
+ };
33
+ declare const ImageUploader: import("svelte").Component<$$ComponentProps, {
34
+ reset: () => void;
35
+ }, "value">;
36
+ type ImageUploader = ReturnType<typeof ImageUploader>;
37
+ export default ImageUploader;
@@ -0,0 +1,157 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+ import IconButton from './IconButton.svelte';
4
+ import { getStyleFromNumber } from '../utils/style';
5
+
6
+ type ImagePreviewProps = {
7
+ file: File;
8
+ width?: string | number;
9
+ height?: string | number;
10
+ adaptiveSize?: boolean;
11
+ rounded?: boolean;
12
+ removeFileAriaLabel?: string;
13
+ onRemove: () => void;
14
+ id?: string;
15
+ };
16
+
17
+ let {
18
+ file,
19
+ width = '120px',
20
+ height = '120px',
21
+ adaptiveSize = false,
22
+ rounded = false,
23
+ removeFileAriaLabel,
24
+ onRemove,
25
+ id
26
+ }: ImagePreviewProps = $props();
27
+
28
+ let imageSizes = $state<Record<string, { width: number; height: number }>>({});
29
+
30
+ const getImageUrl = (file: File): string => {
31
+ return URL.createObjectURL(file);
32
+ };
33
+
34
+ // 画像サイズを取得してキャッシュに保存
35
+ const loadImageSize = (file: File) => {
36
+ const fileKey = file.name + file.size;
37
+
38
+ if (imageSizes[fileKey]) {
39
+ return; // 既にキャッシュされている
40
+ }
41
+
42
+ const img = new Image();
43
+ img.onload = () => {
44
+ imageSizes[fileKey] = { width: img.naturalWidth, height: img.naturalHeight };
45
+ };
46
+ img.onerror = () => {
47
+ imageSizes[fileKey] = { width: 120, height: 120 };
48
+ };
49
+ img.src = getImageUrl(file);
50
+ };
51
+
52
+ // adaptiveSizeによる分岐を最初に行う
53
+ const imageSizeStyle = $derived.by(() => {
54
+ if (adaptiveSize) {
55
+ return `
56
+ width: 100%;
57
+ height: auto;
58
+ max-width: '100%';
59
+ `;
60
+ } else {
61
+ return `
62
+ width: ${getStyleFromNumber(width)};
63
+ height: ${getStyleFromNumber(height)};
64
+ `;
65
+ }
66
+ });
67
+
68
+ onDestroy(() => {
69
+ if (file) {
70
+ URL.revokeObjectURL(getImageUrl(file));
71
+ }
72
+ });
73
+ </script>
74
+
75
+ <div
76
+ class="image-uploader-preview"
77
+ class:image-uploader-preview--rounded={rounded}
78
+ class:image-uploader-preview--adaptive={adaptiveSize}
79
+ style={imageSizeStyle}
80
+ >
81
+ <img
82
+ src={getImageUrl(file)}
83
+ alt={file.name}
84
+ class="image-uploader-preview__image"
85
+ class:image-uploader-preview__image--rounded={rounded}
86
+ onload={() => {
87
+ loadImageSize(file);
88
+ }}
89
+ />
90
+ <div class="image-uploader-preview__delete">
91
+ <IconButton
92
+ iconFilled={true}
93
+ size={24}
94
+ color="var(--svelte-ui-text-color)"
95
+ onclick={(e) => {
96
+ e.stopPropagation();
97
+ onRemove();
98
+ }}
99
+ ariaLabel={removeFileAriaLabel}
100
+ tabindex={-1}
101
+ >
102
+ cancel
103
+ </IconButton>
104
+ </div>
105
+ </div>
106
+
107
+ <style>
108
+ .image-uploader-preview {
109
+ position: relative;
110
+ }
111
+
112
+ .image-uploader-preview__image {
113
+ display: block;
114
+ width: 100%;
115
+ height: 100%;
116
+ object-fit: cover;
117
+ object-position: center;
118
+ border-radius: var(--svelte-ui-image-uploader-preview-border-radius);
119
+ }
120
+
121
+ .image-uploader-preview--adaptive .image-uploader-preview__image {
122
+ object-fit: contain;
123
+ }
124
+
125
+ .image-uploader-preview::after {
126
+ content: '';
127
+ position: absolute;
128
+ top: 0;
129
+ left: 0;
130
+ width: 100%;
131
+ height: 100%;
132
+ border-radius: var(--svelte-ui-image-uploader-preview-border-radius);
133
+ box-shadow: inset 0 0 0 var(--svelte-ui-border-width) var(--svelte-ui-border-weak-color);
134
+ }
135
+
136
+ .image-uploader-preview--rounded::after,
137
+ .image-uploader-preview__image--rounded {
138
+ border-radius: var(--svelte-ui-image-uploader-preview-border-radius-rounded);
139
+ }
140
+
141
+ .image-uploader-preview__delete {
142
+ position: absolute;
143
+ top: 4px;
144
+ right: 4px;
145
+ background-color: var(--svelte-ui-surface-color);
146
+ border-radius: var(--svelte-ui-border-radius-rounded);
147
+ opacity: 0;
148
+ transition: opacity var(--svelte-ui-transition-duration);
149
+ z-index: 10;
150
+ }
151
+
152
+ @media (hover: hover) {
153
+ .image-uploader-preview:hover .image-uploader-preview__delete {
154
+ opacity: 1;
155
+ }
156
+ }
157
+ </style>
@@ -0,0 +1,13 @@
1
+ type ImagePreviewProps = {
2
+ file: File;
3
+ width?: string | number;
4
+ height?: string | number;
5
+ adaptiveSize?: boolean;
6
+ rounded?: boolean;
7
+ removeFileAriaLabel?: string;
8
+ onRemove: () => void;
9
+ id?: string;
10
+ };
11
+ declare const ImageUploaderPreview: import("svelte").Component<ImagePreviewProps, {}, "">;
12
+ type ImageUploaderPreview = ReturnType<typeof ImageUploaderPreview>;
13
+ export default ImageUploaderPreview;