svelte-comp 1.2.2 → 1.2.6
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.
Potentially problematic release.
This version of svelte-comp might be problematic. Click here for more details.
- package/README.md +1 -1
- package/package.json +1 -1
- package/dist/App.svelte +0 -551
- package/dist/App.svelte.d.ts +0 -3
- package/dist/Container.svelte +0 -60
- package/dist/Container.svelte.d.ts +0 -12
- package/dist/app.css +0 -235
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -6
- package/dist/lang.d.ts +0 -1081
- package/dist/lang.js +0 -1096
- package/dist/lib/Accordion.svelte +0 -155
- package/dist/lib/Accordion.svelte.d.ts +0 -40
- package/dist/lib/Button.svelte +0 -170
- package/dist/lib/Button.svelte.d.ts +0 -53
- package/dist/lib/Card.svelte +0 -103
- package/dist/lib/Card.svelte.d.ts +0 -42
- package/dist/lib/Carousel.svelte +0 -293
- package/dist/lib/Carousel.svelte.d.ts +0 -13
- package/dist/lib/CheckBox.svelte +0 -210
- package/dist/lib/CheckBox.svelte.d.ts +0 -53
- package/dist/lib/CodeView.svelte +0 -307
- package/dist/lib/CodeView.svelte.d.ts +0 -64
- package/dist/lib/ColorPicker.svelte +0 -161
- package/dist/lib/ColorPicker.svelte.d.ts +0 -40
- package/dist/lib/DatePicker.svelte +0 -170
- package/dist/lib/DatePicker.svelte.d.ts +0 -53
- package/dist/lib/Dialog.svelte +0 -235
- package/dist/lib/Dialog.svelte.d.ts +0 -58
- package/dist/lib/Field.svelte +0 -299
- package/dist/lib/Field.svelte.d.ts +0 -8
- package/dist/lib/FilePicker.svelte +0 -241
- package/dist/lib/FilePicker.svelte.d.ts +0 -52
- package/dist/lib/Form.svelte +0 -438
- package/dist/lib/Form.svelte.d.ts +0 -20
- package/dist/lib/Hamburger.svelte +0 -211
- package/dist/lib/Hamburger.svelte.d.ts +0 -52
- package/dist/lib/Menu.svelte +0 -623
- package/dist/lib/Menu.svelte.d.ts +0 -33
- package/dist/lib/PaginatedCard.svelte +0 -73
- package/dist/lib/PaginatedCard.svelte.d.ts +0 -11
- package/dist/lib/Pagination.svelte +0 -98
- package/dist/lib/Pagination.svelte.d.ts +0 -9
- package/dist/lib/PrimaryColorSelect.svelte +0 -113
- package/dist/lib/PrimaryColorSelect.svelte.d.ts +0 -9
- package/dist/lib/ProgressBar.svelte +0 -141
- package/dist/lib/ProgressBar.svelte.d.ts +0 -48
- package/dist/lib/ProgressCircle.svelte +0 -192
- package/dist/lib/ProgressCircle.svelte.d.ts +0 -39
- package/dist/lib/Radio.svelte +0 -189
- package/dist/lib/Radio.svelte.d.ts +0 -55
- package/dist/lib/SearchInput.svelte +0 -97
- package/dist/lib/SearchInput.svelte.d.ts +0 -12
- package/dist/lib/Select.svelte +0 -524
- package/dist/lib/Select.svelte.d.ts +0 -21
- package/dist/lib/Slider.svelte +0 -253
- package/dist/lib/Slider.svelte.d.ts +0 -56
- package/dist/lib/Splitter.svelte +0 -150
- package/dist/lib/Splitter.svelte.d.ts +0 -43
- package/dist/lib/Switch.svelte +0 -167
- package/dist/lib/Switch.svelte.d.ts +0 -42
- package/dist/lib/Table.svelte +0 -299
- package/dist/lib/Table.svelte.d.ts +0 -17
- package/dist/lib/Tabs.svelte +0 -213
- package/dist/lib/Tabs.svelte.d.ts +0 -48
- package/dist/lib/ThemeToggle.svelte +0 -127
- package/dist/lib/ThemeToggle.svelte.d.ts +0 -32
- package/dist/lib/TimePicker.svelte +0 -269
- package/dist/lib/TimePicker.svelte.d.ts +0 -48
- package/dist/lib/Toast.svelte +0 -226
- package/dist/lib/Toast.svelte.d.ts +0 -14
- package/dist/lib/Tooltip.svelte +0 -110
- package/dist/lib/Tooltip.svelte.d.ts +0 -40
- package/dist/lib/index.d.ts +0 -32
- package/dist/lib/index.js +0 -33
- package/dist/lib/lang.d.ts +0 -149
- package/dist/lib/lang.js +0 -141
- package/dist/lib/types/index.d.ts +0 -111
- package/dist/lib/types/index.js +0 -26
- package/dist/main.d.ts +0 -3
- package/dist/main.js +0 -7
- package/dist/styles.css +0 -232
- package/dist/utils/index.d.ts +0 -34
- package/dist/utils/index.js +0 -268
package/dist/lib/Form.svelte
DELETED
|
@@ -1,438 +0,0 @@
|
|
|
1
|
-
<!-- src/lib/Form.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* @component Form
|
|
5
|
-
* @description Declarative, schema-driven form generator. Renders `Field`, `Select`, and `CheckBox` based on `FieldSchema`. Supports validation, controlled state, and an external API via `expose`.
|
|
6
|
-
*
|
|
7
|
-
* @prop schema {FieldSchema[]} - Field configuration for the generated form
|
|
8
|
-
*
|
|
9
|
-
* @prop value {FormValues} - Initial form data
|
|
10
|
-
* @default {}
|
|
11
|
-
*
|
|
12
|
-
* @prop rowGap {number | SizeKey} - Vertical spacing between fields
|
|
13
|
-
* @default "md"
|
|
14
|
-
*
|
|
15
|
-
* @prop validateOn {"input" | "blur" | "submit"} - When validation should run
|
|
16
|
-
* @default "blur"
|
|
17
|
-
*
|
|
18
|
-
* @prop onChange {(form: FormValues) => void} - Fired when form values change
|
|
19
|
-
*
|
|
20
|
-
* @prop onSubmit {(form: FormValues, ctx: { reset: () => void }) => void | Promise<void>} - Submission handler
|
|
21
|
-
*
|
|
22
|
-
* @prop formId {string} - Stable identifier for form elements
|
|
23
|
-
*
|
|
24
|
-
* @prop expose {(api: FormApi) => void} - Exposes imperative Form API
|
|
25
|
-
*
|
|
26
|
-
* @prop labelAlign {AlignText} - Alignment for labels
|
|
27
|
-
* @options left|center|right
|
|
28
|
-
* @default "left"
|
|
29
|
-
*
|
|
30
|
-
* @prop labelWeight {LabelWeight} - Font weight for labels
|
|
31
|
-
* @options normal|medium|semibold|bold
|
|
32
|
-
* @default "medium"
|
|
33
|
-
*
|
|
34
|
-
* @prop labelSize {SizeKey} - Size preset for labels
|
|
35
|
-
* @options xs|sm|md|lg|xl
|
|
36
|
-
* @default "md"
|
|
37
|
-
*
|
|
38
|
-
* @prop compact {boolean} - Enables denser sizing across controls
|
|
39
|
-
* @default false
|
|
40
|
-
*
|
|
41
|
-
* @note Initial value for each field: `value[name]` → `schema.default` → `''` (or `false` for checkboxes).
|
|
42
|
-
* @note `validateOn='input'|'blur'|'submit'` controls when validators run; built-in checks: `required`, `number`, and `email` regex.
|
|
43
|
-
* @note `when(form)` hides a field dynamically; hidden fields are skipped during validation.
|
|
44
|
-
* @note `Select` options are coerced to strings for the underlying control; provide string values if you rely on strict equality.
|
|
45
|
-
* @note Errors are rendered with stable `id`s and wired via `aria-describedby`; `invalid` flags are passed to inputs.
|
|
46
|
-
* @note `expose` provides `{ reset, submit, validate, getData }`; `validate` returns `Promise<boolean>`.
|
|
47
|
-
* @note `compact` reduces control sizes (`xs→xs`, `sm→xs`, `md→sm`, `lg→md`, `xl→lg`) and centers labels where applicable.
|
|
48
|
-
*/
|
|
49
|
-
import Field from "./Field.svelte";
|
|
50
|
-
import Select from "./Select.svelte";
|
|
51
|
-
import CheckBox from "./CheckBox.svelte";
|
|
52
|
-
import Toast from "./Toast.svelte";
|
|
53
|
-
import type {
|
|
54
|
-
AlignText,
|
|
55
|
-
LabelWeight,
|
|
56
|
-
SizeKey,
|
|
57
|
-
ToastVariant,
|
|
58
|
-
FieldSchema,
|
|
59
|
-
FormApi,
|
|
60
|
-
FormValues,
|
|
61
|
-
} from "./types";
|
|
62
|
-
import { TEXT } from "./types";
|
|
63
|
-
import { cx, debounce } from "../utils";
|
|
64
|
-
|
|
65
|
-
type Props = {
|
|
66
|
-
schema: FieldSchema[];
|
|
67
|
-
value?: FormValues;
|
|
68
|
-
rowGap?: number | SizeKey;
|
|
69
|
-
validateOn?: "input" | "blur" | "submit";
|
|
70
|
-
onChange?: (form: FormValues) => void;
|
|
71
|
-
onSubmit?: (
|
|
72
|
-
form: FormValues,
|
|
73
|
-
ctx: { reset: () => void }
|
|
74
|
-
) => void | Promise<void>;
|
|
75
|
-
formId?: string;
|
|
76
|
-
expose?: (api: FormApi) => void;
|
|
77
|
-
labelAlign?: AlignText;
|
|
78
|
-
labelWeight?: LabelWeight;
|
|
79
|
-
labelSize?: SizeKey;
|
|
80
|
-
compact?: boolean;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
let {
|
|
84
|
-
schema,
|
|
85
|
-
value = {},
|
|
86
|
-
rowGap = "md",
|
|
87
|
-
validateOn = "blur",
|
|
88
|
-
onChange,
|
|
89
|
-
onSubmit,
|
|
90
|
-
formId,
|
|
91
|
-
expose,
|
|
92
|
-
labelAlign = "left",
|
|
93
|
-
labelWeight = "medium",
|
|
94
|
-
labelSize = "md",
|
|
95
|
-
compact = false,
|
|
96
|
-
}: Props = $props();
|
|
97
|
-
|
|
98
|
-
const compactMap = {
|
|
99
|
-
xs: "xs",
|
|
100
|
-
sm: "xs",
|
|
101
|
-
md: "sm",
|
|
102
|
-
lg: "md",
|
|
103
|
-
xl: "lg",
|
|
104
|
-
} as const;
|
|
105
|
-
|
|
106
|
-
const weightClasses = {
|
|
107
|
-
normal: "font-normal",
|
|
108
|
-
medium: "font-medium",
|
|
109
|
-
semibold: "font-semibold",
|
|
110
|
-
bold: "font-bold",
|
|
111
|
-
} as const;
|
|
112
|
-
|
|
113
|
-
const gapClasses = {
|
|
114
|
-
xs: "gap-1",
|
|
115
|
-
sm: "gap-2",
|
|
116
|
-
md: "gap-4",
|
|
117
|
-
lg: "gap-6",
|
|
118
|
-
xl: "gap-8",
|
|
119
|
-
} as const;
|
|
120
|
-
|
|
121
|
-
const toStr = (v: unknown) => (v == null ? "" : String(v));
|
|
122
|
-
|
|
123
|
-
let form = $state<FormValues>({});
|
|
124
|
-
|
|
125
|
-
const touched = $state<Record<string, boolean>>({});
|
|
126
|
-
const errors = $state<Record<string, string[]>>({});
|
|
127
|
-
let toasts = $state<
|
|
128
|
-
Array<{
|
|
129
|
-
id: number;
|
|
130
|
-
message: string;
|
|
131
|
-
variant: ToastVariant;
|
|
132
|
-
title?: string;
|
|
133
|
-
}>
|
|
134
|
-
>([]);
|
|
135
|
-
let toastId = 0;
|
|
136
|
-
|
|
137
|
-
const inputWidthClass = $derived(compact ? "" : "w-full");
|
|
138
|
-
|
|
139
|
-
const rowGapClass = $derived(
|
|
140
|
-
typeof rowGap === "number" ? "" : (gapClasses[rowGap] ?? gapClasses.md)
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
const rowGapStyle = $derived(typeof rowGap === "number" ? rowGap + "px" : "");
|
|
144
|
-
|
|
145
|
-
const getFieldValue = (name: string): string | number => {
|
|
146
|
-
const current = form[name];
|
|
147
|
-
return typeof current === "number" ? current : toStr(current);
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
function getCompactSize(size: SizeKey) {
|
|
151
|
-
return compact ? (compactMap[size] ?? "md") : size;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const globalFormInstanceCounter = globalThis as unknown as {
|
|
155
|
-
__svelteCompFormInstanceCounter?: number;
|
|
156
|
-
};
|
|
157
|
-
globalFormInstanceCounter.__svelteCompFormInstanceCounter ??= 0;
|
|
158
|
-
const instanceId = $state(globalFormInstanceCounter.__svelteCompFormInstanceCounter++);
|
|
159
|
-
|
|
160
|
-
const baseFormId = $derived.by(() => {
|
|
161
|
-
if (formId && formId.trim()) return formId.trim();
|
|
162
|
-
const key = schema.map((f) => f.name).join("|");
|
|
163
|
-
let hash = 0;
|
|
164
|
-
for (let i = 0; i < key.length; i += 1) {
|
|
165
|
-
hash = (hash << 5) - hash + key.charCodeAt(i);
|
|
166
|
-
hash |= 0;
|
|
167
|
-
}
|
|
168
|
-
const suffix = Math.abs(hash >>> 0).toString(36) || "form";
|
|
169
|
-
return `form-${suffix}`;
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
const stableFormId = $derived.by(() => {
|
|
173
|
-
if (formId && formId.trim()) return baseFormId;
|
|
174
|
-
return `${baseFormId}-${instanceId}`;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
$effect(() => {
|
|
178
|
-
const nextNames = new Set(schema.map((f) => f.name));
|
|
179
|
-
|
|
180
|
-
for (const key of Object.keys(form)) {
|
|
181
|
-
if (!nextNames.has(key)) {
|
|
182
|
-
delete form[key];
|
|
183
|
-
delete touched[key];
|
|
184
|
-
delete errors[key];
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
for (const f of schema) {
|
|
189
|
-
const hasIncoming = Object.prototype.hasOwnProperty.call(value, f.name);
|
|
190
|
-
if (hasIncoming) {
|
|
191
|
-
form[f.name] = value[f.name];
|
|
192
|
-
} else if (!(f.name in form)) {
|
|
193
|
-
form[f.name] = f.default ?? (f.type === "checkbox" ? false : "");
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!(f.name in touched)) touched[f.name] = false;
|
|
197
|
-
if (!errors[f.name]) errors[f.name] = [];
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const scheduleValidation = debounce((name: string) => {
|
|
202
|
-
void validateField(name);
|
|
203
|
-
}, 150);
|
|
204
|
-
|
|
205
|
-
function setValue(name: string, v: unknown) {
|
|
206
|
-
if (form[name] === v) return;
|
|
207
|
-
form[name] = v;
|
|
208
|
-
if (validateOn === "input") scheduleValidation(name);
|
|
209
|
-
onChange?.({ ...form });
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function blur(name: string) {
|
|
213
|
-
if (!touched[name]) touched[name] = true;
|
|
214
|
-
if (validateOn !== "submit") validateField(name);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function removeToast(id: number) {
|
|
218
|
-
toasts = toasts.filter((t) => t.id !== id);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function addToast(variant: ToastVariant, message: string, title?: string) {
|
|
222
|
-
const id = toastId++;
|
|
223
|
-
toasts = [...toasts, { id, message, variant, title }];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
async function validateField(name: string) {
|
|
227
|
-
const f = schema.find((s) => s.name === name);
|
|
228
|
-
if (!f) return;
|
|
229
|
-
const val = form[name];
|
|
230
|
-
const out: string[] = [];
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
f.required &&
|
|
234
|
-
(val === "" || val == null || (f.type === "checkbox" && !val))
|
|
235
|
-
) {
|
|
236
|
-
out.push("Required");
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
if (f.type === "number" && val !== "" && Number.isNaN(Number(val))) {
|
|
240
|
-
out.push("Must be a number");
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (f.type === "email" && val !== "") {
|
|
244
|
-
const ok = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(val));
|
|
245
|
-
if (!ok) out.push("Invalid email");
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
if (f.validators) {
|
|
249
|
-
for (const v of f.validators) {
|
|
250
|
-
const r = await v(val, form);
|
|
251
|
-
if (r) out.push(r);
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
errors[name] = out;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function showField(f: FieldSchema) {
|
|
259
|
-
return f.when ? !!f.when(form) : true;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const visible = $derived(schema.filter(showField));
|
|
263
|
-
const visibleNames = $derived(visible.map((f) => f.name));
|
|
264
|
-
|
|
265
|
-
async function validateForm() {
|
|
266
|
-
await Promise.all(visibleNames.map(validateField));
|
|
267
|
-
return visibleNames.every((n) => (errors[n]?.length ?? 0) === 0);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function reset() {
|
|
271
|
-
for (const f of schema) {
|
|
272
|
-
form[f.name] = f.default ?? (f.type === "checkbox" ? false : "");
|
|
273
|
-
touched[f.name] = false;
|
|
274
|
-
errors[f.name] = [];
|
|
275
|
-
}
|
|
276
|
-
onChange?.({ ...form });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
async function submit(e?: Event) {
|
|
280
|
-
e?.preventDefault?.();
|
|
281
|
-
const ok = await validateForm();
|
|
282
|
-
if (!ok) return;
|
|
283
|
-
try {
|
|
284
|
-
await onSubmit?.({ ...form }, { reset });
|
|
285
|
-
addToast("success", "Form submitted successfully");
|
|
286
|
-
} catch (err) {
|
|
287
|
-
addToast("danger", "Form submission failed", "Error");
|
|
288
|
-
throw err;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
$effect(() => {
|
|
293
|
-
const api: FormApi = {
|
|
294
|
-
reset,
|
|
295
|
-
submit: () => submit(),
|
|
296
|
-
validate: () => validateForm(),
|
|
297
|
-
getData: () => ({ ...form }),
|
|
298
|
-
setValue: (name, value) => setValue(name, value),
|
|
299
|
-
};
|
|
300
|
-
expose?.(api);
|
|
301
|
-
});
|
|
302
|
-
</script>
|
|
303
|
-
|
|
304
|
-
<form
|
|
305
|
-
id={stableFormId}
|
|
306
|
-
onsubmit={submit}
|
|
307
|
-
novalidate
|
|
308
|
-
class={cx(
|
|
309
|
-
"grid font-sans w-full",
|
|
310
|
-
rowGapClass,
|
|
311
|
-
compact && "gap-2 justify-items-center"
|
|
312
|
-
)}
|
|
313
|
-
style:gap={rowGapStyle}
|
|
314
|
-
>
|
|
315
|
-
{#each visible as f (f.name)}
|
|
316
|
-
{#key f.name}
|
|
317
|
-
<div
|
|
318
|
-
class={cx(
|
|
319
|
-
"grid gap-1 w-full min-w-0",
|
|
320
|
-
compact ? "justify-items-center" : "justify-items-stretch"
|
|
321
|
-
)}
|
|
322
|
-
>
|
|
323
|
-
{#if f.label}
|
|
324
|
-
<div
|
|
325
|
-
class={cx(
|
|
326
|
-
weightClasses[labelWeight],
|
|
327
|
-
compact
|
|
328
|
-
? "text-xs -mb-1 text-center"
|
|
329
|
-
: [
|
|
330
|
-
TEXT[labelSize],
|
|
331
|
-
labelAlign === "center"
|
|
332
|
-
? "text-center"
|
|
333
|
-
: labelAlign === "right"
|
|
334
|
-
? "text-right"
|
|
335
|
-
: "text-left",
|
|
336
|
-
],
|
|
337
|
-
"text-[var(--color-text-default)] leading-normal"
|
|
338
|
-
)}
|
|
339
|
-
>
|
|
340
|
-
{f.label}
|
|
341
|
-
{#if f.required}
|
|
342
|
-
<span class="text-[var(--color-text-default)]">*</span>
|
|
343
|
-
{/if}
|
|
344
|
-
</div>
|
|
345
|
-
{/if}
|
|
346
|
-
|
|
347
|
-
{#if f.type === "select"}
|
|
348
|
-
{@const hasErr =
|
|
349
|
-
(touched[f.name] || validateOn !== "submit") &&
|
|
350
|
-
errors[f.name]?.length > 0}
|
|
351
|
-
{@const errId = hasErr ? `${stableFormId}-err-${f.name}` : undefined}
|
|
352
|
-
<Select
|
|
353
|
-
class={cx(f.class, inputWidthClass)}
|
|
354
|
-
options={(f.options ?? []).map((o) => ({
|
|
355
|
-
label: toStr(o.label),
|
|
356
|
-
value: toStr(o.value),
|
|
357
|
-
disabled: o.disabled,
|
|
358
|
-
}))}
|
|
359
|
-
sz={getCompactSize(f.sz ?? "md")}
|
|
360
|
-
variant={f.variant ?? "default"}
|
|
361
|
-
value={toStr(form[f.name])}
|
|
362
|
-
onChange={(v: string) => setValue(f.name, v)}
|
|
363
|
-
onblur={() => blur(f.name)}
|
|
364
|
-
invalid={hasErr}
|
|
365
|
-
describedBy={errId}
|
|
366
|
-
/>
|
|
367
|
-
{:else if f.type === "checkbox"}
|
|
368
|
-
<div class={cx(f.class, compact && "mx-auto justify-self-center")}>
|
|
369
|
-
<CheckBox
|
|
370
|
-
class={f.class ?? ""}
|
|
371
|
-
sz={getCompactSize(f.sz ?? "md")}
|
|
372
|
-
variant="default"
|
|
373
|
-
checked={!!form[f.name]}
|
|
374
|
-
onChange={(c: boolean) => setValue(f.name, c)}
|
|
375
|
-
onblur={() => blur(f.name)}
|
|
376
|
-
/>
|
|
377
|
-
</div>
|
|
378
|
-
{:else}
|
|
379
|
-
{@const hasErr =
|
|
380
|
-
(touched[f.name] || validateOn !== "submit") &&
|
|
381
|
-
errors[f.name]?.length > 0}
|
|
382
|
-
|
|
383
|
-
{@const errId = hasErr ? `${stableFormId}-err-${f.name}` : undefined}
|
|
384
|
-
|
|
385
|
-
<Field
|
|
386
|
-
class={cx(f.class, inputWidthClass)}
|
|
387
|
-
as={f.type === "textarea" ? "textarea" : "input"}
|
|
388
|
-
type={f.type === "number"
|
|
389
|
-
? "number"
|
|
390
|
-
: f.type === "password"
|
|
391
|
-
? "password"
|
|
392
|
-
: f.type === "email"
|
|
393
|
-
? "email"
|
|
394
|
-
: "text"}
|
|
395
|
-
rows={f.rows ?? 3}
|
|
396
|
-
parseNumber={f.type === "number"}
|
|
397
|
-
sz={getCompactSize(f.sz ?? "md")}
|
|
398
|
-
variant={f.variant ?? "default"}
|
|
399
|
-
value={getFieldValue(f.name)}
|
|
400
|
-
onChange={(v: string | number) => setValue(f.name, v)}
|
|
401
|
-
onblur={() => blur(f.name)}
|
|
402
|
-
invalid={hasErr}
|
|
403
|
-
describedBy={errId}
|
|
404
|
-
autocomplete={f.type === "email" ? "email" : undefined}
|
|
405
|
-
inputmode={f.type === "email" ? "email" : undefined}
|
|
406
|
-
/>
|
|
407
|
-
{/if}
|
|
408
|
-
|
|
409
|
-
{#if (touched[f.name] || validateOn !== "submit") && errors[f.name]?.length}
|
|
410
|
-
<ul
|
|
411
|
-
id={`${stableFormId}-err-${f.name}`}
|
|
412
|
-
class={cx("m-0 pl-4 list-none", compact && "text-center")}
|
|
413
|
-
>
|
|
414
|
-
{#each errors[f.name] ?? [] as m, idx (`${f.name}-${idx}`)}
|
|
415
|
-
<li
|
|
416
|
-
class={cx(
|
|
417
|
-
"text-xs leading-tight mt-1 text-[var(--color-text-red)]",
|
|
418
|
-
compact && "text-[10px]"
|
|
419
|
-
)}
|
|
420
|
-
>
|
|
421
|
-
{m}
|
|
422
|
-
</li>
|
|
423
|
-
{/each}
|
|
424
|
-
</ul>
|
|
425
|
-
{/if}
|
|
426
|
-
</div>
|
|
427
|
-
{/key}
|
|
428
|
-
{/each}
|
|
429
|
-
</form>
|
|
430
|
-
|
|
431
|
-
{#each toasts as t (t.id)}
|
|
432
|
-
<Toast
|
|
433
|
-
title={t.title}
|
|
434
|
-
message={t.message}
|
|
435
|
-
variant={t.variant}
|
|
436
|
-
onClose={() => removeToast(t.id)}
|
|
437
|
-
/>
|
|
438
|
-
{/each}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import type { AlignText, LabelWeight, SizeKey, FieldSchema, FormApi, FormValues } from "./types";
|
|
2
|
-
type Props = {
|
|
3
|
-
schema: FieldSchema[];
|
|
4
|
-
value?: FormValues;
|
|
5
|
-
rowGap?: number | SizeKey;
|
|
6
|
-
validateOn?: "input" | "blur" | "submit";
|
|
7
|
-
onChange?: (form: FormValues) => void;
|
|
8
|
-
onSubmit?: (form: FormValues, ctx: {
|
|
9
|
-
reset: () => void;
|
|
10
|
-
}) => void | Promise<void>;
|
|
11
|
-
formId?: string;
|
|
12
|
-
expose?: (api: FormApi) => void;
|
|
13
|
-
labelAlign?: AlignText;
|
|
14
|
-
labelWeight?: LabelWeight;
|
|
15
|
-
labelSize?: SizeKey;
|
|
16
|
-
compact?: boolean;
|
|
17
|
-
};
|
|
18
|
-
declare const Form: import("svelte").Component<Props, {}, "">;
|
|
19
|
-
type Form = ReturnType<typeof Form>;
|
|
20
|
-
export default Form;
|
|
@@ -1,211 +0,0 @@
|
|
|
1
|
-
<!-- src/lib/Hamburger.svelte -->
|
|
2
|
-
<script lang="ts">
|
|
3
|
-
/**
|
|
4
|
-
* @component Hamburger
|
|
5
|
-
* @description Off-canvas navigation drawer controlled by a floating hamburger button.
|
|
6
|
-
*
|
|
7
|
-
* @prop menuItems {Item[]} - Menu entries rendered in the drawer
|
|
8
|
-
* @default []
|
|
9
|
-
*
|
|
10
|
-
* @prop activeItem {string} - ID of the currently active item
|
|
11
|
-
* @default ""
|
|
12
|
-
*
|
|
13
|
-
* @prop header {Snippet} - Custom content rendered above the menu
|
|
14
|
-
*
|
|
15
|
-
* @prop footer {Snippet} - Custom content rendered below the menu
|
|
16
|
-
*
|
|
17
|
-
* @prop closeOnSelect {boolean} - Automatically closes after selecting an item
|
|
18
|
-
* @default true
|
|
19
|
-
*
|
|
20
|
-
* @prop onSelect {(id: string) => void} - Fired when a menu item is chosen
|
|
21
|
-
*
|
|
22
|
-
* @prop onOpenChange {(v: boolean) => void} - Fired when open state changes in controlled mode
|
|
23
|
-
*
|
|
24
|
-
* @prop pressed {boolean} - Controlled open state
|
|
25
|
-
*
|
|
26
|
-
* @prop class {string} - Extra classes applied to the trigger button
|
|
27
|
-
* @default ""
|
|
28
|
-
*
|
|
29
|
-
* @prop width {number | string} - Drawer width (px or CSS value)
|
|
30
|
-
* @default 300
|
|
31
|
-
*
|
|
32
|
-
* @note Clicking outside the panel or pressing `Escape` closes the drawer.
|
|
33
|
-
* @note Focus moves to the first interactive element inside the panel, is trapped while open, and returns to the trigger on close.
|
|
34
|
-
* @note In controlled mode (`pressed` is defined), state changes are requested via `onOpenChange(open)`.
|
|
35
|
-
* @note When `menuItems` is empty, a "No items" placeholder is shown.
|
|
36
|
-
* @note The drawer uses `role=\"dialog\"` and `aria-modal=\"true\"`; the trigger reflects state via `aria-expanded`.
|
|
37
|
-
*/
|
|
38
|
-
import type { Snippet } from "svelte";
|
|
39
|
-
import type { Item } from "./types";
|
|
40
|
-
import { cx, throttle, focusFirstInteractive, trapFocus } from "../utils";
|
|
41
|
-
|
|
42
|
-
type Props = {
|
|
43
|
-
menuItems?: Item[];
|
|
44
|
-
activeItem?: string;
|
|
45
|
-
header?: Snippet;
|
|
46
|
-
footer?: Snippet;
|
|
47
|
-
closeOnSelect?: boolean;
|
|
48
|
-
onSelect?: (id: string) => void;
|
|
49
|
-
onOpenChange?: (v: boolean) => void;
|
|
50
|
-
pressed?: boolean;
|
|
51
|
-
class?: string;
|
|
52
|
-
width?: number | string;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
let {
|
|
56
|
-
menuItems = [],
|
|
57
|
-
activeItem = "",
|
|
58
|
-
header,
|
|
59
|
-
footer,
|
|
60
|
-
closeOnSelect = true,
|
|
61
|
-
onSelect,
|
|
62
|
-
onOpenChange,
|
|
63
|
-
pressed,
|
|
64
|
-
class: externalClass = "",
|
|
65
|
-
width = 300,
|
|
66
|
-
}: Props = $props();
|
|
67
|
-
|
|
68
|
-
let triggerEl = $state<HTMLButtonElement | undefined>(undefined);
|
|
69
|
-
let panelEl = $state<HTMLDivElement | undefined>(undefined);
|
|
70
|
-
let releaseFocus: (() => void) | null = null;
|
|
71
|
-
|
|
72
|
-
let _open = $state(false);
|
|
73
|
-
const open = $derived(pressed ?? _open);
|
|
74
|
-
|
|
75
|
-
function setOpen(v: boolean) {
|
|
76
|
-
if (pressed === undefined) {
|
|
77
|
-
_open = v;
|
|
78
|
-
} else {
|
|
79
|
-
onOpenChange?.(v);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function toggle() {
|
|
84
|
-
setOpen(!open);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function closeMenu() {
|
|
88
|
-
setOpen(false);
|
|
89
|
-
queueMicrotask(() => triggerEl?.focus());
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const throttledClose = throttle(() => closeMenu(), 150);
|
|
93
|
-
|
|
94
|
-
function handleKeydown(e: KeyboardEvent) {
|
|
95
|
-
if (e.key === "Escape") throttledClose();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
$effect(() => {
|
|
99
|
-
if (open && panelEl) {
|
|
100
|
-
queueMicrotask(() => {
|
|
101
|
-
focusFirstInteractive(panelEl!);
|
|
102
|
-
});
|
|
103
|
-
releaseFocus?.();
|
|
104
|
-
releaseFocus = trapFocus(panelEl);
|
|
105
|
-
document.addEventListener("keydown", handleKeydown);
|
|
106
|
-
} else {
|
|
107
|
-
releaseFocus?.();
|
|
108
|
-
releaseFocus = null;
|
|
109
|
-
document.removeEventListener("keydown", handleKeydown);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return () => {
|
|
113
|
-
document.removeEventListener("keydown", handleKeydown);
|
|
114
|
-
releaseFocus?.();
|
|
115
|
-
releaseFocus = null;
|
|
116
|
-
};
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
const triggerBase =
|
|
120
|
-
"fixed top-4 left-4 inline-flex items-center justify-center h-8 w-8 rounded-md border border-[var(--border-color-default)] bg-[var(--color-bg-secondary)] hover:bg-[var(--color-bg-hover)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--border-color-focus)] transition-colors z-[var(--z-modal)]";
|
|
121
|
-
|
|
122
|
-
const triggerClass = $derived(cx(triggerBase, externalClass));
|
|
123
|
-
</script>
|
|
124
|
-
|
|
125
|
-
<button
|
|
126
|
-
type="button"
|
|
127
|
-
aria-label="Toggle navigation"
|
|
128
|
-
aria-expanded={open}
|
|
129
|
-
class={triggerClass}
|
|
130
|
-
onclick={toggle}
|
|
131
|
-
bind:this={triggerEl}
|
|
132
|
-
>
|
|
133
|
-
<span class="relative block w-5 h-3.5">
|
|
134
|
-
<span
|
|
135
|
-
class={cx(
|
|
136
|
-
"absolute left-0 top-1/2 h-[2px] w-full bg-current transition-transform duration-200",
|
|
137
|
-
open ? "translate-y-[-50%] rotate-45" : "translate-y-[calc(-50%_-_6px)]"
|
|
138
|
-
)}
|
|
139
|
-
></span>
|
|
140
|
-
<span
|
|
141
|
-
class={cx(
|
|
142
|
-
"absolute left-0 top-1/2 h-[2px] w-full bg-current transition-opacity duration-200 translate-y-[-50%]",
|
|
143
|
-
open ? "opacity-0" : "opacity-100"
|
|
144
|
-
)}
|
|
145
|
-
></span>
|
|
146
|
-
<span
|
|
147
|
-
class={cx(
|
|
148
|
-
"absolute left-0 top-1/2 h-[2px] w-full bg-current transition-transform duration-200",
|
|
149
|
-
open
|
|
150
|
-
? "translate-y-[-50%] -rotate-45"
|
|
151
|
-
: "translate-y-[calc(-50%_+_6px)]"
|
|
152
|
-
)}
|
|
153
|
-
></span>
|
|
154
|
-
</span>
|
|
155
|
-
</button>
|
|
156
|
-
|
|
157
|
-
{#if open}
|
|
158
|
-
<div class="fixed inset-0 z-[var(--z-overlay)] flex">
|
|
159
|
-
<div
|
|
160
|
-
role="dialog"
|
|
161
|
-
aria-modal="true"
|
|
162
|
-
tabindex="-1"
|
|
163
|
-
bind:this={panelEl}
|
|
164
|
-
class="flex flex-col h-full bg-[var(--color-bg-surface)] shadow-xl"
|
|
165
|
-
style={`width:${typeof width === "number" ? `${width}px` : width}`}
|
|
166
|
-
>
|
|
167
|
-
{#if header}
|
|
168
|
-
<div class="p-4 border-b border-[var(--border-color-default)]">
|
|
169
|
-
{@render header?.()}
|
|
170
|
-
</div>
|
|
171
|
-
{/if}
|
|
172
|
-
|
|
173
|
-
<div class="flex-1 overflow-y-auto" tabindex="-1">
|
|
174
|
-
{#if menuItems.length === 0}
|
|
175
|
-
<div class="text-xs opacity-70 px-3 py-2 text-center">No items</div>
|
|
176
|
-
{:else}
|
|
177
|
-
<ul class="grid gap-2 p-4">
|
|
178
|
-
{#each menuItems as it (it.id)}
|
|
179
|
-
<li>
|
|
180
|
-
<button
|
|
181
|
-
type="button"
|
|
182
|
-
class="w-full text-left px-3 py-2 rounded-md hover:bg-[var(--color-bg-hover)] focus:outline-[var(--border-color-focus)] focus:outline-2 transition-colors"
|
|
183
|
-
aria-current={activeItem === it.id ? "page" : undefined}
|
|
184
|
-
onclick={() => {
|
|
185
|
-
onSelect?.(it.id);
|
|
186
|
-
if (closeOnSelect) closeMenu();
|
|
187
|
-
}}
|
|
188
|
-
>
|
|
189
|
-
{it.label}
|
|
190
|
-
</button>
|
|
191
|
-
</li>
|
|
192
|
-
{/each}
|
|
193
|
-
</ul>
|
|
194
|
-
{/if}
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
{#if footer}
|
|
198
|
-
<div class="p-4 border-t border-[var(--border-color-default)]">
|
|
199
|
-
{@render footer?.()}
|
|
200
|
-
</div>
|
|
201
|
-
{/if}
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
<button
|
|
205
|
-
type="button"
|
|
206
|
-
class="flex-1 bg-black/40"
|
|
207
|
-
aria-hidden="true"
|
|
208
|
-
onclick={closeMenu}
|
|
209
|
-
></button>
|
|
210
|
-
</div>
|
|
211
|
-
{/if}
|