@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,676 @@
1
+ <!-- Popup.svelte -->
2
+
3
+ <script lang="ts">
4
+ /**
5
+ * 🚨 CRITICAL: DO NOT MANAGE POPUP STATE FROM PARENT COMPONENTS
6
+ *
7
+ * This Popup component manages its own open/closed state internally.
8
+ * Parent components must NEVER create their own popup state variables.
9
+ *
10
+ * ❌ WRONG: let isPopupOpen = $state(false)
11
+ * ✅ RIGHT: Use popupRef.open(), popupRef.close(), popupRef.toggle()
12
+ * ✅ RIGHT: Use onOpen/onClose callbacks for side effects
13
+ *
14
+ * This prevents state synchronization bugs and ensures consistent behavior.
15
+ */
16
+
17
+ import type { Snippet } from 'svelte';
18
+ import { onDestroy, tick, onMount } from 'svelte';
19
+ import { isMobileDevice, disableBodyScroll, getViewportSize } from '../utils/mobile';
20
+ import { announceOpenClose } from '../utils/accessibility';
21
+
22
+ // =========================================================================
23
+ // Props, States & Constants
24
+ // =========================================================================
25
+ let {
26
+ // Snippet
27
+ children,
28
+
29
+ // DOM参照
30
+ anchorElement,
31
+
32
+ // 基本プロパティ
33
+ role = 'menu',
34
+
35
+ // HTML属性
36
+ id,
37
+
38
+ // スタイル/レイアウト
39
+ position = 'bottom',
40
+ margin = 8,
41
+
42
+ // 状態/動作
43
+ isOpen = $bindable(false),
44
+ focusTrap = false,
45
+ restoreFocus = false,
46
+ mobileFullscreen = false,
47
+ mobileBehavior = 'auto',
48
+ allowRepositioning = true,
49
+
50
+ // ARIA/アクセシビリティ
51
+ ariaLabel,
52
+ ariaLabelledby,
53
+ ariaDescribedby,
54
+
55
+ // イベントハンドラー
56
+ onOpen = () => {}, // No params for type inference
57
+ onClose = () => {}
58
+ }: {
59
+ // Snippet
60
+ children: Snippet;
61
+
62
+ // DOM参照
63
+ anchorElement: HTMLElement;
64
+
65
+ // 基本プロパティ
66
+ role?: string;
67
+
68
+ // HTML属性
69
+ id?: string;
70
+
71
+ // スタイル/レイアウト
72
+ position?:
73
+ | 'top'
74
+ | 'bottom'
75
+ | 'left'
76
+ | 'right'
77
+ | 'top-left'
78
+ | 'top-center'
79
+ | 'top-right'
80
+ | 'bottom-left'
81
+ | 'bottom-center'
82
+ | 'bottom-right'
83
+ | 'left-top'
84
+ | 'left-center'
85
+ | 'left-bottom'
86
+ | 'right-top'
87
+ | 'right-center'
88
+ | 'right-bottom'
89
+ | 'auto';
90
+ margin?: number;
91
+
92
+ // 状態/動作
93
+ isOpen?: boolean;
94
+ focusTrap?: boolean;
95
+ restoreFocus?: boolean;
96
+ mobileFullscreen?: boolean;
97
+ mobileBehavior?: 'auto' | 'fullscreen' | 'popup';
98
+ allowRepositioning?: boolean;
99
+
100
+ // ARIA/アクセシビリティ
101
+ ariaLabel?: string;
102
+ ariaLabelledby?: string;
103
+ ariaDescribedby?: string;
104
+
105
+ // イベントハンドラー
106
+ onOpen?: () => void;
107
+ onClose?: () => void;
108
+ } = $props();
109
+
110
+ let popupRef: HTMLDivElement | undefined = $state();
111
+ let popupId: string = $state(id || `popup-${Math.random().toString(36).substring(2, 15)}`);
112
+ let previousActiveElement: HTMLElement | null = null;
113
+ let isMobile: boolean = $state(false);
114
+ let shouldUseFullscreen: boolean = $state(false);
115
+ let bodyScrollCleanup: (() => void) | undefined = $state();
116
+
117
+ // =========================================================================
118
+ // Lifecycle
119
+ // =========================================================================
120
+ onMount(() => {
121
+ isMobile = isMobileDevice();
122
+
123
+ if (mobileBehavior === 'auto') {
124
+ shouldUseFullscreen = isMobile;
125
+ } else if (mobileBehavior === 'fullscreen') {
126
+ shouldUseFullscreen = true;
127
+ } else {
128
+ shouldUseFullscreen = mobileFullscreen;
129
+ }
130
+ });
131
+
132
+ onDestroy(() => {
133
+ removeEventListenersToClose();
134
+ removeKeyboardListener();
135
+ cleanupMobileFeatures();
136
+ });
137
+
138
+ // =========================================================================
139
+ // Effects
140
+ // =========================================================================
141
+ $effect(() => {
142
+ if (isOpen) {
143
+ if (popupRef && popupRef.matches(':popover-open')) {
144
+ return;
145
+ }
146
+ open();
147
+ } else {
148
+ if (!popupRef || !popupRef.matches(':popover-open')) {
149
+ return;
150
+ }
151
+ close();
152
+ }
153
+ });
154
+
155
+ // =========================================================================
156
+ // Methods
157
+ // =========================================================================
158
+ const handleKeyDown = (event: KeyboardEvent) => {
159
+ if (!isOpen) return;
160
+
161
+ switch (event.key) {
162
+ case 'Escape':
163
+ event.preventDefault();
164
+ close();
165
+ break;
166
+ case 'Tab':
167
+ if (focusTrap) {
168
+ handleTabKey(event);
169
+ }
170
+ break;
171
+ }
172
+ };
173
+
174
+ const handleTabKey = (event: KeyboardEvent) => {
175
+ if (!popupRef) return;
176
+
177
+ const focusableElements = popupRef.querySelectorAll(
178
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
179
+ );
180
+ const firstElement = focusableElements[0] as HTMLElement;
181
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
182
+
183
+ if (event.shiftKey) {
184
+ if (document.activeElement === firstElement) {
185
+ event.preventDefault();
186
+ lastElement?.focus();
187
+ }
188
+ } else {
189
+ if (document.activeElement === lastElement) {
190
+ event.preventDefault();
191
+ firstElement?.focus();
192
+ }
193
+ }
194
+ };
195
+
196
+ const handleScroll = (event: Event) => {
197
+ const target = event.target;
198
+
199
+ if (
200
+ popupRef &&
201
+ target &&
202
+ target instanceof Element &&
203
+ (popupRef.contains(target) || target === popupRef)
204
+ ) {
205
+ return;
206
+ }
207
+
208
+ if (anchorElement && target) {
209
+ if (target === document || target === document.documentElement || target === document.body) {
210
+ close();
211
+ return;
212
+ }
213
+
214
+ if (target instanceof Element && target.contains(anchorElement)) {
215
+ close();
216
+ return;
217
+ }
218
+ }
219
+ };
220
+
221
+ const getBestPosition = (
222
+ anchorRect: DOMRect,
223
+ popupRect: DOMRect,
224
+ viewport: { width: number; height: number },
225
+ margin: number
226
+ ): typeof position => {
227
+ const spaceAbove = anchorRect.top;
228
+ const spaceBelow = viewport.height - anchorRect.bottom;
229
+ const spaceLeft = anchorRect.left;
230
+ const spaceRight = viewport.width - anchorRect.right;
231
+
232
+ const needHeight = popupRect.height + margin;
233
+ const needWidth = popupRect.width + margin;
234
+
235
+ const candidates = [
236
+ { position: 'bottom-center' as const, score: spaceBelow >= needHeight ? spaceBelow : 0 },
237
+ {
238
+ position: 'top-center' as const,
239
+ score: !allowRepositioning ? 0 : spaceAbove >= needHeight ? spaceAbove : 0
240
+ },
241
+ { position: 'right-center' as const, score: spaceRight >= needWidth ? spaceRight : 0 },
242
+ { position: 'left-center' as const, score: spaceLeft >= needWidth ? spaceLeft : 0 }
243
+ ];
244
+
245
+ const best = candidates.reduce((a, b) => (a.score > b.score ? a : b));
246
+ return best.score > 0 ? best.position : 'bottom-center';
247
+ };
248
+
249
+ const calculatePosition = (
250
+ pos: typeof position,
251
+ anchorRect: DOMRect,
252
+ popupRect: DOMRect,
253
+ viewport: { width: number; height: number },
254
+ margin: number
255
+ ): { x: number; y: number } => {
256
+ let x = 0;
257
+ let y = 0;
258
+
259
+ switch (pos) {
260
+ case 'top':
261
+ case 'top-center':
262
+ x = anchorRect.left + (anchorRect.width - popupRect.width) / 2;
263
+ y = anchorRect.top - popupRect.height - margin;
264
+ break;
265
+ case 'top-left':
266
+ x = anchorRect.left;
267
+ y = anchorRect.top - popupRect.height - margin;
268
+ break;
269
+ case 'top-right':
270
+ x = anchorRect.right - popupRect.width;
271
+ y = anchorRect.top - popupRect.height - margin;
272
+ break;
273
+ case 'bottom':
274
+ case 'bottom-center':
275
+ x = anchorRect.left + (anchorRect.width - popupRect.width) / 2;
276
+ y = anchorRect.bottom + margin;
277
+ break;
278
+ case 'bottom-left':
279
+ x = anchorRect.left;
280
+ y = anchorRect.bottom + margin;
281
+ break;
282
+ case 'bottom-right':
283
+ x = anchorRect.right - popupRect.width;
284
+ y = anchorRect.bottom + margin;
285
+ break;
286
+ case 'left':
287
+ case 'left-center':
288
+ x = anchorRect.left - popupRect.width - margin;
289
+ y = anchorRect.top + (anchorRect.height - popupRect.height) / 2;
290
+ break;
291
+ case 'left-top':
292
+ x = anchorRect.left - popupRect.width - margin;
293
+ y = anchorRect.top;
294
+ break;
295
+ case 'left-bottom':
296
+ x = anchorRect.left - popupRect.width - margin;
297
+ y = anchorRect.bottom - popupRect.height;
298
+ break;
299
+ case 'right':
300
+ case 'right-center':
301
+ x = anchorRect.right + margin;
302
+ y = anchorRect.top + (anchorRect.height - popupRect.height) / 2;
303
+ break;
304
+ case 'right-top':
305
+ x = anchorRect.right + margin;
306
+ y = anchorRect.top;
307
+ break;
308
+ case 'right-bottom':
309
+ x = anchorRect.right + margin;
310
+ y = anchorRect.bottom - popupRect.height;
311
+ break;
312
+ }
313
+
314
+ x = Math.max(margin, Math.min(x, viewport.width - popupRect.width - margin));
315
+
316
+ if (!allowRepositioning) {
317
+ y = Math.max(margin, y);
318
+ if (y + popupRect.height + margin > viewport.height) {
319
+ const availableHeight = viewport.height - y - margin;
320
+ if (popupRef) {
321
+ popupRef.style.maxHeight = `${availableHeight}px`;
322
+ popupRef.style.overflowY = 'auto';
323
+ }
324
+ }
325
+ } else {
326
+ y = Math.max(margin, Math.min(y, viewport.height - popupRect.height - margin));
327
+ }
328
+
329
+ return { x, y };
330
+ };
331
+
332
+ const setPosition = (): void => {
333
+ if (anchorElement && popupRef) {
334
+ const anchorRect = anchorElement.getBoundingClientRect();
335
+ const popupRect = popupRef.getBoundingClientRect();
336
+ const viewport = {
337
+ width: window.innerWidth,
338
+ height: window.innerHeight
339
+ };
340
+
341
+ let actualPosition = position;
342
+
343
+ if (position === 'auto') {
344
+ actualPosition = getBestPosition(anchorRect, popupRect, viewport, margin);
345
+ }
346
+
347
+ const coords = calculatePosition(actualPosition, anchorRect, popupRect, viewport, margin);
348
+
349
+ popupRef.style.setProperty('left', '0px', 'important');
350
+ popupRef.style.setProperty('top', '0px', 'important');
351
+ popupRef.style.setProperty('position', 'fixed', 'important');
352
+ popupRef.style.setProperty(
353
+ 'transform',
354
+ `translate(${coords.x}px, ${coords.y}px)`,
355
+ 'important'
356
+ );
357
+ }
358
+ };
359
+
360
+ const addEventListenersToClose = () => {
361
+ window.addEventListener('resize', close);
362
+ window.addEventListener('scroll', handleScroll, true);
363
+ document.querySelectorAll('.scrollable').forEach((element) => {
364
+ element.addEventListener('scroll', handleScroll);
365
+ });
366
+ };
367
+
368
+ const removeEventListenersToClose = () => {
369
+ window.removeEventListener('resize', close);
370
+ window.removeEventListener('scroll', handleScroll, true);
371
+ document.querySelectorAll('.scrollable').forEach((element) => {
372
+ element.removeEventListener('scroll', handleScroll);
373
+ });
374
+ };
375
+
376
+ const addKeyboardListener = () => {
377
+ document.addEventListener('keydown', handleKeyDown);
378
+ };
379
+
380
+ const removeKeyboardListener = () => {
381
+ document.removeEventListener('keydown', handleKeyDown);
382
+ };
383
+
384
+ const focusFirstElement = () => {
385
+ if (!popupRef) return;
386
+
387
+ const focusableElement = popupRef.querySelector(
388
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
389
+ ) as HTMLElement;
390
+
391
+ if (focusableElement) {
392
+ focusableElement.focus();
393
+ } else {
394
+ popupRef.focus();
395
+ }
396
+ };
397
+
398
+ const announceToScreenReader = () => {
399
+ if (!ariaLabel && !ariaLabelledby) return;
400
+ announceOpenClose('Popup', true, ariaLabel);
401
+ };
402
+
403
+ const setupMobileFeatures = () => {
404
+ if (!popupRef || !isMobile) return;
405
+
406
+ if (shouldUseFullscreen) {
407
+ bodyScrollCleanup = disableBodyScroll();
408
+ }
409
+ };
410
+
411
+ const cleanupMobileFeatures = () => {
412
+ if (bodyScrollCleanup) {
413
+ bodyScrollCleanup();
414
+ bodyScrollCleanup = undefined;
415
+ }
416
+ };
417
+
418
+ const closeEnd = () => {
419
+ popupRef?.hidePopover();
420
+
421
+ if (popupRef) {
422
+ popupRef.style.maxHeight = '';
423
+ popupRef.style.overflowY = '';
424
+ }
425
+
426
+ if (restoreFocus && previousActiveElement) {
427
+ previousActiveElement.focus();
428
+ }
429
+ previousActiveElement = null;
430
+
431
+ // アニメーション完了後にonCloseを呼ぶ
432
+ onClose?.();
433
+ };
434
+
435
+ const clickOutside = (element: HTMLElement, callbackFunction: Function) => {
436
+ function onClick(event: MouseEvent) {
437
+ if (!(event.target instanceof Node)) return;
438
+
439
+ const isInsidePopup = element.contains(event.target);
440
+ const isInsideAnchor = anchorElement && anchorElement.contains(event.target);
441
+ const isOutside = !isInsidePopup && !isInsideAnchor;
442
+
443
+ if (isOutside) {
444
+ callbackFunction();
445
+ event.stopPropagation();
446
+ }
447
+ }
448
+ setTimeout(() => {
449
+ document.body.addEventListener('click', onClick);
450
+ }, 1);
451
+ return {
452
+ update(newCallbackFunction: Function) {
453
+ callbackFunction = newCallbackFunction;
454
+ },
455
+ destroy() {
456
+ document.body.removeEventListener('click', onClick);
457
+ }
458
+ };
459
+ };
460
+
461
+ export const open = async () => {
462
+ previousActiveElement = document.activeElement as HTMLElement;
463
+
464
+ setTimeout(async () => {
465
+ popupRef?.removeEventListener('animationend', closeEnd);
466
+ popupRef?.showPopover();
467
+ isOpen = true;
468
+ addEventListenersToClose();
469
+ addKeyboardListener();
470
+
471
+ await tick();
472
+
473
+ if (!shouldUseFullscreen) {
474
+ setPosition();
475
+ await new Promise((resolve) => requestAnimationFrame(resolve));
476
+ setPosition();
477
+ }
478
+
479
+ setupMobileFeatures();
480
+
481
+ if (focusTrap) {
482
+ focusFirstElement();
483
+ }
484
+
485
+ announceToScreenReader();
486
+ onOpen?.();
487
+ }, 1);
488
+ };
489
+
490
+ export const close = () => {
491
+ isOpen = false;
492
+ removeEventListenersToClose();
493
+ removeKeyboardListener();
494
+ cleanupMobileFeatures();
495
+ popupRef?.addEventListener('animationend', closeEnd, { once: true });
496
+ // onCloseはアニメーション完了後に呼ぶ(closeEndで実行)
497
+ };
498
+
499
+ export const toggle = () => {
500
+ if (isOpen) {
501
+ close();
502
+ } else {
503
+ open();
504
+ }
505
+ };
506
+
507
+ export const getIsOpen = () => {
508
+ return isOpen;
509
+ };
510
+ </script>
511
+
512
+ <div
513
+ popover="manual"
514
+ bind:this={popupRef}
515
+ class="popup"
516
+ class:popup--fade-out={!isOpen}
517
+ class:popup--mobile={isMobile}
518
+ class:popup--fullscreen={shouldUseFullscreen}
519
+ {role}
520
+ aria-label={ariaLabel}
521
+ aria-labelledby={ariaLabelledby}
522
+ aria-describedby={ariaDescribedby}
523
+ aria-modal={undefined}
524
+ id={popupId}
525
+ use:clickOutside={() => {
526
+ close();
527
+ }}
528
+ >
529
+ {#if shouldUseFullscreen}
530
+ <div class="popup__mobile">
531
+ <div class="popup__mobile-content">
532
+ {@render children()}
533
+ </div>
534
+ </div>
535
+ {:else}
536
+ {@render children()}
537
+ {/if}
538
+ </div>
539
+
540
+ <style>@charset "UTF-8";
541
+ :popover-open {
542
+ border: solid 1px var(--svelte-ui-border-weak-color);
543
+ border-radius: 4px;
544
+ 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);
545
+ background: var(--svelte-ui-surface-color);
546
+ z-index: 1000; /* Popupを最前面に表示 */
547
+ }
548
+
549
+ :popover-open:focus-visible {
550
+ outline: 2px solid var(--svelte-ui-popup-focus-color);
551
+ outline-offset: 2px;
552
+ }
553
+
554
+ @keyframes fadeIn {
555
+ from {
556
+ opacity: 0;
557
+ transform: scale(0.95);
558
+ }
559
+ to {
560
+ opacity: 1;
561
+ transform: scale(1);
562
+ }
563
+ }
564
+ :popover-open,
565
+ :popover-open::backdrop {
566
+ animation: fadeIn 300ms forwards;
567
+ }
568
+
569
+ @keyframes fadeOut {
570
+ from {
571
+ opacity: 1;
572
+ transform: scale(1);
573
+ }
574
+ to {
575
+ opacity: 0;
576
+ transform: scale(0.95);
577
+ }
578
+ }
579
+ :popover-open.popup--fade-out,
580
+ :popover-open.popup--fade-out::backdrop {
581
+ animation: fadeOut 300ms forwards;
582
+ }
583
+
584
+ /* Screen reader only content */
585
+ :global(.sr-only) {
586
+ position: absolute;
587
+ width: 1px;
588
+ height: 1px;
589
+ padding: 0;
590
+ margin: -1px;
591
+ overflow: hidden;
592
+ clip: rect(0, 0, 0, 0);
593
+ white-space: nowrap;
594
+ border: 0;
595
+ }
596
+
597
+ /* =============================================
598
+ * Mobile-specific styles
599
+ * ============================================= */
600
+ :popover-open.popup--mobile {
601
+ position: fixed;
602
+ inset: 0;
603
+ width: 100%;
604
+ height: 100%;
605
+ margin: 0;
606
+ border: none;
607
+ border-radius: 0;
608
+ box-shadow: none;
609
+ background: transparent;
610
+ z-index: var(--svelte-ui-z-modal);
611
+ }
612
+
613
+ :popover-open.popup--mobile.popup--fullscreen {
614
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
615
+ }
616
+
617
+ .popup__mobile {
618
+ position: absolute;
619
+ bottom: 0;
620
+ left: 0;
621
+ right: 0;
622
+ background: var(--svelte-ui-surface-color);
623
+ border-top-left-radius: var(--svelte-ui-popup-mobile-border-radius);
624
+ border-top-right-radius: var(--svelte-ui-popup-mobile-border-radius);
625
+ max-height: 90vh;
626
+ overflow: hidden;
627
+ animation: slideUpMobile 300ms ease-out;
628
+ box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1), 0 -2px 4px -1px rgba(0, 0, 0, 0.06);
629
+ }
630
+
631
+ .popup__mobile-content {
632
+ padding: var(--svelte-ui-popup-mobile-margin);
633
+ max-height: calc(90vh - 60px);
634
+ overflow-y: auto;
635
+ }
636
+
637
+ /* Mobile animations */
638
+ @keyframes slideUpMobile {
639
+ from {
640
+ transform: translateY(100%);
641
+ }
642
+ to {
643
+ transform: translateY(0);
644
+ }
645
+ }
646
+ :popover-open.popup--mobile.popup--fullscreen.popup--fade-out .popup__mobile {
647
+ animation: slideDownMobile 300ms ease-in;
648
+ }
649
+
650
+ @keyframes slideDownMobile {
651
+ from {
652
+ transform: translateY(0);
653
+ }
654
+ to {
655
+ transform: translateY(100%);
656
+ }
657
+ }
658
+ /* Responsive design adjustments */
659
+ @media (max-width: 480px) {
660
+ .popup__mobile {
661
+ border-radius: 0;
662
+ max-height: 100vh;
663
+ }
664
+ .popup__mobile-content {
665
+ max-height: calc(100vh - 60px);
666
+ }
667
+ }
668
+ /* Reduced motion support */
669
+ @media (prefers-reduced-motion: reduce) {
670
+ :popover-open,
671
+ :popover-open::backdrop,
672
+ :popover-open.popup--fade-out,
673
+ :popover-open.popup--fade-out::backdrop {
674
+ animation-duration: 0.01s;
675
+ }
676
+ }</style>
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 🚨 CRITICAL: DO NOT MANAGE POPUP STATE FROM PARENT COMPONENTS
3
+ *
4
+ * This Popup component manages its own open/closed state internally.
5
+ * Parent components must NEVER create their own popup state variables.
6
+ *
7
+ * ❌ WRONG: let isPopupOpen = $state(false)
8
+ * ✅ RIGHT: Use popupRef.open(), popupRef.close(), popupRef.toggle()
9
+ * ✅ RIGHT: Use onOpen/onClose callbacks for side effects
10
+ *
11
+ * This prevents state synchronization bugs and ensures consistent behavior.
12
+ */
13
+ import type { Snippet } from 'svelte';
14
+ type $$ComponentProps = {
15
+ children: Snippet;
16
+ anchorElement: HTMLElement;
17
+ role?: string;
18
+ id?: string;
19
+ 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';
20
+ margin?: number;
21
+ isOpen?: boolean;
22
+ focusTrap?: boolean;
23
+ restoreFocus?: boolean;
24
+ mobileFullscreen?: boolean;
25
+ mobileBehavior?: 'auto' | 'fullscreen' | 'popup';
26
+ allowRepositioning?: boolean;
27
+ ariaLabel?: string;
28
+ ariaLabelledby?: string;
29
+ ariaDescribedby?: string;
30
+ onOpen?: () => void;
31
+ onClose?: () => void;
32
+ };
33
+ declare const Popup: import("svelte").Component<$$ComponentProps, {
34
+ open: () => Promise<void>;
35
+ close: () => void;
36
+ toggle: () => void;
37
+ getIsOpen: () => boolean;
38
+ }, "isOpen">;
39
+ type Popup = ReturnType<typeof Popup>;
40
+ export default Popup;