@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,421 @@
1
+ <!-- PopupMenu.svelte -->
2
+
3
+ <script lang="ts">
4
+ import Popup from './Popup.svelte';
5
+ import Icon from './Icon.svelte';
6
+ import type { MenuItem } from '../types/menuItem';
7
+
8
+ import type { SvelteComponent } from 'svelte';
9
+ import { tick } from 'svelte';
10
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
11
+
12
+ // =========================================================================
13
+ // Props, States & Constants
14
+ // =========================================================================
15
+ let {
16
+ // DOM参照
17
+ anchorElement,
18
+
19
+ // 基本プロパティ
20
+ menuItems,
21
+
22
+ // HTML属性
23
+ id,
24
+
25
+ // スタイル/レイアウト
26
+ position = 'bottom',
27
+
28
+ // 状態/動作
29
+ isOpen = $bindable(false),
30
+ mobileFullscreen = true,
31
+ mobileBehavior = 'auto',
32
+
33
+ // アイコン関連
34
+ iconFilled = false,
35
+ iconWeight = 300,
36
+ iconGrade = 0,
37
+ iconOpticalSize = 24,
38
+ iconVariant = 'outlined',
39
+
40
+ // ARIA/アクセシビリティ
41
+ ariaLabel = 'Menu'
42
+ }: {
43
+ isOpen?: boolean;
44
+ anchorElement: HTMLElement;
45
+ position?:
46
+ | 'top'
47
+ | 'bottom'
48
+ | 'left'
49
+ | 'right'
50
+ | 'top-left'
51
+ | 'top-center'
52
+ | 'top-right'
53
+ | 'bottom-left'
54
+ | 'bottom-center'
55
+ | 'bottom-right'
56
+ | 'left-top'
57
+ | 'left-center'
58
+ | 'left-bottom'
59
+ | 'right-top'
60
+ | 'right-center'
61
+ | 'right-bottom'
62
+ | 'auto';
63
+ menuItems: (MenuItem | 'separator')[];
64
+ id?: string;
65
+ ariaLabel?: string;
66
+ mobileFullscreen?: boolean;
67
+ mobileBehavior?: 'auto' | 'fullscreen' | 'popup';
68
+ iconFilled?: boolean;
69
+ iconWeight?: IconWeight;
70
+ iconGrade?: IconGrade;
71
+ iconOpticalSize?: IconOpticalSize;
72
+ iconVariant?: IconVariant;
73
+ } = $props();
74
+
75
+ let popupRef: SvelteComponent | undefined = $state();
76
+ let menuContainerRef: HTMLDivElement | undefined = $state();
77
+ let menuItemRefs: HTMLButtonElement[] = $state([]);
78
+ let activeIndex: number = $state(-1);
79
+ let menuId: string = $state(`menu-${Math.random().toString(36).substring(2, 15)}`);
80
+
81
+ // =========================================================================
82
+ // Methods
83
+ // =========================================================================
84
+ const goto = (url: string) => {
85
+ if (typeof window !== 'undefined') {
86
+ window.location.href = url;
87
+ }
88
+ };
89
+
90
+ const getMenuItemId = (index: number): string => `${menuId}-item-${index}`;
91
+
92
+ const handleClick = (event: MouseEvent, item: MenuItem | 'separator') => {
93
+ event.preventDefault();
94
+ event.stopPropagation();
95
+ if (item === 'separator') return;
96
+
97
+ executeMenuItem(item as MenuItem);
98
+ };
99
+
100
+ const executeMenuItem = (item: MenuItem) => {
101
+ if (item.href) {
102
+ goto(`${item.href}`);
103
+ }
104
+ if (item.callback) {
105
+ item.callback();
106
+ }
107
+ close();
108
+ };
109
+
110
+ const handleKeyDown = async (event: KeyboardEvent) => {
111
+ switch (event.key) {
112
+ case 'ArrowDown':
113
+ event.preventDefault();
114
+ moveToNextItem();
115
+ break;
116
+ case 'ArrowUp':
117
+ event.preventDefault();
118
+ moveToPreviousItem();
119
+ break;
120
+ case 'Home':
121
+ event.preventDefault();
122
+ moveToFirstItem();
123
+ break;
124
+ case 'End':
125
+ event.preventDefault();
126
+ moveToLastItem();
127
+ break;
128
+ case 'Enter':
129
+ case ' ':
130
+ event.preventDefault();
131
+ if (activeIndex >= 0 && activeIndex < actionableItems.length) {
132
+ executeMenuItem(actionableItems[activeIndex].item);
133
+ }
134
+ break;
135
+ case 'Escape':
136
+ event.preventDefault();
137
+ close();
138
+ break;
139
+ case 'Tab':
140
+ close();
141
+ break;
142
+ }
143
+ };
144
+
145
+ const moveToNextItem = () => {
146
+ if (actionableItems.length === 0) return;
147
+ activeIndex = activeIndex < actionableItems.length - 1 ? activeIndex + 1 : 0;
148
+ focusActiveItem();
149
+ };
150
+
151
+ const moveToPreviousItem = () => {
152
+ if (actionableItems.length === 0) return;
153
+ activeIndex = activeIndex > 0 ? activeIndex - 1 : actionableItems.length - 1;
154
+ focusActiveItem();
155
+ };
156
+
157
+ const moveToFirstItem = () => {
158
+ if (actionableItems.length === 0) return;
159
+ activeIndex = 0;
160
+ focusActiveItem();
161
+ };
162
+
163
+ const moveToLastItem = () => {
164
+ if (actionableItems.length === 0) return;
165
+ activeIndex = actionableItems.length - 1;
166
+ focusActiveItem();
167
+ };
168
+
169
+ const focusActiveItem = async () => {
170
+ await tick();
171
+ if (activeIndex >= 0 && activeIndex < menuItemRefs.length) {
172
+ menuItemRefs[activeIndex]?.focus();
173
+ }
174
+ };
175
+
176
+ const handleMouseEnter = (index: number) => {
177
+ activeIndex = index;
178
+ };
179
+
180
+ const handleFocus = (index: number) => {
181
+ activeIndex = index;
182
+ };
183
+
184
+ const announceMenuOpened = () => {
185
+ const announcement = document.createElement('div');
186
+ announcement.setAttribute('aria-live', 'polite');
187
+ announcement.setAttribute('aria-atomic', 'true');
188
+ announcement.className = 'sr-only';
189
+ announcement.textContent = `${ariaLabel} opened with ${actionableItems.length} items`;
190
+ document.body.appendChild(announcement);
191
+
192
+ setTimeout(() => {
193
+ document.body.removeChild(announcement);
194
+ }, 1000);
195
+ };
196
+
197
+ const handlePopupOpen = () => {
198
+ activeIndex = 0;
199
+ tick().then(() => {
200
+ menuContainerRef?.focus();
201
+ });
202
+
203
+ document.addEventListener('keydown', handleKeyDown);
204
+ announceMenuOpened();
205
+ };
206
+
207
+ const handlePopupClose = () => {
208
+ activeIndex = -1;
209
+ document.removeEventListener('keydown', handleKeyDown);
210
+
211
+ if (anchorElement) {
212
+ anchorElement.focus();
213
+ }
214
+ };
215
+
216
+ export const open = () => {
217
+ popupRef?.open();
218
+ };
219
+
220
+ export const close = () => {
221
+ popupRef?.close();
222
+ };
223
+
224
+ export const toggle = () => {
225
+ popupRef?.toggle();
226
+ };
227
+
228
+ // =========================================================================
229
+ // $derived
230
+ // =========================================================================
231
+ const actionableItems: { item: MenuItem; originalIndex: number }[] = $derived(
232
+ menuItems
233
+ .map((item, index) => ({ item, originalIndex: index }))
234
+ .filter(
235
+ (entry): entry is { item: MenuItem; originalIndex: number } => entry.item !== 'separator'
236
+ )
237
+ );
238
+ </script>
239
+
240
+ <Popup
241
+ bind:this={popupRef}
242
+ bind:isOpen
243
+ {anchorElement}
244
+ {position}
245
+ onOpen={handlePopupOpen}
246
+ onClose={handlePopupClose}
247
+ role="menu"
248
+ {mobileFullscreen}
249
+ {mobileBehavior}
250
+ id={id ? `${id}-popup` : undefined}
251
+ >
252
+ <div
253
+ class="popup-menu"
254
+ bind:this={menuContainerRef}
255
+ role="menu"
256
+ aria-label={ariaLabel}
257
+ aria-activedescendant={activeIndex >= 0 ? getMenuItemId(activeIndex) : undefined}
258
+ tabindex="-1"
259
+ {id}
260
+ >
261
+ <ul class="popup-menu__list" role="none">
262
+ {#each menuItems as item, index}
263
+ <li class="popup-menu__item" role="none">
264
+ {#if item === 'separator'}
265
+ <div class="popup-menu__separator" role="separator" aria-orientation="horizontal"></div>
266
+ {:else}
267
+ {@const actionableIndex = actionableItems.findIndex((ai) => ai.originalIndex === index)}
268
+ {@const isActive = actionableIndex === activeIndex}
269
+ <button
270
+ bind:this={menuItemRefs[actionableIndex]}
271
+ id={getMenuItemId(actionableIndex)}
272
+ class="popup-menu__button"
273
+ class:popup-menu__button--active={isActive}
274
+ role="menuitem"
275
+ tabindex="-1"
276
+ aria-describedby={item.icon ? `${getMenuItemId(actionableIndex)}-icon` : undefined}
277
+ onclick={(event) => handleClick(event, item)}
278
+ onmouseenter={() => handleMouseEnter(actionableIndex)}
279
+ onfocus={() => handleFocus(actionableIndex)}
280
+ >
281
+ {#if item.icon}
282
+ <Icon
283
+ id="{getMenuItemId(actionableIndex)}-icon"
284
+ aria-hidden="true"
285
+ filled={iconFilled}
286
+ weight={iconWeight}
287
+ grade={iconGrade}
288
+ opticalSize={iconOpticalSize}
289
+ variant={iconVariant}
290
+ >
291
+ {item.icon}
292
+ </Icon>
293
+ {/if}
294
+ <span class="popup-menu__text">{item.title}</span>
295
+ {#if item.href}
296
+ <span class="sr-only">link</span>
297
+ {/if}
298
+ </button>
299
+ {/if}
300
+ </li>
301
+ {/each}
302
+ </ul>
303
+ </div>
304
+ </Popup>
305
+
306
+ <style>.popup-menu {
307
+ background-color: var(--svelte-ui-surface-color);
308
+ border-radius: 4px;
309
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
310
+ outline: none;
311
+ min-width: 160px;
312
+ }
313
+
314
+ .popup-menu__list {
315
+ padding: 8px 0;
316
+ margin: 0;
317
+ list-style: none;
318
+ }
319
+
320
+ .popup-menu__item {
321
+ margin: 0;
322
+ }
323
+
324
+ .popup-menu__button {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 8px;
328
+ width: 100%;
329
+ padding: 8px 16px;
330
+ background: transparent;
331
+ border: none;
332
+ font-size: 1rem;
333
+ color: var(--svelte-ui-popupmenu-text-color);
334
+ text-align: left;
335
+ white-space: nowrap;
336
+ cursor: pointer;
337
+ transition: background-color 0.2s ease;
338
+ border-radius: 0;
339
+ }
340
+
341
+ @media (hover: hover) {
342
+ .popup-menu__button:hover,
343
+ .popup-menu__button--active {
344
+ background-color: var(--svelte-ui-hover-overlay);
345
+ outline: none;
346
+ }
347
+ }
348
+ .popup-menu__button--active {
349
+ background-color: var(--svelte-ui-hover-overlay);
350
+ outline: none;
351
+ }
352
+
353
+ .popup-menu__button:focus-visible {
354
+ background-color: var(--svelte-ui-hover-overlay);
355
+ outline: 2px solid var(--svelte-ui-popupmenu-focus-color);
356
+ outline-offset: -2px;
357
+ }
358
+
359
+ .popup-menu__text {
360
+ flex: 1;
361
+ }
362
+
363
+ .popup-menu__separator {
364
+ height: 0;
365
+ margin: 8px 0;
366
+ border-bottom: solid 1px var(--svelte-ui-border-color);
367
+ }
368
+
369
+ /* Screen reader only content */
370
+ .sr-only {
371
+ position: absolute;
372
+ width: 1px;
373
+ height: 1px;
374
+ padding: 0;
375
+ margin: -1px;
376
+ overflow: hidden;
377
+ clip: rect(0, 0, 0, 0);
378
+ white-space: nowrap;
379
+ border: 0;
380
+ }
381
+
382
+ /* Reduced motion support */
383
+ @media (prefers-reduced-motion: reduce) {
384
+ .popup-menu__button {
385
+ transition: none;
386
+ }
387
+ }
388
+ /* Mobile touch target optimization */
389
+ @media (max-width: 768px) {
390
+ .popup-menu__button {
391
+ min-height: var(--svelte-ui-touch-target);
392
+ padding: 12px 16px;
393
+ }
394
+ }
395
+ /* Enhanced mobile menu styles */
396
+ :global(.mobile .popup-menu) {
397
+ border-radius: 0;
398
+ box-shadow: none;
399
+ background: transparent;
400
+ width: 100%;
401
+ max-width: none;
402
+ min-width: auto;
403
+ }
404
+
405
+ :global(.mobile.fullscreen .popup-menu) {
406
+ background: var(--svelte-ui-surface-color);
407
+ border-radius: var(--svelte-ui-popup-mobile-border-radius);
408
+ box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
409
+ margin: 0;
410
+ padding: 0;
411
+ }
412
+
413
+ :global(.mobile.fullscreen .popup-menu__list) {
414
+ padding: 16px 0;
415
+ }
416
+
417
+ :global(.mobile.fullscreen .popup-menu__button) {
418
+ padding: 16px 24px;
419
+ font-size: 1.1rem;
420
+ min-height: var(--svelte-ui-touch-target-lg);
421
+ }</style>
@@ -0,0 +1,24 @@
1
+ import type { MenuItem } from '../types/menuItem';
2
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
+ type $$ComponentProps = {
4
+ isOpen?: boolean;
5
+ anchorElement: HTMLElement;
6
+ position?: 'top' | 'bottom' | 'left' | 'right' | 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right' | 'left-top' | 'left-center' | 'left-bottom' | 'right-top' | 'right-center' | 'right-bottom' | 'auto';
7
+ menuItems: (MenuItem | 'separator')[];
8
+ id?: string;
9
+ ariaLabel?: string;
10
+ mobileFullscreen?: boolean;
11
+ mobileBehavior?: 'auto' | 'fullscreen' | 'popup';
12
+ iconFilled?: boolean;
13
+ iconWeight?: IconWeight;
14
+ iconGrade?: IconGrade;
15
+ iconOpticalSize?: IconOpticalSize;
16
+ iconVariant?: IconVariant;
17
+ };
18
+ declare const PopupMenu: import("svelte").Component<$$ComponentProps, {
19
+ open: () => void;
20
+ close: () => void;
21
+ toggle: () => void;
22
+ }, "isOpen">;
23
+ type PopupMenu = ReturnType<typeof PopupMenu>;
24
+ export default PopupMenu;