@delightstack/components 0.1.0

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,41 @@
1
+ import { type Snippet } from 'svelte';
2
+ declare const FileUpload: import("svelte").Component<{
3
+ files?: File[];
4
+ accept?: string | undefined;
5
+ multiple?: boolean;
6
+ max_size?: number | undefined;
7
+ max_files?: number | undefined;
8
+ disabled?: boolean;
9
+ preview?: boolean;
10
+ dropzone?: boolean;
11
+ compact?: boolean;
12
+ avatar?: boolean;
13
+ size?: "0" | "1" | "2" | "3";
14
+ skeleton?: boolean;
15
+ label?: string | undefined;
16
+ error?: string | undefined;
17
+ dense?: boolean;
18
+ comfortable?: boolean;
19
+ id?: string;
20
+ name?: string | undefined;
21
+ class?: string;
22
+ onselect?: ((detail: {
23
+ files: File[];
24
+ }) => void) | undefined;
25
+ onremove?: ((detail: {
26
+ file: File;
27
+ index: number;
28
+ }) => void) | undefined;
29
+ onerror?: ((detail: {
30
+ file: File;
31
+ error: string;
32
+ }) => void) | undefined;
33
+ file_item?: Snippet<[{
34
+ file: File;
35
+ index: number;
36
+ remove: () => void;
37
+ }]> | undefined;
38
+ }, {}, "files">;
39
+ type FileUpload = ReturnType<typeof FileUpload>;
40
+ export default FileUpload;
41
+ //# sourceMappingURL=FileUpload.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FileUpload.svelte.d.ts","sourceRoot":"","sources":["../../src/form/FileUpload.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAyXtC,QAAA,MAAM,UAAU;YA3ViE,IAAI,EAAE;aAAW,MAAM,GAAG,SAAS;eAAa,OAAO;eAAa,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;eAAa,OAAO;cAAY,OAAO;eAAa,OAAO;cAAY,OAAO;aAAW,OAAO;WAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;eAAa,OAAO;YAAU,MAAM,GAAG,SAAS;YAAU,MAAM,GAAG,SAAS;YAAU,OAAO;kBAAgB,OAAO;;WAA6B,MAAM,GAAG,SAAS;YAAU,MAAM;eAAa,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,IAAI,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;eAAa,CAAC,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;cAAY,CAAC,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;gBAAgB,OAAO,CAAC,CAAC;QAAE,IAAI,EAAE,IAAI,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC,CAAC,GAChxB,SAAS;eA0V0C,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,530 @@
1
+ <script lang="ts" module>
2
+ /**
3
+ * Standard Schema interface (v1) — inlined to avoid a hard dependency
4
+ * on `@standard-schema/spec`. Any Zod, Valibot, or ArkType schema
5
+ * satisfies this interface.
6
+ */
7
+ export interface StandardSchema<Input = unknown, Output = Input> {
8
+ readonly '~standard': {
9
+ readonly version: 1;
10
+ readonly vendor: string;
11
+ readonly validate: (
12
+ value: unknown,
13
+ ) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
14
+ readonly types?: { readonly input: Input; readonly output: Output } | undefined;
15
+ };
16
+ }
17
+
18
+ type StandardSchemaResult<Output> =
19
+ | { readonly value: Output; readonly issues?: undefined }
20
+ | { readonly issues: ReadonlyArray<StandardSchemaIssue> };
21
+
22
+ interface StandardSchemaIssue {
23
+ readonly message: string;
24
+ readonly path?:
25
+ | ReadonlyArray<PropertyKey | { readonly key: PropertyKey }>
26
+ | undefined;
27
+ }
28
+
29
+ /**
30
+ * The minimal entity shape the Form's `entity` prop accepts. `EntityState`
31
+ * from `@delightstack/database` satisfies it, but any object with an
32
+ * editable `value` and a `save()` works — the interface is structural, so
33
+ * the components package has no dependency on the database package.
34
+ */
35
+ export interface FormEntity {
36
+ /** The editable draft the form reads & writes (field names may be dot-notation paths) */
37
+ value: Record<string, unknown>;
38
+ /** Persists the draft; called on submit after validation passes */
39
+ save: () => Promise<unknown>;
40
+ /** Whether a save is in flight (drives the form's submitting state) */
41
+ readonly saving?: boolean;
42
+ /** Whether the draft differs from the persisted state (drives is_dirty) */
43
+ readonly has_changes?: boolean;
44
+ /** Restores the draft to the persisted state (used by form reset) */
45
+ reset?: () => void;
46
+ /** Last save/load error, if the entity tracks one */
47
+ readonly error?: unknown;
48
+ }
49
+
50
+ export interface FormContext {
51
+ /** The current form data keyed by field name */
52
+ data: Record<string, unknown>;
53
+ /** Current validation error messages keyed by field name */
54
+ errors: Record<string, string>;
55
+ /** Which fields have been touched (blurred/edited) keyed by field name */
56
+ touched: Record<string, boolean>;
57
+ /** Whether any field has been changed from its initial value */
58
+ is_dirty: boolean;
59
+ /** Whether the form is currently submitting */
60
+ is_submitting: boolean;
61
+ /** Whether the form currently has no validation errors */
62
+ is_valid: boolean;
63
+ /** Whether the whole form is disabled */
64
+ disabled: boolean;
65
+ /** When fields run validation */
66
+ validate_on: 'change' | 'blur' | 'submit';
67
+ /**
68
+ * Registers a field's element with the form (for focus-on-error).
69
+ * A field may also register a validator (like the `parse` function from a
70
+ * database table's form props) — the form runs it alongside the form-level
71
+ * schema. When both produce an error for the same field, the schema wins.
72
+ */
73
+ register: (
74
+ name: string,
75
+ element: HTMLElement,
76
+ validator?: (value: unknown) => unknown,
77
+ ) => void;
78
+ /** Removes a field from the form when it unmounts */
79
+ unregister: (name: string) => void;
80
+ /** Updates a field's value in the form data */
81
+ setValue: (name: string, value: unknown) => void;
82
+ /**
83
+ * Reads a field's value from the form data. Resolves dot-notation
84
+ * names against nested data (a literal flat key wins), so fields work
85
+ * over both flat data records and nested entity values.
86
+ */
87
+ getValue: (name: string) => unknown;
88
+ /** Marks a field as touched */
89
+ setTouched: (name: string) => void;
90
+ /** Runs validation for a single field */
91
+ validateField: (name: string) => void;
92
+ }
93
+ </script>
94
+
95
+ <script lang="ts">
96
+ import { setContext, type Snippet } from 'svelte';
97
+
98
+ const propId = $props.id();
99
+
100
+ let {
101
+ /** The form data object (bindable). Ignored when `entity` is set. */
102
+ data = $bindable({}) as Record<string, unknown>,
103
+
104
+ /**
105
+ * An entity to bind the form to (e.g. `db.entity('person', id)` from
106
+ * `@delightstack/database`). The form edits `entity.value` directly,
107
+ * derives dirty/submitting state from it, and calls `entity.save()` on
108
+ * submit once validation passes. Fields spread from `entity.form.field`
109
+ * need no `bind:value` — values flow through the form context.
110
+ */
111
+ entity = undefined as FormEntity | undefined,
112
+
113
+ /** Any Standard Schema compatible validator (Zod, Valibot, ArkType, etc.) */
114
+ schema = undefined as StandardSchema | undefined,
115
+
116
+ /** When to validate fields: 'change', 'blur', or 'submit' */
117
+ validate_on = 'blur' as 'change' | 'blur' | 'submit',
118
+
119
+ /** Whether the entire form is disabled */
120
+ disabled = false,
121
+
122
+ /** Reset form to initial values after successful submission */
123
+ reset_on_submit = false,
124
+
125
+ /** Use compact spacing between child fields */
126
+ dense = false,
127
+
128
+ /** Use relaxed spacing between child fields */
129
+ comfortable = false,
130
+
131
+ /** Element ID */
132
+ id = propId,
133
+
134
+ /** Additional CSS classes */
135
+ class: class_name = '',
136
+
137
+ /** Child content */
138
+ children = undefined as Snippet | undefined,
139
+
140
+ /** Called on form submission. May return a Promise for automatic loading state */
141
+ onsubmit = undefined as
142
+ | ((detail: {
143
+ data: Record<string, unknown>;
144
+ is_valid: boolean;
145
+ }) => void | Promise<void>)
146
+ | undefined,
147
+
148
+ /** Called when form data changes */
149
+ onchange = undefined as
150
+ | ((detail: {
151
+ data: Record<string, unknown>;
152
+ errors: Record<string, string>;
153
+ }) => void)
154
+ | undefined,
155
+
156
+ /** Called when validation fails on submit, or when an entity save rejects */
157
+ onerror = undefined as
158
+ | ((detail: { errors: Record<string, string>; error?: unknown }) => void)
159
+ | undefined,
160
+
161
+ /** Called after an entity-backed form saves successfully */
162
+ onsaved = undefined as ((detail: { entity: FormEntity }) => void) | undefined,
163
+
164
+ /** Called when the form is reset */
165
+ onreset = undefined as (() => void) | undefined,
166
+ } = $props();
167
+
168
+ /* ------------------------------------------------------------------ */
169
+ /* Data access */
170
+ /* ------------------------------------------------------------------ */
171
+
172
+ /** The record the form reads & writes: the entity's draft, or the data prop */
173
+ const form_data = $derived(entity ? entity.value : data);
174
+
175
+ /**
176
+ * Reads a (possibly dot-notation) field name from the form data. A literal
177
+ * flat key wins, then the nested path is walked — so the same field names
178
+ * work over a flat data record or a nested entity value.
179
+ */
180
+ function getValueAtPath(record: Record<string, unknown>, name: string): unknown {
181
+ if (name in record) return record[name];
182
+ if (!name.includes('.')) return undefined;
183
+ let current: unknown = record;
184
+ for (const part of name.split('.')) {
185
+ if (!current || typeof current !== 'object') return undefined;
186
+ current = (current as Record<string, unknown>)[part];
187
+ }
188
+ return current;
189
+ }
190
+
191
+ /** Writes a (possibly dot-notation) field name, creating nested objects as needed */
192
+ function setValueAtPath(
193
+ record: Record<string, unknown>,
194
+ name: string,
195
+ value: unknown,
196
+ ): void {
197
+ if (!name.includes('.') || name in record) {
198
+ record[name] = value;
199
+ return;
200
+ }
201
+ const parts = name.split('.');
202
+ let current = record;
203
+ for (let i = 0; i < parts.length - 1; i++) {
204
+ const part = parts[i];
205
+ if (!current[part] || typeof current[part] !== 'object') {
206
+ current[part] = {};
207
+ }
208
+ current = current[part] as Record<string, unknown>;
209
+ }
210
+ current[parts[parts.length - 1]] = value;
211
+ }
212
+
213
+ /* ------------------------------------------------------------------ */
214
+ /* Internal state */
215
+ /* ------------------------------------------------------------------ */
216
+
217
+ /** Snapshot of the initial data for dirty tracking and reset (data-prop mode only) */
218
+ let initial_snapshot = JSON.stringify(data);
219
+
220
+ /** Registry of field elements by name */
221
+ let field_elements = new Map<string, HTMLElement>();
222
+
223
+ /** Registry of field-level validators by name (e.g. table form props' parse) */
224
+ let field_validators = new Map<string, (value: unknown) => unknown>();
225
+
226
+ /** Validation errors keyed by field name */
227
+ let errors = $state<Record<string, string>>({});
228
+
229
+ /** Touched state keyed by field name */
230
+ let touched = $state<Record<string, boolean>>({});
231
+
232
+ /** Whether the form is currently submitting */
233
+ let is_submitting = $state(false);
234
+
235
+ /** Whether the form data has changed (entity dirty state, or snapshot diff) */
236
+ let is_dirty = $derived(
237
+ entity ? (entity.has_changes ?? false) : JSON.stringify(data) !== initial_snapshot,
238
+ );
239
+
240
+ /** Whether the form currently has no validation errors */
241
+ let is_valid = $derived(Object.keys(errors).length === 0);
242
+
243
+ /** Whether a submit is in flight (locally tracked, or the entity is saving) */
244
+ let effectively_submitting = $derived(is_submitting || (entity?.saving ?? false));
245
+
246
+ /** Whether the form should be effectively disabled (explicit or submitting) */
247
+ let effectively_disabled = $derived(disabled || effectively_submitting);
248
+
249
+ /* ------------------------------------------------------------------ */
250
+ /* Standard Schema validation */
251
+ /* ------------------------------------------------------------------ */
252
+
253
+ async function validate(
254
+ values: Record<string, unknown>,
255
+ ): Promise<Record<string, string>> {
256
+ const field_errors: Record<string, string> = {};
257
+
258
+ // Field-level validators run first; the form-level schema overwrites any
259
+ // error for the same field below, so the two never conflict — fields the
260
+ // schema doesn't cover keep their field-level error.
261
+ for (const [name, validator] of field_validators) {
262
+ try {
263
+ validator(getValueAtPath(values, name));
264
+ } catch (error) {
265
+ field_errors[name] = error instanceof Error ? error.message : 'Invalid value';
266
+ }
267
+ }
268
+
269
+ if (schema) {
270
+ const result = await schema['~standard'].validate(values);
271
+ if (result.issues) {
272
+ for (const issue of result.issues) {
273
+ const path = issue.path
274
+ ?.map((p) => (typeof p === 'object' && p !== null && 'key' in p ? p.key : p))
275
+ .join('.');
276
+ if (path) field_errors[path] = issue.message;
277
+ }
278
+ }
279
+ }
280
+ return field_errors;
281
+ }
282
+
283
+ async function validateSingleField(name: string): Promise<void> {
284
+ const all_errors = await validate(form_data);
285
+ if (all_errors[name]) {
286
+ errors[name] = all_errors[name];
287
+ } else {
288
+ delete errors[name];
289
+ // Force reactivity by re-assigning
290
+ errors = { ...errors };
291
+ }
292
+ }
293
+
294
+ /* ------------------------------------------------------------------ */
295
+ /* Context methods */
296
+ /* ------------------------------------------------------------------ */
297
+
298
+ function register(
299
+ name: string,
300
+ element: HTMLElement,
301
+ validator?: (value: unknown) => unknown,
302
+ ) {
303
+ field_elements.set(name, element);
304
+ if (validator) field_validators.set(name, validator);
305
+ }
306
+
307
+ function unregister(name: string) {
308
+ field_elements.delete(name);
309
+ field_validators.delete(name);
310
+ delete errors[name];
311
+ delete touched[name];
312
+ }
313
+
314
+ function setValue(name: string, value: unknown) {
315
+ setValueAtPath(form_data, name, value);
316
+
317
+ // In 'change' mode, validate on every edit — including the first
318
+ // keystroke, before the field has been blurred. Error display keys off
319
+ // errors[name] (not touched), so this surfaces feedback live as you type.
320
+ if (validate_on === 'change') {
321
+ validateSingleField(name);
322
+ }
323
+
324
+ onchange?.({ data: form_data, errors });
325
+ }
326
+
327
+ function getValue(name: string): unknown {
328
+ return getValueAtPath(form_data, name);
329
+ }
330
+
331
+ function setTouched(name: string) {
332
+ touched[name] = true;
333
+
334
+ if (validate_on === 'blur') {
335
+ validateSingleField(name);
336
+ }
337
+ }
338
+
339
+ function validateField(name: string) {
340
+ validateSingleField(name);
341
+ }
342
+
343
+ /* ------------------------------------------------------------------ */
344
+ /* Context */
345
+ /* ------------------------------------------------------------------ */
346
+
347
+ // svelte-ignore state_referenced_locally
348
+ const ctx = $state<FormContext>({
349
+ data,
350
+ errors,
351
+ touched,
352
+ is_dirty: false,
353
+ is_submitting: false,
354
+ is_valid: true,
355
+ disabled: false,
356
+ validate_on,
357
+ register,
358
+ unregister,
359
+ setValue,
360
+ getValue,
361
+ setTouched,
362
+ validateField,
363
+ });
364
+ setContext<FormContext>('form', ctx);
365
+
366
+ // Keep context in sync with reactive state
367
+ $effect(() => {
368
+ ctx.data = form_data;
369
+ ctx.errors = errors;
370
+ ctx.touched = touched;
371
+ ctx.is_dirty = is_dirty;
372
+ ctx.is_submitting = effectively_submitting;
373
+ ctx.is_valid = is_valid;
374
+ ctx.disabled = effectively_disabled;
375
+ ctx.validate_on = validate_on;
376
+ });
377
+
378
+ /* ------------------------------------------------------------------ */
379
+ /* Auto-focus first error field */
380
+ /* ------------------------------------------------------------------ */
381
+
382
+ function focusFirstError(field_errors: Record<string, string>) {
383
+ for (const [name] of field_elements) {
384
+ if (field_errors[name]) {
385
+ const el = field_elements.get(name);
386
+ if (el) {
387
+ el.focus();
388
+ el.scrollIntoView({ behavior: 'smooth', block: 'center' });
389
+ }
390
+ break;
391
+ }
392
+ }
393
+ }
394
+
395
+ /* ------------------------------------------------------------------ */
396
+ /* Form reset */
397
+ /* ------------------------------------------------------------------ */
398
+
399
+ function resetForm() {
400
+ if (entity) {
401
+ entity.reset?.();
402
+ } else {
403
+ data = JSON.parse(initial_snapshot);
404
+ }
405
+ errors = {};
406
+ touched = {};
407
+ onreset?.();
408
+ }
409
+
410
+ /* ------------------------------------------------------------------ */
411
+ /* Form submission */
412
+ /* ------------------------------------------------------------------ */
413
+
414
+ async function handleSubmit(event: SubmitEvent) {
415
+ event.preventDefault();
416
+
417
+ if (effectively_submitting) return;
418
+
419
+ // Validate all fields
420
+ const field_errors = await validate(form_data);
421
+ errors = field_errors;
422
+
423
+ // Mark all registered fields as touched
424
+ for (const [name] of field_elements) {
425
+ touched[name] = true;
426
+ }
427
+
428
+ const valid = Object.keys(field_errors).length === 0;
429
+
430
+ if (!valid) {
431
+ onerror?.({ errors: field_errors });
432
+ focusFirstError(field_errors);
433
+ return;
434
+ }
435
+
436
+ // Entity-backed submission: optional onsubmit hook (e.g. to massage the
437
+ // draft), then save. The saved entity carries its id (create or update).
438
+ if (entity) {
439
+ // Write each field's PARSED value back into the draft first — parse()
440
+ // normalizes ('' becomes undefined, schema transforms apply), so the
441
+ // entity saves clean data instead of raw input strings.
442
+ for (const [field_name, validator] of field_validators) {
443
+ try {
444
+ setValueAtPath(
445
+ form_data,
446
+ field_name,
447
+ validator(getValueAtPath(form_data, field_name)),
448
+ );
449
+ } catch {
450
+ // Validation above passed; leave the raw value if a validator
451
+ // is non-deterministic
452
+ }
453
+ }
454
+ is_submitting = true;
455
+ try {
456
+ await onsubmit?.({ data: form_data, is_valid: valid });
457
+ await entity.save();
458
+ if (reset_on_submit) resetForm();
459
+ onsaved?.({ entity });
460
+ } catch (error) {
461
+ // The entity tracks the failure on entity.error too (when supported)
462
+ onerror?.({ errors: {}, error });
463
+ } finally {
464
+ is_submitting = false;
465
+ }
466
+ return;
467
+ }
468
+
469
+ if (!onsubmit) return;
470
+
471
+ const result = onsubmit({ data, is_valid: valid });
472
+
473
+ // Promise-aware submission
474
+ if (result && typeof result === 'object' && 'then' in result) {
475
+ is_submitting = true;
476
+ try {
477
+ await result;
478
+ if (reset_on_submit) {
479
+ resetForm();
480
+ }
481
+ } finally {
482
+ is_submitting = false;
483
+ }
484
+ } else {
485
+ if (reset_on_submit) {
486
+ resetForm();
487
+ }
488
+ }
489
+ }
490
+
491
+ function handleReset(event: Event) {
492
+ event.preventDefault();
493
+ resetForm();
494
+ }
495
+ </script>
496
+
497
+ <form
498
+ {id}
499
+ class={['form', class_name].filter(Boolean).join(' ')}
500
+ class:dense
501
+ class:comfortable
502
+ class:disabled={effectively_disabled}
503
+ data-disabled={effectively_disabled || undefined}
504
+ onsubmit={handleSubmit}
505
+ onreset={handleReset}>
506
+ {#if children}
507
+ {@render children()}
508
+ {/if}
509
+ </form>
510
+
511
+ <style>
512
+ /* The `.form` class is part of the public surface (consumers target
513
+ `form.form` via :global) — keep it even though the element is unique. */
514
+ .form {
515
+ display: flex;
516
+ flex-direction: column;
517
+ gap: 1rem;
518
+
519
+ &.dense {
520
+ gap: 0.5rem;
521
+ }
522
+ &.comfortable {
523
+ gap: 1.5rem;
524
+ }
525
+ &.disabled {
526
+ opacity: 0.6;
527
+ pointer-events: none;
528
+ }
529
+ }
530
+ </style>
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Standard Schema interface (v1) — inlined to avoid a hard dependency
3
+ * on `@standard-schema/spec`. Any Zod, Valibot, or ArkType schema
4
+ * satisfies this interface.
5
+ */
6
+ export interface StandardSchema<Input = unknown, Output = Input> {
7
+ readonly '~standard': {
8
+ readonly version: 1;
9
+ readonly vendor: string;
10
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
11
+ readonly types?: {
12
+ readonly input: Input;
13
+ readonly output: Output;
14
+ } | undefined;
15
+ };
16
+ }
17
+ type StandardSchemaResult<Output> = {
18
+ readonly value: Output;
19
+ readonly issues?: undefined;
20
+ } | {
21
+ readonly issues: ReadonlyArray<StandardSchemaIssue>;
22
+ };
23
+ interface StandardSchemaIssue {
24
+ readonly message: string;
25
+ readonly path?: ReadonlyArray<PropertyKey | {
26
+ readonly key: PropertyKey;
27
+ }> | undefined;
28
+ }
29
+ /**
30
+ * The minimal entity shape the Form's `entity` prop accepts. `EntityState`
31
+ * from `@delightstack/database` satisfies it, but any object with an
32
+ * editable `value` and a `save()` works — the interface is structural, so
33
+ * the components package has no dependency on the database package.
34
+ */
35
+ export interface FormEntity {
36
+ /** The editable draft the form reads & writes (field names may be dot-notation paths) */
37
+ value: Record<string, unknown>;
38
+ /** Persists the draft; called on submit after validation passes */
39
+ save: () => Promise<unknown>;
40
+ /** Whether a save is in flight (drives the form's submitting state) */
41
+ readonly saving?: boolean;
42
+ /** Whether the draft differs from the persisted state (drives is_dirty) */
43
+ readonly has_changes?: boolean;
44
+ /** Restores the draft to the persisted state (used by form reset) */
45
+ reset?: () => void;
46
+ /** Last save/load error, if the entity tracks one */
47
+ readonly error?: unknown;
48
+ }
49
+ export interface FormContext {
50
+ /** The current form data keyed by field name */
51
+ data: Record<string, unknown>;
52
+ /** Current validation error messages keyed by field name */
53
+ errors: Record<string, string>;
54
+ /** Which fields have been touched (blurred/edited) keyed by field name */
55
+ touched: Record<string, boolean>;
56
+ /** Whether any field has been changed from its initial value */
57
+ is_dirty: boolean;
58
+ /** Whether the form is currently submitting */
59
+ is_submitting: boolean;
60
+ /** Whether the form currently has no validation errors */
61
+ is_valid: boolean;
62
+ /** Whether the whole form is disabled */
63
+ disabled: boolean;
64
+ /** When fields run validation */
65
+ validate_on: 'change' | 'blur' | 'submit';
66
+ /**
67
+ * Registers a field's element with the form (for focus-on-error).
68
+ * A field may also register a validator (like the `parse` function from a
69
+ * database table's form props) — the form runs it alongside the form-level
70
+ * schema. When both produce an error for the same field, the schema wins.
71
+ */
72
+ register: (name: string, element: HTMLElement, validator?: (value: unknown) => unknown) => void;
73
+ /** Removes a field from the form when it unmounts */
74
+ unregister: (name: string) => void;
75
+ /** Updates a field's value in the form data */
76
+ setValue: (name: string, value: unknown) => void;
77
+ /**
78
+ * Reads a field's value from the form data. Resolves dot-notation
79
+ * names against nested data (a literal flat key wins), so fields work
80
+ * over both flat data records and nested entity values.
81
+ */
82
+ getValue: (name: string) => unknown;
83
+ /** Marks a field as touched */
84
+ setTouched: (name: string) => void;
85
+ /** Runs validation for a single field */
86
+ validateField: (name: string) => void;
87
+ }
88
+ import { type Snippet } from 'svelte';
89
+ declare const Form: import("svelte").Component<{
90
+ data?: Record<string, unknown>;
91
+ entity?: FormEntity | undefined;
92
+ schema?: StandardSchema | undefined;
93
+ validate_on?: "change" | "blur" | "submit";
94
+ disabled?: boolean;
95
+ reset_on_submit?: boolean;
96
+ dense?: boolean;
97
+ comfortable?: boolean;
98
+ id?: string;
99
+ class?: string;
100
+ children?: Snippet | undefined;
101
+ onsubmit?: ((detail: {
102
+ data: Record<string, unknown>;
103
+ is_valid: boolean;
104
+ }) => void | Promise<void>) | undefined;
105
+ onchange?: ((detail: {
106
+ data: Record<string, unknown>;
107
+ errors: Record<string, string>;
108
+ }) => void) | undefined;
109
+ onerror?: ((detail: {
110
+ errors: Record<string, string>;
111
+ error?: unknown;
112
+ }) => void) | undefined;
113
+ onsaved?: ((detail: {
114
+ entity: FormEntity;
115
+ }) => void) | undefined;
116
+ onreset?: (() => void) | undefined;
117
+ }, {}, "data">;
118
+ type Form = ReturnType<typeof Form>;
119
+ export default Form;
120
+ //# sourceMappingURL=Form.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Form.svelte.d.ts","sourceRoot":"","sources":["../../src/form/Form.svelte.ts"],"names":[],"mappings":"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"}