svelte-comp 1.2.7 → 1.3.5
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.
- package/README.md +12 -11
- package/dist/App.svelte +497 -2
- package/dist/app.css +2 -3
- package/dist/app.d.ts +10 -0
- package/dist/lang.d.ts +3 -3
- package/dist/lang.js +3 -3
- package/dist/lib/Accordion.svelte +14 -14
- package/dist/lib/Badge.svelte +44 -0
- package/dist/lib/Badge.svelte.d.ts +10 -0
- package/dist/lib/Button.svelte +23 -8
- package/dist/lib/Calendar.svelte +384 -377
- package/dist/lib/Card.svelte +6 -6
- package/dist/lib/Carousel.svelte +16 -16
- package/dist/lib/Carousel.svelte.d.ts +1 -1
- package/dist/lib/CheckBox.svelte +2 -2
- package/dist/lib/CodeView.svelte +6 -5
- package/dist/lib/ColorPicker.svelte +6 -6
- package/dist/lib/ContextMenu.svelte +328 -0
- package/dist/lib/ContextMenu.svelte.d.ts +14 -0
- package/dist/lib/DatePicker.svelte +161 -161
- package/dist/lib/Dialog.svelte +10 -10
- package/dist/lib/Field.svelte +1 -1
- package/dist/lib/FilePicker.svelte +127 -74
- package/dist/lib/FilePicker.svelte.d.ts +6 -3
- package/dist/lib/Hamburger.svelte +27 -21
- package/dist/lib/InstallPWA.svelte +94 -0
- package/dist/lib/InstallPWA.svelte.d.ts +8 -0
- package/dist/lib/Menu.svelte +18 -18
- package/dist/lib/NoticeBase.svelte +140 -0
- package/dist/lib/NoticeBase.svelte.d.ts +43 -0
- package/dist/lib/PrimaryColorSelect.svelte +6 -6
- package/dist/lib/ProgressCircle.svelte +7 -9
- package/dist/lib/ProgressCircle.svelte.d.ts +7 -9
- package/dist/lib/SearchInput.svelte +6 -6
- package/dist/lib/Select.svelte +2 -2
- package/dist/lib/Slider.svelte +1 -1
- package/dist/lib/Splitter.svelte +15 -6
- package/dist/lib/Switch.svelte +5 -4
- package/dist/lib/Tabs.svelte +6 -6
- package/dist/lib/ThemeToggle.svelte +1 -0
- package/dist/lib/TimePicker.svelte +103 -95
- package/dist/lib/TimePickerNew.svelte +634 -0
- package/dist/lib/TimePickerNew.svelte.d.ts +49 -0
- package/dist/lib/Toast.svelte +17 -120
- package/dist/lib/Tooltip.svelte +7 -7
- package/dist/lib/Topbar.svelte +112 -0
- package/dist/lib/Topbar.svelte.d.ts +44 -0
- package/dist/lib/__tests__/Accordion.test.d.ts +1 -0
- package/dist/lib/__tests__/Accordion.test.js +171 -0
- package/dist/lib/__tests__/Badge.test.d.ts +1 -0
- package/dist/lib/__tests__/Badge.test.js +41 -0
- package/dist/lib/__tests__/Button.test.d.ts +1 -0
- package/dist/lib/__tests__/Button.test.js +269 -0
- package/dist/lib/__tests__/Calendar.test.d.ts +1 -0
- package/dist/lib/__tests__/Calendar.test.js +171 -0
- package/dist/lib/__tests__/Card.test.d.ts +1 -0
- package/dist/lib/__tests__/Card.test.js +148 -0
- package/dist/lib/__tests__/Carousel.test.d.ts +1 -0
- package/dist/lib/__tests__/Carousel.test.js +439 -0
- package/dist/lib/__tests__/CheckBox.test.d.ts +1 -0
- package/dist/lib/__tests__/CheckBox.test.js +152 -0
- package/dist/lib/__tests__/CodeView.test.d.ts +1 -0
- package/dist/lib/__tests__/CodeView.test.js +157 -0
- package/dist/lib/__tests__/ColorPicker.test.d.ts +1 -0
- package/dist/lib/__tests__/ColorPicker.test.js +93 -0
- package/dist/lib/__tests__/ContextMenu.test.d.ts +1 -0
- package/dist/lib/__tests__/ContextMenu.test.js +67 -0
- package/dist/lib/__tests__/DatePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/DatePicker.test.js +108 -0
- package/dist/lib/__tests__/Dialog.test.d.ts +1 -0
- package/dist/lib/__tests__/Dialog.test.js +183 -0
- package/dist/lib/__tests__/Field.test.d.ts +1 -0
- package/dist/lib/__tests__/Field.test.js +190 -0
- package/dist/lib/__tests__/FilePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/FilePicker.test.js +179 -0
- package/dist/lib/__tests__/Form.integration.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.integration.test.js +158 -0
- package/dist/lib/__tests__/Form.test.d.ts +1 -0
- package/dist/lib/__tests__/Form.test.js +463 -0
- package/dist/lib/__tests__/Hamburger.test.d.ts +1 -0
- package/dist/lib/__tests__/Hamburger.test.js +161 -0
- package/dist/lib/__tests__/InstallPWA.test.d.ts +1 -0
- package/dist/lib/__tests__/InstallPWA.test.js +15 -0
- package/dist/lib/__tests__/Menu.test.d.ts +1 -0
- package/dist/lib/__tests__/Menu.test.js +285 -0
- package/dist/lib/__tests__/NoticeBase.test.d.ts +1 -0
- package/dist/lib/__tests__/NoticeBase.test.js +60 -0
- package/dist/lib/__tests__/PaginatedCard.test.d.ts +1 -0
- package/dist/lib/__tests__/PaginatedCard.test.js +89 -0
- package/dist/lib/__tests__/Pagination.test.d.ts +1 -0
- package/dist/lib/__tests__/Pagination.test.js +168 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.d.ts +1 -0
- package/dist/lib/__tests__/PrimaryColorSelect.test.js +92 -0
- package/dist/lib/__tests__/ProgressBar.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressBar.test.js +69 -0
- package/dist/lib/__tests__/ProgressCircle.test.d.ts +1 -0
- package/dist/lib/__tests__/ProgressCircle.test.js +71 -0
- package/dist/lib/__tests__/Radio.test.d.ts +1 -0
- package/dist/lib/__tests__/Radio.test.js +127 -0
- package/dist/lib/__tests__/SearchInput.test.d.ts +1 -0
- package/dist/lib/__tests__/SearchInput.test.js +80 -0
- package/dist/lib/__tests__/Select.test.d.ts +1 -0
- package/dist/lib/__tests__/Select.test.js +408 -0
- package/dist/lib/__tests__/Slider.test.d.ts +1 -0
- package/dist/lib/__tests__/Slider.test.js +213 -0
- package/dist/lib/__tests__/Splitter.test.d.ts +1 -0
- package/dist/lib/__tests__/Splitter.test.js +87 -0
- package/dist/lib/__tests__/Switch.test.d.ts +1 -0
- package/dist/lib/__tests__/Switch.test.js +97 -0
- package/dist/lib/__tests__/Table.test.d.ts +1 -0
- package/dist/lib/__tests__/Table.test.js +349 -0
- package/dist/lib/__tests__/Tabs.test.d.ts +1 -0
- package/dist/lib/__tests__/Tabs.test.js +262 -0
- package/dist/lib/__tests__/ThemeToggle.test.d.ts +1 -0
- package/dist/lib/__tests__/ThemeToggle.test.js +84 -0
- package/dist/lib/__tests__/TimePicker.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePicker.test.js +146 -0
- package/dist/lib/__tests__/TimePickerNew.test.d.ts +1 -0
- package/dist/lib/__tests__/TimePickerNew.test.js +322 -0
- package/dist/lib/__tests__/Toast.test.d.ts +1 -0
- package/dist/lib/__tests__/Toast.test.js +135 -0
- package/dist/lib/__tests__/Tooltip.test.d.ts +1 -0
- package/dist/lib/__tests__/Tooltip.test.js +171 -0
- package/dist/lib/__tests__/Topbar.test.d.ts +1 -0
- package/dist/lib/__tests__/Topbar.test.js +25 -0
- package/dist/lib/__tests__/setupLangContext.d.ts +1 -0
- package/dist/lib/__tests__/setupLangContext.js +65 -0
- package/dist/lib/__tests__/storage.test.d.ts +1 -0
- package/dist/lib/__tests__/storage.test.js +124 -0
- package/dist/lib/__tests__/utils.test.d.ts +1 -0
- package/dist/lib/__tests__/utils.test.js +11 -0
- package/dist/lib/index.d.ts +5 -0
- package/dist/lib/index.js +5 -0
- package/dist/lib/lang.d.ts +64 -0
- package/dist/lib/lang.js +64 -0
- package/dist/lib/types/index.d.ts +1 -0
- package/dist/styles.css +2 -0
- package/dist/utils/index.js +15 -4
- package/package.json +12 -12
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
* @note Preview card includes `aria-live="polite"` to announce updated dates.
|
|
38
38
|
*/
|
|
39
39
|
import type { HTMLAttributes } from "svelte/elements";
|
|
40
|
-
import Button from "./Button.svelte";
|
|
41
|
-
import Calendar from "./Calendar.svelte";
|
|
42
|
-
import { cx, formatDate, uid } from "../utils";
|
|
43
|
-
import { getComponentText, getLangContext, getLangKey } from "./lang-context";
|
|
40
|
+
import Button from "./Button.svelte";
|
|
41
|
+
import Calendar from "./Calendar.svelte";
|
|
42
|
+
import { cx, formatDate, uid } from "../utils";
|
|
43
|
+
import { getComponentText, getLangContext, getLangKey } from "./lang-context";
|
|
44
44
|
|
|
45
45
|
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
46
46
|
value?: string | null;
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
|
|
59
59
|
let {
|
|
60
60
|
value = $bindable<string | null>(null),
|
|
61
|
-
min = "1926-01-01",
|
|
61
|
+
min = "1926-01-01",
|
|
62
62
|
max,
|
|
63
63
|
label,
|
|
64
64
|
placeholder,
|
|
@@ -71,166 +71,166 @@
|
|
|
71
71
|
...rest
|
|
72
72
|
}: Props = $props();
|
|
73
73
|
|
|
74
|
-
const langCtx = getLangContext();
|
|
75
|
-
const langKey = $derived(getLangKey(langCtx));
|
|
76
|
-
const L = $derived(getComponentText("datePicker", langKey));
|
|
74
|
+
const langCtx = getLangContext();
|
|
75
|
+
const langKey = $derived(getLangKey(langCtx));
|
|
76
|
+
const L = $derived(getComponentText("datePicker", langKey));
|
|
77
77
|
|
|
78
78
|
const labelFinal = $derived(label ?? L.text);
|
|
79
79
|
const placeholderFinal = $derived(placeholder ?? L.placeholder);
|
|
80
80
|
|
|
81
|
-
const base = "inline-block w-full";
|
|
82
|
-
const pickerClass = $derived(cx(base, externalClass));
|
|
83
|
-
|
|
84
|
-
const hasValue = $derived(Boolean(value));
|
|
85
|
-
const formattedValue = $derived(formatDate(value, locale, formatOptions));
|
|
86
|
-
|
|
87
|
-
let open = $state(false);
|
|
88
|
-
const panelId = uid("calendar-");
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
let triggerEl = $state<HTMLDivElement | null>(null);
|
|
92
|
-
let panelEl = $state<HTMLDivElement | null>(null);
|
|
93
|
-
let panelTop = $state(0);
|
|
94
|
-
let panelLeft = $state(0);
|
|
95
|
-
|
|
96
|
-
const panelStyle = $derived(
|
|
97
|
-
`position:fixed; top:${panelTop}px; left:${panelLeft}px; width:${
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
function togglePanel() {
|
|
101
|
-
if (disabled) return;
|
|
102
|
-
open = !open;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function handleSelect(nextValue: string | null) {
|
|
106
|
-
value = nextValue;
|
|
107
|
-
onChange?.(nextValue);
|
|
108
|
-
open = false;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function clearSelection() {
|
|
112
|
-
if (!clearable) return;
|
|
113
|
-
value = null;
|
|
114
|
-
onChange?.(null);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
$effect(() => {
|
|
118
|
-
const currentTrigger = triggerEl;
|
|
119
|
-
const currentPanel = panelEl;
|
|
120
|
-
if (!open || !currentTrigger) return;
|
|
121
|
-
|
|
122
|
-
const updatePosition = () => {
|
|
123
|
-
const triggerRect = currentTrigger.getBoundingClientRect();
|
|
124
|
-
const panelHeight =
|
|
125
|
-
currentPanel?.getBoundingClientRect().height ?? 0;
|
|
126
|
-
const spaceBelow = window.innerHeight - triggerRect.bottom;
|
|
127
|
-
const spaceAbove = triggerRect.top;
|
|
128
|
-
const shouldFlip = spaceBelow < panelHeight && spaceAbove > spaceBelow;
|
|
129
|
-
|
|
130
|
-
panelTop = shouldFlip
|
|
131
|
-
? Math.max(0, triggerRect.top - panelHeight - 8)
|
|
132
|
-
: triggerRect.bottom + 8;
|
|
133
|
-
|
|
134
|
-
const viewportLeft = window.scrollX;
|
|
135
|
-
const viewportRight = window.scrollX + window.innerWidth;
|
|
136
|
-
const targetLeft = triggerRect.left + window.scrollX;
|
|
137
|
-
const maxLeft = viewportRight -
|
|
138
|
-
panelLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
const onKeyDown = (event: KeyboardEvent) => {
|
|
142
|
-
if (event.key === "Escape") {
|
|
143
|
-
event.preventDefault();
|
|
144
|
-
open = false;
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
const onClickOutside = (event: MouseEvent) => {
|
|
149
|
-
if (
|
|
150
|
-
currentTrigger &&
|
|
151
|
-
currentPanel &&
|
|
152
|
-
!currentTrigger.contains(event.target as Node) &&
|
|
153
|
-
!currentPanel.contains(event.target as Node)
|
|
154
|
-
) {
|
|
155
|
-
open = false;
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
queueMicrotask(updatePosition);
|
|
160
|
-
window.addEventListener("scroll", updatePosition, true);
|
|
161
|
-
window.addEventListener("resize", updatePosition);
|
|
162
|
-
window.addEventListener("keydown", onKeyDown);
|
|
163
|
-
document.addEventListener("mousedown", onClickOutside);
|
|
164
|
-
|
|
165
|
-
return () => {
|
|
166
|
-
window.removeEventListener("scroll", updatePosition, true);
|
|
167
|
-
window.removeEventListener("resize", updatePosition);
|
|
168
|
-
window.removeEventListener("keydown", onKeyDown);
|
|
169
|
-
document.removeEventListener("mousedown", onClickOutside);
|
|
170
|
-
};
|
|
171
|
-
});
|
|
172
|
-
</script>
|
|
173
|
-
|
|
174
|
-
<div class={pickerClass} {...rest}>
|
|
175
|
-
<div class="text-md font-medium mb-2 [color:var(--color-text-default)]">
|
|
176
|
-
{labelFinal}:
|
|
177
|
-
</div>
|
|
178
|
-
<div class="inline-flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
179
|
-
<div bind:this={triggerEl}>
|
|
180
|
-
<Button
|
|
181
|
-
onClick={togglePanel}
|
|
182
|
-
{disabled}
|
|
183
|
-
sz="xs"
|
|
184
|
-
aria-expanded={open}
|
|
185
|
-
aria-controls={open ? panelId : undefined}
|
|
186
|
-
>
|
|
187
|
-
{L.date}
|
|
188
|
-
</Button>
|
|
189
|
-
</div>
|
|
190
|
-
|
|
191
|
-
{#if clearable}
|
|
192
|
-
<Button
|
|
193
|
-
onClick={clearSelection}
|
|
194
|
-
variant="danger"
|
|
195
|
-
disabled={!hasValue || disabled}
|
|
196
|
-
sz="xs"
|
|
197
|
-
>
|
|
198
|
-
{L.clear}
|
|
199
|
-
</Button>
|
|
200
|
-
{/if}
|
|
201
|
-
|
|
202
|
-
{#if open}
|
|
203
|
-
<div
|
|
204
|
-
role="presentation"
|
|
205
|
-
tabindex="-1"
|
|
206
|
-
class="fixed inset-0 z-[var(--z-overlay)]"
|
|
207
|
-
></div>
|
|
208
|
-
|
|
209
|
-
<div
|
|
210
|
-
bind:this={panelEl}
|
|
211
|
-
id={panelId}
|
|
212
|
-
role="dialog"
|
|
213
|
-
aria-label={labelFinal}
|
|
214
|
-
class="z-[var(--z-modal)] p-3 border border-[var(--border-color-default)] rounded-[var(--radius-md)] bg-[var(--color-bg-surface)] shadow-[0_2px_6px_var(--shadow-color)]"
|
|
215
|
-
style={panelStyle}
|
|
216
|
-
>
|
|
217
|
-
<Calendar
|
|
218
|
-
value={value}
|
|
219
|
-
{min}
|
|
220
|
-
{max}
|
|
221
|
-
{locale}
|
|
222
|
-
onChange={handleSelect}
|
|
223
|
-
showOutsideDays
|
|
224
|
-
class="
|
|
225
|
-
/>
|
|
226
|
-
</div>
|
|
227
|
-
{/if}
|
|
228
|
-
</div>
|
|
229
|
-
|
|
230
|
-
<div
|
|
231
|
-
class="mt-3 p-4 bg-[var(--color-bg-surface)] text-center"
|
|
232
|
-
aria-live="polite"
|
|
233
|
-
>
|
|
81
|
+
const base = "inline-block w-full";
|
|
82
|
+
const pickerClass = $derived(cx(base, externalClass));
|
|
83
|
+
|
|
84
|
+
const hasValue = $derived(Boolean(value));
|
|
85
|
+
const formattedValue = $derived(formatDate(value, locale, formatOptions));
|
|
86
|
+
|
|
87
|
+
let open = $state(false);
|
|
88
|
+
const panelId = uid("calendar-");
|
|
89
|
+
const panelSize = 240;
|
|
90
|
+
|
|
91
|
+
let triggerEl = $state<HTMLDivElement | null>(null);
|
|
92
|
+
let panelEl = $state<HTMLDivElement | null>(null);
|
|
93
|
+
let panelTop = $state(0);
|
|
94
|
+
let panelLeft = $state(0);
|
|
95
|
+
|
|
96
|
+
const panelStyle = $derived(
|
|
97
|
+
`position:fixed; top:${panelTop}px; left:${panelLeft}px; width:${panelSize}px; height:${panelSize}px;`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
function togglePanel() {
|
|
101
|
+
if (disabled) return;
|
|
102
|
+
open = !open;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function handleSelect(nextValue: string | null) {
|
|
106
|
+
value = nextValue;
|
|
107
|
+
onChange?.(nextValue);
|
|
108
|
+
open = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function clearSelection() {
|
|
112
|
+
if (!clearable) return;
|
|
113
|
+
value = null;
|
|
114
|
+
onChange?.(null);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
$effect(() => {
|
|
118
|
+
const currentTrigger = triggerEl;
|
|
119
|
+
const currentPanel = panelEl;
|
|
120
|
+
if (!open || !currentTrigger) return;
|
|
121
|
+
|
|
122
|
+
const updatePosition = () => {
|
|
123
|
+
const triggerRect = currentTrigger.getBoundingClientRect();
|
|
124
|
+
const panelHeight =
|
|
125
|
+
currentPanel?.getBoundingClientRect().height ?? 0;
|
|
126
|
+
const spaceBelow = window.innerHeight - triggerRect.bottom;
|
|
127
|
+
const spaceAbove = triggerRect.top;
|
|
128
|
+
const shouldFlip = spaceBelow < panelHeight && spaceAbove > spaceBelow;
|
|
129
|
+
|
|
130
|
+
panelTop = shouldFlip
|
|
131
|
+
? Math.max(0, triggerRect.top - panelHeight - 8)
|
|
132
|
+
: triggerRect.bottom + 8;
|
|
133
|
+
|
|
134
|
+
const viewportLeft = window.scrollX;
|
|
135
|
+
const viewportRight = window.scrollX + window.innerWidth;
|
|
136
|
+
const targetLeft = triggerRect.left + window.scrollX;
|
|
137
|
+
const maxLeft = viewportRight - panelSize;
|
|
138
|
+
panelLeft = Math.max(viewportLeft, Math.min(targetLeft, maxLeft));
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const onKeyDown = (event: KeyboardEvent) => {
|
|
142
|
+
if (event.key === "Escape") {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
open = false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const onClickOutside = (event: MouseEvent) => {
|
|
149
|
+
if (
|
|
150
|
+
currentTrigger &&
|
|
151
|
+
currentPanel &&
|
|
152
|
+
!currentTrigger.contains(event.target as Node) &&
|
|
153
|
+
!currentPanel.contains(event.target as Node)
|
|
154
|
+
) {
|
|
155
|
+
open = false;
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
queueMicrotask(updatePosition);
|
|
160
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
161
|
+
window.addEventListener("resize", updatePosition);
|
|
162
|
+
window.addEventListener("keydown", onKeyDown);
|
|
163
|
+
document.addEventListener("mousedown", onClickOutside);
|
|
164
|
+
|
|
165
|
+
return () => {
|
|
166
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
167
|
+
window.removeEventListener("resize", updatePosition);
|
|
168
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
169
|
+
document.removeEventListener("mousedown", onClickOutside);
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
</script>
|
|
173
|
+
|
|
174
|
+
<div class={pickerClass} {...rest}>
|
|
175
|
+
<div class="text-md font-medium mb-2 [color:var(--color-text-default)]">
|
|
176
|
+
{labelFinal}:
|
|
177
|
+
</div>
|
|
178
|
+
<div class="inline-flex flex-wrap items-center gap-x-3 gap-y-2">
|
|
179
|
+
<div bind:this={triggerEl}>
|
|
180
|
+
<Button
|
|
181
|
+
onClick={togglePanel}
|
|
182
|
+
{disabled}
|
|
183
|
+
sz="xs"
|
|
184
|
+
aria-expanded={open}
|
|
185
|
+
aria-controls={open ? panelId : undefined}
|
|
186
|
+
>
|
|
187
|
+
{L.date}
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{#if clearable}
|
|
192
|
+
<Button
|
|
193
|
+
onClick={clearSelection}
|
|
194
|
+
variant="danger"
|
|
195
|
+
disabled={!hasValue || disabled}
|
|
196
|
+
sz="xs"
|
|
197
|
+
>
|
|
198
|
+
{L.clear}
|
|
199
|
+
</Button>
|
|
200
|
+
{/if}
|
|
201
|
+
|
|
202
|
+
{#if open}
|
|
203
|
+
<div
|
|
204
|
+
role="presentation"
|
|
205
|
+
tabindex="-1"
|
|
206
|
+
class="fixed inset-0 z-[var(--z-overlay)]"
|
|
207
|
+
></div>
|
|
208
|
+
|
|
209
|
+
<div
|
|
210
|
+
bind:this={panelEl}
|
|
211
|
+
id={panelId}
|
|
212
|
+
role="dialog"
|
|
213
|
+
aria-label={labelFinal}
|
|
214
|
+
class="z-[var(--z-modal)] p-3 border border-[var(--border-color-default)] rounded-[var(--radius-md)] bg-[var(--color-bg-surface)] shadow-[0_2px_6px_var(--shadow-color)]"
|
|
215
|
+
style={panelStyle}
|
|
216
|
+
>
|
|
217
|
+
<Calendar
|
|
218
|
+
value={value}
|
|
219
|
+
{min}
|
|
220
|
+
{max}
|
|
221
|
+
{locale}
|
|
222
|
+
onChange={handleSelect}
|
|
223
|
+
showOutsideDays
|
|
224
|
+
class="w-full h-full"
|
|
225
|
+
/>
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
<div
|
|
231
|
+
class="mt-3 p-4 bg-[var(--color-bg-surface)] text-center"
|
|
232
|
+
aria-live="polite"
|
|
233
|
+
>
|
|
234
234
|
<p class="text-xs uppercase tracking-wide [color:var(--color-text-muted)]">
|
|
235
235
|
{L.selectedDate}
|
|
236
236
|
</p>
|
package/dist/lib/Dialog.svelte
CHANGED
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
* @note `sz` adjusts both dialog padding and text sizes to match the rest of the system tokens.
|
|
43
43
|
*/
|
|
44
44
|
import type { Snippet } from "svelte";
|
|
45
|
-
import Button from "./Button.svelte";
|
|
46
|
-
import { type SizeKey, TEXT } from "./types";
|
|
47
|
-
import { cx, focusFirstInteractive, trapFocus } from "../utils";
|
|
48
|
-
import { getComponentText, getLangContext, getLangKey } from "./lang-context";
|
|
45
|
+
import Button from "./Button.svelte";
|
|
46
|
+
import { type SizeKey, TEXT } from "./types";
|
|
47
|
+
import { cx, focusFirstInteractive, trapFocus } from "../utils";
|
|
48
|
+
import { getComponentText, getLangContext, getLangKey } from "./lang-context";
|
|
49
49
|
|
|
50
50
|
type Props = {
|
|
51
51
|
open?: boolean;
|
|
@@ -73,9 +73,9 @@
|
|
|
73
73
|
children,
|
|
74
74
|
}: Props = $props();
|
|
75
75
|
|
|
76
|
-
const langCtx = getLangContext();
|
|
77
|
-
const langKey = $derived(getLangKey(langCtx));
|
|
78
|
-
const L = $derived(getComponentText("dialog", langKey));
|
|
76
|
+
const langCtx = getLangContext();
|
|
77
|
+
const langKey = $derived(getLangKey(langCtx));
|
|
78
|
+
const L = $derived(getComponentText("dialog", langKey));
|
|
79
79
|
|
|
80
80
|
let panelEl = $state<HTMLDivElement | null>(null);
|
|
81
81
|
let releaseFocus: (() => void) | null = null;
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
const panelBase =
|
|
101
|
-
"fusion-dialog bg-[var(--color-bg-surface)] rounded-[var(--radius-lg)] shadow-
|
|
101
|
+
"fusion-dialog bg-[var(--color-bg-surface)] rounded-[var(--radius-lg)] shadow-[0_8px_24px_var(--shadow-color)] min-w-0 max-w-[min(100%,28rem)] max-h-[calc(100svh-var(--spacing-lg)*2)] overflow-auto w-full border border-[var(--border-color-default)]";
|
|
102
102
|
|
|
103
103
|
const paddingBySize: Record<SizeKey, string> = {
|
|
104
104
|
xs: "p-[var(--spacing-sm)]",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
{#if open}
|
|
165
165
|
{#if modal}
|
|
166
166
|
<div
|
|
167
|
-
class="fixed inset-0 z-[var(--z-modal)] bg-oklch(0_0_0/var(--opacity-overlay)) flex items-center justify-center p-
|
|
167
|
+
class="fixed inset-0 z-[var(--z-modal)] bg-[oklch(0_0_0/var(--opacity-overlay))] flex items-center justify-center p-[var(--spacing-md)]"
|
|
168
168
|
role="presentation"
|
|
169
169
|
tabindex="-1"
|
|
170
170
|
onkeydown={handleKeydown}
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
</div>
|
|
196
196
|
{:else}
|
|
197
197
|
<div
|
|
198
|
-
class="fixed top-
|
|
198
|
+
class="fixed top-[var(--spacing-md)] right-[var(--spacing-md)] z-[var(--z-modal)] max-w-[calc(100vw-var(--spacing-md)*2)]"
|
|
199
199
|
role="dialog"
|
|
200
200
|
aria-modal="false"
|
|
201
201
|
aria-label={title}
|
package/dist/lib/Field.svelte
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
}: Props = $props();
|
|
87
87
|
|
|
88
88
|
const base =
|
|
89
|
-
"w-full outline-none transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)] box-border rounded-[var(--radius-md)] border focus:border-[var(--border-color-focus)] focus:ring-2 focus:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed";
|
|
89
|
+
"w-full outline-none transition-colors duration-[var(--transition-fast)] ease-[var(--timing-default)] box-border rounded-[var(--radius-md)] border focus:border-[var(--border-color-focus)] focus:ring-2 focus:ring-[var(--border-color-focus)] disabled:opacity-[var(--opacity-disabled)] disabled:cursor-not-allowed [@media(pointer:coarse)]:min-h-11";
|
|
90
90
|
|
|
91
91
|
const sizes: Record<SizeKey, string> = {
|
|
92
92
|
xs: "px-2 h-6",
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* @prop clearable {boolean} - Shows a clear button to reset selection
|
|
19
19
|
* @default true
|
|
20
20
|
*
|
|
21
|
+
* @prop maxBytes {number} - Maximum allowed file size in bytes
|
|
22
|
+
*
|
|
23
|
+
* @prop onError {(error: string) => void} - Fired when selected files are rejected
|
|
24
|
+
*
|
|
21
25
|
* @prop placeholder {string} - Placeholder text for the drop zone
|
|
22
26
|
*
|
|
23
27
|
* @prop value {FileList | null} - Controlled selected files (bindable)
|
|
@@ -25,21 +29,19 @@
|
|
|
25
29
|
*
|
|
26
30
|
* @prop onFilesSelected {(files: FileList | null) => void} - Fired when files are chosen
|
|
27
31
|
*
|
|
28
|
-
* @prop onError {(error: string) => void} - Fired on validation errors
|
|
29
|
-
*
|
|
30
32
|
* @prop class {string} - Additional classes for the wrapper
|
|
31
33
|
* @default ""
|
|
32
34
|
*
|
|
33
35
|
* @note The entire area is clickable and supports drag-and-drop.
|
|
34
36
|
* @note After a selection, the underlying input resets its value, so choosing the same file twice still triggers updates.
|
|
35
|
-
* @note `accept`
|
|
37
|
+
* @note `accept` and `maxBytes` are enforced for both input and dropped files.
|
|
36
38
|
* @note When `clearable=true`, the user can clear selected files and the callback receives `null`.
|
|
37
39
|
* @note When `disabled=true`, clicks, drag events, focus, and keyboard input are blocked.
|
|
38
40
|
*/
|
|
39
41
|
import type { HTMLAttributes } from "svelte/elements";
|
|
40
|
-
import Button from "./Button.svelte";
|
|
41
|
-
import { cx, formatFileSize } from "../utils";
|
|
42
|
-
import { getComponentText, getLangContext, getLangKey } from "./lang-context";
|
|
42
|
+
import Button from "./Button.svelte";
|
|
43
|
+
import { cx, formatFileSize } from "../utils";
|
|
44
|
+
import { getComponentText, getLangContext, getLangKey } from "./lang-context";
|
|
43
45
|
|
|
44
46
|
type Props = HTMLAttributes<HTMLDivElement> & {
|
|
45
47
|
accept?: string;
|
|
@@ -49,79 +51,74 @@
|
|
|
49
51
|
clearable?: boolean;
|
|
50
52
|
placeholder?: string;
|
|
51
53
|
value?: FileList | null;
|
|
54
|
+
maxBytes?: number;
|
|
52
55
|
onFilesSelected?: (files: FileList | null) => void;
|
|
53
56
|
onError?: (error: string) => void;
|
|
54
57
|
class?: string;
|
|
55
58
|
};
|
|
56
59
|
|
|
57
|
-
let {
|
|
58
|
-
accept = "*/*",
|
|
59
|
-
multiple = false,
|
|
60
|
-
label,
|
|
61
|
-
disabled = false,
|
|
62
|
-
clearable = true,
|
|
63
|
-
placeholder,
|
|
64
|
-
value = $bindable<FileList | null>(null),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
60
|
+
let {
|
|
61
|
+
accept = "*/*",
|
|
62
|
+
multiple = false,
|
|
63
|
+
label,
|
|
64
|
+
disabled = false,
|
|
65
|
+
clearable = true,
|
|
66
|
+
placeholder,
|
|
67
|
+
value = $bindable<FileList | null>(null),
|
|
68
|
+
maxBytes = Number.POSITIVE_INFINITY,
|
|
69
|
+
onFilesSelected,
|
|
70
|
+
onError,
|
|
71
|
+
class: externalClass = "",
|
|
72
|
+
...rest
|
|
73
|
+
}: Props = $props();
|
|
74
|
+
|
|
75
|
+
const langCtx = getLangContext();
|
|
76
|
+
const langKey = $derived(getLangKey(langCtx));
|
|
77
|
+
const L = $derived(getComponentText("filePicker", langKey));
|
|
73
78
|
|
|
74
79
|
const labelFinal = $derived(label ?? L.text);
|
|
75
80
|
const placeholderFinal = $derived(placeholder ?? L.placeholder);
|
|
76
81
|
|
|
77
|
-
let inputEl: HTMLInputElement;
|
|
78
|
-
let isDragOver = $state(false);
|
|
82
|
+
let inputEl: HTMLInputElement;
|
|
83
|
+
let isDragOver = $state(false);
|
|
79
84
|
|
|
80
85
|
const base = "inline-block w-full";
|
|
81
86
|
const pickerClass = $derived(cx(base, externalClass));
|
|
82
87
|
|
|
83
|
-
const hasValue = $derived(Boolean(value && value.length > 0));
|
|
84
|
-
const fileNames = $derived(
|
|
85
|
-
value
|
|
86
|
-
? Array.from(value)
|
|
87
|
-
.map((file) => file.name)
|
|
88
|
-
.join(", ")
|
|
89
|
-
: ""
|
|
90
|
-
);
|
|
91
|
-
const totalBytes = $derived(
|
|
92
|
-
value ? Array.from(value).reduce((acc, file) => acc + file.size, 0) : 0
|
|
93
|
-
);
|
|
88
|
+
const hasValue = $derived(Boolean(value && value.length > 0));
|
|
89
|
+
const fileNames = $derived(
|
|
90
|
+
value
|
|
91
|
+
? Array.from(value)
|
|
92
|
+
.map((file) => file.name)
|
|
93
|
+
.join(", ")
|
|
94
|
+
: ""
|
|
95
|
+
);
|
|
96
|
+
const totalBytes = $derived(
|
|
97
|
+
value ? Array.from(value).reduce((acc, file) => acc + file.size, 0) : 0
|
|
98
|
+
);
|
|
94
99
|
|
|
95
100
|
function handleButtonClick() {
|
|
96
101
|
if (disabled) return;
|
|
97
102
|
inputEl?.click();
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
function handleFileChange(event: Event) {
|
|
101
|
-
const target = event.target as HTMLInputElement;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (inputEl) {
|
|
108
|
-
inputEl.value = "";
|
|
109
|
-
}
|
|
110
|
-
}
|
|
105
|
+
function handleFileChange(event: Event) {
|
|
106
|
+
const target = event.target as HTMLInputElement;
|
|
107
|
+
selectFiles(target.files);
|
|
108
|
+
if (inputEl) {
|
|
109
|
+
inputEl.value = "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
111
112
|
|
|
112
113
|
function handleDrop(event: DragEvent) {
|
|
113
114
|
event.preventDefault();
|
|
114
115
|
isDragOver = false;
|
|
115
|
-
if (disabled) return;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (inputEl) {
|
|
122
|
-
inputEl.value = "";
|
|
123
|
-
}
|
|
124
|
-
}
|
|
116
|
+
if (disabled) return;
|
|
117
|
+
selectFiles(event.dataTransfer?.files ?? null);
|
|
118
|
+
if (inputEl) {
|
|
119
|
+
inputEl.value = "";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
125
122
|
|
|
126
123
|
function handleDragOver(event: DragEvent) {
|
|
127
124
|
event.preventDefault();
|
|
@@ -147,14 +144,70 @@
|
|
|
147
144
|
}
|
|
148
145
|
}
|
|
149
146
|
|
|
150
|
-
function clearSelection() {
|
|
151
|
-
if (!clearable) return;
|
|
152
|
-
value = null;
|
|
153
|
-
if (inputEl) {
|
|
154
|
-
inputEl.value = "";
|
|
155
|
-
}
|
|
156
|
-
onFilesSelected?.(null);
|
|
157
|
-
}
|
|
147
|
+
function clearSelection() {
|
|
148
|
+
if (!clearable) return;
|
|
149
|
+
value = null;
|
|
150
|
+
if (inputEl) {
|
|
151
|
+
inputEl.value = "";
|
|
152
|
+
}
|
|
153
|
+
onFilesSelected?.(null);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function selectFiles(files: FileList | null) {
|
|
157
|
+
const acceptedFiles = filterFiles(files);
|
|
158
|
+
value = acceptedFiles;
|
|
159
|
+
if (acceptedFiles && acceptedFiles.length > 0) {
|
|
160
|
+
onFilesSelected?.(acceptedFiles);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function filterFiles(files: FileList | null) {
|
|
165
|
+
if (!files || files.length === 0) return null;
|
|
166
|
+
|
|
167
|
+
const selected = Array.from(files);
|
|
168
|
+
const accepted = selected.filter(isAllowedFile);
|
|
169
|
+
|
|
170
|
+
if (accepted.length !== selected.length) {
|
|
171
|
+
onError?.("Some files were rejected by type or size constraints.");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (accepted.length === 0) return null;
|
|
175
|
+
if (accepted.length === selected.length) return files;
|
|
176
|
+
|
|
177
|
+
return toFileList(accepted);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isAllowedFile(file: File) {
|
|
181
|
+
if (Number.isFinite(maxBytes) && file.size > maxBytes) return false;
|
|
182
|
+
return matchesAccept(file, accept);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function matchesAccept(file: File, acceptValue: string) {
|
|
186
|
+
const rules = acceptValue
|
|
187
|
+
.split(",")
|
|
188
|
+
.map((rule) => rule.trim().toLowerCase())
|
|
189
|
+
.filter(Boolean);
|
|
190
|
+
|
|
191
|
+
if (rules.length === 0 || rules.includes("*/*")) return true;
|
|
192
|
+
|
|
193
|
+
const fileName = file.name.toLowerCase();
|
|
194
|
+
const fileType = file.type.toLowerCase();
|
|
195
|
+
|
|
196
|
+
return rules.some((rule) => {
|
|
197
|
+
if (rule.startsWith(".")) return fileName.endsWith(rule);
|
|
198
|
+
if (rule.endsWith("/*")) return fileType.startsWith(rule.slice(0, -1));
|
|
199
|
+
return fileType === rule;
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function toFileList(files: File[]) {
|
|
204
|
+
if (typeof DataTransfer === "undefined") return null;
|
|
205
|
+
const transfer = new DataTransfer();
|
|
206
|
+
for (const file of files) {
|
|
207
|
+
transfer.items.add(file);
|
|
208
|
+
}
|
|
209
|
+
return transfer.files;
|
|
210
|
+
}
|
|
158
211
|
</script>
|
|
159
212
|
|
|
160
213
|
<div class={pickerClass} {...rest}>
|
|
@@ -188,7 +241,7 @@
|
|
|
188
241
|
class="mt-2 p-4 border-2 border-dashed rounded-[var(--radius-md)] text-center transition-colors duration-200"
|
|
189
242
|
class:border-[var(--color-primary)]={isDragOver && !disabled}
|
|
190
243
|
class:border-[var(--border-color-default)]={!isDragOver || disabled}
|
|
191
|
-
class:bg-[var(--color-bg-hover)]={isDragOver && !disabled}
|
|
244
|
+
class:bg-[var(--color-bg-hover)]={isDragOver && !disabled}
|
|
192
245
|
class:cursor-pointer={!disabled}
|
|
193
246
|
class:opacity-[var(--opacity-disabled)]={disabled}
|
|
194
247
|
class:cursor-not-allowed={disabled}
|
|
@@ -229,14 +282,14 @@
|
|
|
229
282
|
{placeholderFinal}
|
|
230
283
|
{/if}
|
|
231
284
|
</p>
|
|
232
|
-
{#if hasValue && value}
|
|
233
|
-
<p class="text-sm mt-1 [color:var(--color-text-muted)]">
|
|
234
|
-
{L.fileCount.replace("{n}", String(value.length))}
|
|
235
|
-
|
|
236
|
-
{#if multiple && value.length > 1}
|
|
237
|
-
- {L.totalSize}: {formatFileSize(totalBytes)}
|
|
238
|
-
{/if}
|
|
239
|
-
</p>
|
|
240
|
-
{/if}
|
|
285
|
+
{#if hasValue && value}
|
|
286
|
+
<p class="text-sm mt-1 [color:var(--color-text-muted)]">
|
|
287
|
+
{L.fileCount.replace("{n}", String(value.length))}
|
|
288
|
+
|
|
289
|
+
{#if multiple && value.length > 1}
|
|
290
|
+
- {L.totalSize}: {formatFileSize(totalBytes)}
|
|
291
|
+
{/if}
|
|
292
|
+
</p>
|
|
293
|
+
{/if}
|
|
241
294
|
</div>
|
|
242
295
|
</div>
|