@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,75 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { IconVariant, IconWeight, IconGrade, IconOpticalSize } from '../types/icon';
3
+ type $$ComponentProps = {
4
+ name?: string;
5
+ value: string | number | undefined;
6
+ id?: string | null;
7
+ type?: 'text' | 'password' | 'email' | 'tel' | 'url' | 'number';
8
+ tabindex?: number | null;
9
+ maxlength?: number | null;
10
+ pattern?: string | null;
11
+ min?: number | null;
12
+ max?: number | null;
13
+ step?: number | null;
14
+ size?: number | null;
15
+ autocomplete?: HTMLInputElement['autocomplete'] | null;
16
+ inputAttributes?: HTMLInputAttributes | undefined;
17
+ inline?: boolean;
18
+ focusStyle?: 'background' | 'outline' | 'none';
19
+ placeholder?: string;
20
+ fullWidth?: boolean;
21
+ width?: string | number | null;
22
+ minWidth?: string | number | null;
23
+ maxWidth?: string | number | null;
24
+ rounded?: boolean;
25
+ customStyle?: string;
26
+ rightIcon?: string;
27
+ leftIcon?: string;
28
+ leftIconAriaLabel?: string;
29
+ rightIconAriaLabel?: string;
30
+ iconFilled?: boolean;
31
+ iconWeight?: IconWeight;
32
+ iconGrade?: IconGrade;
33
+ iconOpticalSize?: IconOpticalSize;
34
+ iconVariant?: IconVariant;
35
+ disabled?: boolean;
36
+ readonly?: boolean;
37
+ required?: boolean;
38
+ clearable?: boolean;
39
+ onfocus?: Function;
40
+ onblur?: Function;
41
+ onkeydown?: Function;
42
+ onkeyup?: Function;
43
+ onclick?: Function;
44
+ onmousedown?: Function;
45
+ onmouseup?: Function;
46
+ onmouseenter?: Function;
47
+ onmouseleave?: Function;
48
+ onmouseover?: Function;
49
+ onmouseout?: Function;
50
+ oncontextmenu?: Function;
51
+ onauxclick?: Function;
52
+ ontouchstart?: Function;
53
+ ontouchend?: Function;
54
+ ontouchmove?: Function;
55
+ ontouchcancel?: Function;
56
+ onpointerdown?: Function;
57
+ onpointerup?: Function;
58
+ onpointerenter?: Function;
59
+ onpointerleave?: Function;
60
+ onpointermove?: Function;
61
+ onpointercancel?: Function;
62
+ onchange?: (value: any) => void;
63
+ oninput?: (value: any) => void;
64
+ onsubmit?: (value: any) => void;
65
+ oncompositionstart?: Function;
66
+ oncompositionend?: Function;
67
+ onRightIconClick?: Function;
68
+ onLeftIconClick?: Function;
69
+ [key: string]: any;
70
+ };
71
+ declare const Input: import("svelte").Component<$$ComponentProps, {
72
+ focus: () => void;
73
+ }, "value">;
74
+ type Input = ReturnType<typeof Input>;
75
+ export default Input;
@@ -0,0 +1,116 @@
1
+ <!-- LoadingSpinner.svelte -->
2
+
3
+ <script lang="ts">
4
+ import { getStyleFromNumber } from '../utils/style';
5
+ // =========================================================================
6
+ // Props, States & Constants
7
+ // =========================================================================
8
+ let {
9
+ // スタイル/レイアウト
10
+ size = 32,
11
+ color,
12
+ strokeWidth = 3,
13
+
14
+ // 状態/動作
15
+ speed = 1,
16
+ reducedMotion = false
17
+ }: {
18
+ // スタイル/レイアウト
19
+ size?: number;
20
+ color?: string;
21
+ strokeWidth?: number;
22
+
23
+ // 状態/動作
24
+ speed?: number;
25
+ reducedMotion?: boolean;
26
+ } = $props();
27
+
28
+ // =========================================================================
29
+ // $derived
30
+ // =========================================================================
31
+ const radius = $derived((size - strokeWidth) / 2);
32
+ const circumference = $derived(2 * Math.PI * radius);
33
+ const halfCircumference = $derived((circumference * 3) / 4);
34
+ const negativeHalfCircumference = $derived(-halfCircumference);
35
+ const growDuration = $derived(1.6 / speed);
36
+ const rotateDuration = $derived(0.8 / speed);
37
+ </script>
38
+
39
+ <div
40
+ class="loading-spinner"
41
+ class:spinner--no-motion={reducedMotion}
42
+ style:--grow-duration="{growDuration}s"
43
+ style:--rotate-duration="{rotateDuration}s"
44
+ style:--spinner-size="{size}px"
45
+ style:--spinner-color={color}
46
+ style:--circumference={circumference}
47
+ style:--half-circumference={halfCircumference}
48
+ style:--negative-half-circumference={negativeHalfCircumference}
49
+ data-testid="loading-spinner"
50
+ >
51
+ <svg viewBox="0 0 {size} {size}" width={size} height={size}>
52
+ <circle cx={size / 2} cy={size / 2} r={radius} style:stroke-width={strokeWidth} />
53
+ </svg>
54
+ </div>
55
+
56
+ <style>
57
+ .loading-spinner {
58
+ width: var(--spinner-size, var(--svelte-ui-loadingspinner-size));
59
+ height: var(--spinner-size, var(--svelte-ui-loadingspinner-size));
60
+ line-height: 1px;
61
+ }
62
+
63
+ .loading-spinner svg {
64
+ width: 100%;
65
+ height: 100%;
66
+ transform: rotate(-90deg);
67
+ animation: rotate var(--rotate-duration, 0.8s) linear infinite;
68
+ }
69
+
70
+ .loading-spinner circle {
71
+ fill: none;
72
+ stroke: var(--spinner-color, var(--svelte-ui-loadingspinner-color));
73
+ stroke-linecap: butt;
74
+ animation: complex-grow var(--grow-duration, 1.6s) linear infinite;
75
+ transform-origin: center;
76
+ }
77
+
78
+ @keyframes rotate {
79
+ 0% {
80
+ transform: rotate(-90deg);
81
+ }
82
+ 100% {
83
+ transform: rotate(270deg);
84
+ }
85
+ }
86
+
87
+ @keyframes complex-grow {
88
+ /* 負の値も別のCSS変数として定義 */
89
+ 0% {
90
+ stroke-dasharray: 0 var(--circumference);
91
+ stroke-dashoffset: var(--negative-half-circumference);
92
+ }
93
+ 50% {
94
+ stroke-dasharray: var(--half-circumference) var(--circumference);
95
+ stroke-dashoffset: 0;
96
+ }
97
+ 100% {
98
+ stroke-dasharray: 0 var(--circumference);
99
+ stroke-dashoffset: var(--negative-half-circumference);
100
+ }
101
+ }
102
+
103
+ /* Reduced motion */
104
+ .spinner--no-motion svg,
105
+ .spinner--no-motion circle {
106
+ animation-duration: 0.01s;
107
+ }
108
+
109
+ /* Prefers reduced motion */
110
+ @media (prefers-reduced-motion: reduce) {
111
+ .loading-spinner svg,
112
+ .loading-spinner circle {
113
+ animation-duration: 0.01s;
114
+ }
115
+ }
116
+ </style>
@@ -0,0 +1,10 @@
1
+ type $$ComponentProps = {
2
+ size?: number;
3
+ color?: string;
4
+ strokeWidth?: number;
5
+ speed?: number;
6
+ reducedMotion?: boolean;
7
+ };
8
+ declare const LoadingSpinner: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type LoadingSpinner = ReturnType<typeof LoadingSpinner>;
10
+ export default LoadingSpinner;
@@ -0,0 +1,313 @@
1
+ <!-- Modal.svelte -->
2
+ <script lang="ts">
3
+ /**
4
+ * 🚨 CRITICAL: DO NOT MANAGE MODAL STATE FROM PARENT COMPONENTS
5
+ *
6
+ * This Modal component manages its own open/closed state internally.
7
+ * Parent components must NEVER create their own Modal state variables.
8
+ *
9
+ * ❌ WRONG: let isModalOpen = $state(false)
10
+ * ✅ RIGHT: Use modalRef.open(), modalRef.close(), modalRef.toggle()
11
+ * ✅ RIGHT: Use onOpen/onClose callbacks for side effects
12
+ *
13
+ * This prevents state synchronization bugs and ensures consistent behavior.
14
+ */
15
+ import type { Snippet } from 'svelte';
16
+ import { announceOpenClose } from '../utils/accessibility';
17
+
18
+ // =========================================================================
19
+ // Props, States & Constants
20
+ // =========================================================================
21
+ let {
22
+ // Snippet
23
+ children,
24
+
25
+ // 基本プロパティ
26
+ componentType = 'Modal',
27
+
28
+ // スタイル/レイアウト
29
+ customClass = '',
30
+ customStyles = '',
31
+
32
+ // 状態/動作
33
+ isOpen = $bindable(false),
34
+ closeIfClickOutside = true,
35
+ restoreFocus = false,
36
+
37
+ // ARIA/アクセシビリティ
38
+ ariaLabel,
39
+ ariaLabelledby,
40
+ ariaDescribedby,
41
+
42
+ // HTML属性
43
+ id
44
+ }: {
45
+ // Snippet
46
+ children?: Snippet;
47
+
48
+ // 基本プロパティ
49
+ componentType?: string;
50
+
51
+ // スタイル/レイアウト
52
+ customClass?: string;
53
+ customStyles?: string;
54
+
55
+ // 状態/動作
56
+ isOpen?: boolean;
57
+ closeIfClickOutside?: boolean;
58
+ restoreFocus?: boolean;
59
+
60
+ // ARIA/アクセシビリティ
61
+ ariaLabel?: string;
62
+ ariaLabelledby?: string;
63
+ ariaDescribedby?: string;
64
+
65
+ // HTML属性
66
+ id?: string;
67
+ } = $props();
68
+
69
+ let dialogRef: HTMLDialogElement;
70
+ let containerRef: HTMLDivElement;
71
+ let previousActiveElement: HTMLElement | null = null;
72
+
73
+ // =========================================================================
74
+ // Effects
75
+ // =========================================================================
76
+ $effect(() => {
77
+ if (!dialogRef || !isOpen) return;
78
+
79
+ const handleClick = (event: MouseEvent) => {
80
+ if (!closeIfClickOutside) return;
81
+ if (!containerRef || !event.target) return;
82
+ if (!containerRef.contains(event.target as Node)) {
83
+ close();
84
+ }
85
+ };
86
+
87
+ dialogRef.addEventListener('click', handleClick);
88
+
89
+ return () => {
90
+ if (dialogRef) {
91
+ dialogRef.removeEventListener('click', handleClick);
92
+ }
93
+ };
94
+ });
95
+
96
+ $effect(() => {
97
+ if (!isOpen || !dialogRef) return;
98
+
99
+ const handleKeyDown = (event: KeyboardEvent) => {
100
+ if (event.key === 'Escape') {
101
+ close();
102
+ }
103
+ };
104
+
105
+ document.addEventListener('keydown', handleKeyDown);
106
+
107
+ return () => {
108
+ document.removeEventListener('keydown', handleKeyDown);
109
+ };
110
+ });
111
+
112
+ $effect(() => {
113
+ if (!isOpen || !dialogRef) return;
114
+
115
+ const handleKeyDown = (event: KeyboardEvent) => {
116
+ if (event.key !== 'Tab') return;
117
+
118
+ const focusableElements = dialogRef.querySelectorAll(
119
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
120
+ );
121
+
122
+ if (focusableElements.length === 0) return;
123
+
124
+ const firstElement = focusableElements[0] as HTMLElement;
125
+ const lastElement = focusableElements[focusableElements.length - 1] as HTMLElement;
126
+
127
+ if (event.shiftKey) {
128
+ if (document.activeElement === firstElement) {
129
+ event.preventDefault();
130
+ lastElement?.focus();
131
+ }
132
+ } else {
133
+ if (document.activeElement === lastElement) {
134
+ event.preventDefault();
135
+ firstElement?.focus();
136
+ }
137
+ }
138
+ };
139
+
140
+ dialogRef.addEventListener('keydown', handleKeyDown);
141
+
142
+ return () => {
143
+ if (dialogRef) {
144
+ dialogRef.removeEventListener('keydown', handleKeyDown);
145
+ }
146
+ };
147
+ });
148
+
149
+ $effect(() => {
150
+ if (dialogRef) {
151
+ if (isOpen) {
152
+ open();
153
+ } else {
154
+ close();
155
+ }
156
+ }
157
+ });
158
+
159
+ // =========================================================================
160
+ // Methods
161
+ // =========================================================================
162
+ export const open = (title?: string): void => {
163
+ if (!dialogRef) return;
164
+
165
+ isOpen = true;
166
+ previousActiveElement = document.activeElement as HTMLElement;
167
+
168
+ dialogRef.classList.add('fade-in');
169
+ dialogRef.removeEventListener('animationend', closeEnd);
170
+ dialogRef.showModal();
171
+
172
+ setTimeout(() => {
173
+ const firstFocusableElement = dialogRef?.querySelector(
174
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
175
+ ) as HTMLElement;
176
+ firstFocusableElement?.focus();
177
+
178
+ announceOpenClose(componentType, true, title || ariaLabel || '');
179
+ }, 0);
180
+
181
+ // 自動フォーカス時の枠線制御用クラス
182
+ dialogRef.classList.add('modal-opening');
183
+ setTimeout(() => {
184
+ dialogRef?.classList.remove('modal-opening');
185
+ }, 100); // 短い時間で制御
186
+ };
187
+
188
+ export const close = (title?: string): void => {
189
+ if (!dialogRef) return;
190
+
191
+ isOpen = false;
192
+ dialogRef.classList.add('fade-out');
193
+ dialogRef.addEventListener('animationend', closeEnd, { once: true });
194
+
195
+ announceOpenClose(componentType, false, title || ariaLabel || '');
196
+ };
197
+
198
+ export const closeEnd = (): void => {
199
+ if (!dialogRef) return;
200
+
201
+ dialogRef.close();
202
+ dialogRef.classList.remove('fade-out');
203
+
204
+ if (restoreFocus && previousActiveElement) {
205
+ previousActiveElement.focus();
206
+ }
207
+ previousActiveElement = null;
208
+ };
209
+
210
+ export const toggle = (title?: string): void => {
211
+ if (isOpen) {
212
+ close(title);
213
+ } else {
214
+ open(title);
215
+ }
216
+ };
217
+ </script>
218
+
219
+ <dialog
220
+ bind:this={dialogRef}
221
+ class="modal {customClass} {isOpen ? 'fade-in' : 'fade-out'}"
222
+ style={customStyles}
223
+ aria-modal="true"
224
+ aria-label={ariaLabel}
225
+ aria-labelledby={ariaLabelledby}
226
+ aria-describedby={ariaDescribedby}
227
+ data-testid="modal"
228
+ {id}
229
+ >
230
+ <div class="modal-contents" bind:this={containerRef}>
231
+ {#if children}
232
+ {@render children()}
233
+ {/if}
234
+ </div>
235
+ </dialog>
236
+
237
+ <style>@charset "UTF-8";
238
+ .modal {
239
+ padding: 0;
240
+ border-width: 0;
241
+ background-color: var(--svelte-ui-surface-color);
242
+ 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);
243
+ }
244
+
245
+ .modal::backdrop {
246
+ position: fixed;
247
+ inset: 0;
248
+ background: rgba(0, 0, 0, 0.4);
249
+ }
250
+
251
+ .modal:focus {
252
+ outline: none;
253
+ }
254
+
255
+ /* Modal内の要素のフォーカス枠線をコントロール */
256
+ .modal *:focus {
257
+ outline: none;
258
+ }
259
+
260
+ /* キーボードナビゲーション時のみフォーカス枠線を表示 */
261
+ .modal *:focus-visible {
262
+ outline: var(--svelte-ui-focus-outline-outer);
263
+ outline-offset: var(--svelte-ui-focus-outline-offset-outer);
264
+ }
265
+
266
+ /* 自動フォーカス時の枠線制御用 */
267
+ .modal.modal-opening *:focus {
268
+ outline: none !important;
269
+ }
270
+
271
+ .modal-contents {
272
+ width: 100%;
273
+ height: 100%;
274
+ display: flex;
275
+ flex-direction: column;
276
+ }
277
+
278
+ /* アニメーション */
279
+ @keyframes fadeIn {
280
+ from {
281
+ opacity: 0;
282
+ }
283
+ to {
284
+ opacity: 1;
285
+ }
286
+ }
287
+ @keyframes fadeOut {
288
+ from {
289
+ opacity: 1;
290
+ }
291
+ to {
292
+ opacity: 0;
293
+ }
294
+ }
295
+ .fade-in,
296
+ .fade-in::backdrop {
297
+ animation: fadeIn var(--svelte-ui-transition-duration, 300ms) forwards;
298
+ }
299
+
300
+ .fade-out,
301
+ .fade-out::backdrop {
302
+ animation: fadeOut var(--svelte-ui-transition-duration, 300ms) forwards;
303
+ }
304
+
305
+ /* Reduced motion support */
306
+ @media (prefers-reduced-motion: reduce) {
307
+ .fade-in,
308
+ .fade-in::backdrop,
309
+ .fade-out,
310
+ .fade-out::backdrop {
311
+ animation-duration: 0.01s;
312
+ }
313
+ }</style>
@@ -0,0 +1,34 @@
1
+ /**
2
+ * 🚨 CRITICAL: DO NOT MANAGE MODAL STATE FROM PARENT COMPONENTS
3
+ *
4
+ * This Modal component manages its own open/closed state internally.
5
+ * Parent components must NEVER create their own Modal state variables.
6
+ *
7
+ * ❌ WRONG: let isModalOpen = $state(false)
8
+ * ✅ RIGHT: Use modalRef.open(), modalRef.close(), modalRef.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
+ componentType?: string;
17
+ customClass?: string;
18
+ customStyles?: string;
19
+ isOpen?: boolean;
20
+ closeIfClickOutside?: boolean;
21
+ restoreFocus?: boolean;
22
+ ariaLabel?: string;
23
+ ariaLabelledby?: string;
24
+ ariaDescribedby?: string;
25
+ id?: string;
26
+ };
27
+ declare const Modal: import("svelte").Component<$$ComponentProps, {
28
+ open: (title?: string) => void;
29
+ close: (title?: string) => void;
30
+ closeEnd: () => void;
31
+ toggle: (title?: string) => void;
32
+ }, "isOpen">;
33
+ type Modal = ReturnType<typeof Modal>;
34
+ export default Modal;