@happyvertical/smrt-svelte 0.55.7 → 0.55.8

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 (197) hide show
  1. package/AGENTS.md +7 -44
  2. package/README.md +112 -1
  3. package/agents/browser-ai.md +47 -0
  4. package/agents/command-palette.md +85 -0
  5. package/agents/import-export.md +62 -0
  6. package/agents/overview-surfaces.md +255 -0
  7. package/agents/screens.md +57 -0
  8. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
  9. package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
  10. package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
  11. package/dist/components/command-palette/CommandPalette.svelte +75 -0
  12. package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
  13. package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
  14. package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
  15. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
  16. package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
  17. package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
  18. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
  19. package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
  20. package/dist/components/command-palette/context.d.ts +18 -0
  21. package/dist/components/command-palette/context.d.ts.map +1 -0
  22. package/dist/components/command-palette/context.js +38 -0
  23. package/dist/components/command-palette/controller.svelte.d.ts +70 -0
  24. package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
  25. package/dist/components/command-palette/controller.svelte.js +379 -0
  26. package/dist/components/command-palette/hotkeys.d.ts +30 -0
  27. package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
  28. package/dist/components/command-palette/hotkeys.js +161 -0
  29. package/dist/components/command-palette/index.d.ts +19 -0
  30. package/dist/components/command-palette/index.d.ts.map +1 -0
  31. package/dist/components/command-palette/index.js +17 -0
  32. package/dist/components/command-palette/match.d.ts +38 -0
  33. package/dist/components/command-palette/match.d.ts.map +1 -0
  34. package/dist/components/command-palette/match.js +172 -0
  35. package/dist/components/command-palette/providers.d.ts +159 -0
  36. package/dist/components/command-palette/providers.d.ts.map +1 -0
  37. package/dist/components/command-palette/providers.js +345 -0
  38. package/dist/components/command-palette/rank.d.ts +20 -0
  39. package/dist/components/command-palette/rank.d.ts.map +1 -0
  40. package/dist/components/command-palette/rank.js +74 -0
  41. package/dist/components/command-palette/types.d.ts +119 -0
  42. package/dist/components/command-palette/types.d.ts.map +1 -0
  43. package/dist/components/command-palette/types.js +10 -0
  44. package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
  45. package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
  46. package/dist/components/command-palette/use-provider.svelte.js +23 -0
  47. package/dist/components/import-export/ImportExport.svelte +678 -0
  48. package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
  49. package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
  50. package/dist/components/import-export/collection-adapter.d.ts +35 -0
  51. package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
  52. package/dist/components/import-export/collection-adapter.js +42 -0
  53. package/dist/components/import-export/csv.d.ts +60 -0
  54. package/dist/components/import-export/csv.d.ts.map +1 -0
  55. package/dist/components/import-export/csv.js +192 -0
  56. package/dist/components/import-export/download.d.ts +4 -0
  57. package/dist/components/import-export/download.d.ts.map +1 -0
  58. package/dist/components/import-export/download.js +17 -0
  59. package/dist/components/import-export/export.d.ts +25 -0
  60. package/dist/components/import-export/export.d.ts.map +1 -0
  61. package/dist/components/import-export/export.js +68 -0
  62. package/dist/components/import-export/fields.d.ts +48 -0
  63. package/dist/components/import-export/fields.d.ts.map +1 -0
  64. package/dist/components/import-export/fields.js +103 -0
  65. package/dist/components/import-export/index.d.ts +12 -0
  66. package/dist/components/import-export/index.d.ts.map +1 -0
  67. package/dist/components/import-export/index.js +9 -0
  68. package/dist/components/import-export/mapping.d.ts +12 -0
  69. package/dist/components/import-export/mapping.d.ts.map +1 -0
  70. package/dist/components/import-export/mapping.js +38 -0
  71. package/dist/components/import-export/props.d.ts +34 -0
  72. package/dist/components/import-export/props.d.ts.map +1 -0
  73. package/dist/components/import-export/props.js +1 -0
  74. package/dist/components/import-export/report.d.ts +13 -0
  75. package/dist/components/import-export/report.d.ts.map +1 -0
  76. package/dist/components/import-export/report.js +50 -0
  77. package/dist/components/import-export/runner.d.ts +17 -0
  78. package/dist/components/import-export/runner.d.ts.map +1 -0
  79. package/dist/components/import-export/runner.js +39 -0
  80. package/dist/components/import-export/types.d.ts +100 -0
  81. package/dist/components/import-export/types.d.ts.map +1 -0
  82. package/dist/components/import-export/types.js +6 -0
  83. package/dist/components/import-export/validate.d.ts +39 -0
  84. package/dist/components/import-export/validate.d.ts.map +1 -0
  85. package/dist/components/import-export/validate.js +268 -0
  86. package/dist/components/overview/OverviewGrid.svelte +579 -0
  87. package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
  88. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
  89. package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
  90. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
  91. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
  92. package/dist/components/overview/controller.svelte.d.ts +92 -0
  93. package/dist/components/overview/controller.svelte.d.ts.map +1 -0
  94. package/dist/components/overview/controller.svelte.js +336 -0
  95. package/dist/components/overview/format.d.ts +13 -0
  96. package/dist/components/overview/format.d.ts.map +1 -0
  97. package/dist/components/overview/format.js +45 -0
  98. package/dist/components/overview/grid-types.d.ts +52 -0
  99. package/dist/components/overview/grid-types.d.ts.map +1 -0
  100. package/dist/components/overview/grid-types.js +1 -0
  101. package/dist/components/overview/index.d.ts +24 -0
  102. package/dist/components/overview/index.d.ts.map +1 -0
  103. package/dist/components/overview/index.js +22 -0
  104. package/dist/components/overview/load.d.ts +30 -0
  105. package/dist/components/overview/load.d.ts.map +1 -0
  106. package/dist/components/overview/load.js +92 -0
  107. package/dist/components/overview/markdown.d.ts +53 -0
  108. package/dist/components/overview/markdown.d.ts.map +1 -0
  109. package/dist/components/overview/markdown.js +203 -0
  110. package/dist/components/overview/model.d.ts +100 -0
  111. package/dist/components/overview/model.d.ts.map +1 -0
  112. package/dist/components/overview/model.js +460 -0
  113. package/dist/components/overview/recipe-widgets.d.ts +77 -0
  114. package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
  115. package/dist/components/overview/recipe-widgets.js +151 -0
  116. package/dist/components/overview/registry.d.ts +45 -0
  117. package/dist/components/overview/registry.d.ts.map +1 -0
  118. package/dist/components/overview/registry.js +112 -0
  119. package/dist/components/overview/resize.d.ts +25 -0
  120. package/dist/components/overview/resize.d.ts.map +1 -0
  121. package/dist/components/overview/resize.js +41 -0
  122. package/dist/components/overview/schema.d.ts +34 -0
  123. package/dist/components/overview/schema.d.ts.map +1 -0
  124. package/dist/components/overview/schema.js +180 -0
  125. package/dist/components/overview/server.d.ts +14 -0
  126. package/dist/components/overview/server.d.ts.map +1 -0
  127. package/dist/components/overview/server.js +13 -0
  128. package/dist/components/overview/types.d.ts +235 -0
  129. package/dist/components/overview/types.d.ts.map +1 -0
  130. package/dist/components/overview/types.js +17 -0
  131. package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
  132. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
  133. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
  134. package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
  135. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
  136. package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
  137. package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
  138. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
  139. package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
  140. package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
  141. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
  142. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
  143. package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
  144. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
  145. package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
  146. package/dist/components/overview/widgets/core.d.ts +44 -0
  147. package/dist/components/overview/widgets/core.d.ts.map +1 -0
  148. package/dist/components/overview/widgets/core.js +222 -0
  149. package/dist/components/overview/widgets/data.d.ts +76 -0
  150. package/dist/components/overview/widgets/data.d.ts.map +1 -0
  151. package/dist/components/overview/widgets/data.js +40 -0
  152. package/dist/components/screens/DetailScreen.svelte +220 -0
  153. package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
  154. package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
  155. package/dist/components/screens/EditForm.svelte +313 -0
  156. package/dist/components/screens/EditForm.svelte.d.ts +5 -0
  157. package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
  158. package/dist/components/screens/ListScreen.svelte +191 -0
  159. package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
  160. package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
  161. package/dist/components/screens/RecipeScreens.svelte +237 -0
  162. package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
  163. package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
  164. package/dist/components/screens/fields.d.ts +54 -0
  165. package/dist/components/screens/fields.d.ts.map +1 -0
  166. package/dist/components/screens/fields.js +229 -0
  167. package/dist/components/screens/index.d.ts +8 -0
  168. package/dist/components/screens/index.d.ts.map +1 -0
  169. package/dist/components/screens/index.js +6 -0
  170. package/dist/components/screens/types.d.ts +215 -0
  171. package/dist/components/screens/types.d.ts.map +1 -0
  172. package/dist/components/screens/types.js +1 -0
  173. package/dist/components/screens/values.d.ts +62 -0
  174. package/dist/components/screens/values.d.ts.map +1 -0
  175. package/dist/components/screens/values.js +289 -0
  176. package/dist/components/sortable/controller.svelte.d.ts +10 -0
  177. package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
  178. package/dist/components/sortable/controller.svelte.js +29 -7
  179. package/dist/components/workspace/manifest-nav.d.ts +20 -0
  180. package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
  181. package/dist/components/workspace/manifest-nav.js +42 -13
  182. package/dist/i18n/server.d.ts +6 -0
  183. package/dist/i18n/server.d.ts.map +1 -1
  184. package/dist/i18n/server.js +6 -0
  185. package/dist/i18n/strings.import-export.d.ts +88 -0
  186. package/dist/i18n/strings.import-export.d.ts.map +1 -0
  187. package/dist/i18n/strings.import-export.js +88 -0
  188. package/dist/i18n/strings.overview.d.ts +100 -0
  189. package/dist/i18n/strings.overview.d.ts.map +1 -0
  190. package/dist/i18n/strings.overview.js +107 -0
  191. package/dist/i18n/strings.palette.d.ts +18 -0
  192. package/dist/i18n/strings.palette.d.ts.map +1 -0
  193. package/dist/i18n/strings.palette.js +18 -0
  194. package/dist/i18n/strings.screens.d.ts +31 -0
  195. package/dist/i18n/strings.screens.d.ts.map +1 -0
  196. package/dist/i18n/strings.screens.js +31 -0
  197. package/package.json +45 -16
@@ -0,0 +1,220 @@
1
+ <script lang="ts">
2
+ /**
3
+ * DetailScreen - a read-only record view for any model, derived from its
4
+ * manifest definition and resolved field policy (#3718).
5
+ *
6
+ * Basic-tier fields show first, grouped by the policy/manifest `group`;
7
+ * advanced-tier fields and the read-only created/updated timestamps sit
8
+ * behind a "More details" disclosure. Hidden fields and fields outside the
9
+ * policy never render, so a sensitive field the policy omits cannot leak
10
+ * through this screen.
11
+ */
12
+ import { Alert, ConfirmDialog } from '@happyvertical/smrt-ui/feedback';
13
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
14
+ import { PageHeader } from '@happyvertical/smrt-ui/layout';
15
+ import { Button, Disclosure } from '@happyvertical/smrt-ui/ui';
16
+ import { M } from '../../i18n/strings.screens.js';
17
+ import {
18
+ deriveScreenFields,
19
+ groupScreenFields,
20
+ screenSourceError,
21
+ screenTitles,
22
+ } from './fields.js';
23
+ import type {
24
+ DetailScreenProps,
25
+ ScreenCollectionDefinition,
26
+ ScreenField,
27
+ ScreenPolicy,
28
+ ScreenRecord,
29
+ } from './types.js';
30
+ import { formatScreenValue } from './values.js';
31
+
32
+ let {
33
+ definition,
34
+ policy = null,
35
+ record,
36
+ title,
37
+ fields: include,
38
+ currency,
39
+ locale,
40
+ showHeader = true,
41
+ backLabel,
42
+ deleting = false,
43
+ onback,
44
+ onedit,
45
+ ondelete,
46
+ extraActions,
47
+ children,
48
+ }: DetailScreenProps = $props();
49
+
50
+ const { t } = useI18n();
51
+
52
+ const titles = $derived(screenTitles(definition));
53
+ const sourceError = $derived(screenSourceError(definition, policy));
54
+
55
+ const allFields = $derived(
56
+ sourceError
57
+ ? []
58
+ : deriveScreenFields(definition, policy, { mode: 'view', include }),
59
+ );
60
+ const primary = $derived(allFields.filter((f) => f.tier === 'basic'));
61
+ const secondary = $derived(allFields.filter((f) => f.tier === 'advanced'));
62
+
63
+ const heading = $derived.by(() => {
64
+ if (title) return title;
65
+ const label = allFields.find((f) => f.kind === 'text' && f.tier === 'basic');
66
+ const value = label ? record[label.name] : undefined;
67
+ return typeof value === 'string' && value.trim() !== ''
68
+ ? value
69
+ : titles.singular;
70
+ });
71
+
72
+ const formatContext = $derived({
73
+ currency,
74
+ locale,
75
+ yes: t(M['ui.screens.yes']),
76
+ no: t(M['ui.screens.no']),
77
+ });
78
+
79
+ function display(field: ScreenField): string {
80
+ return formatScreenValue(field, record[field.name], formatContext);
81
+ }
82
+
83
+ /** Only http(s) URLs become links; anything else renders as text. */
84
+ function safeHref(field: ScreenField, text: string): string | null {
85
+ if (field.kind === 'email') return `mailto:${text}`;
86
+ if (field.kind === 'url' && /^https?:\/\//i.test(text)) return text;
87
+ return null;
88
+ }
89
+
90
+ let confirmOpen = $state(false);
91
+ let deleteError = $state(false);
92
+
93
+ async function confirmDelete(): Promise<void> {
94
+ deleteError = false;
95
+ try {
96
+ await ondelete?.();
97
+ confirmOpen = false;
98
+ } catch {
99
+ deleteError = true;
100
+ confirmOpen = false;
101
+ }
102
+ }
103
+ </script>
104
+
105
+ {#snippet fieldList(list: ScreenField[])}
106
+ {#each groupScreenFields(list) as [group, groupFields] (group ?? '')}
107
+ <section class="smrt-detail-screen__group">
108
+ {#if group}<h2 class="smrt-detail-screen__group-title">{group}</h2>{/if}
109
+ <dl class="smrt-detail-screen__fields">
110
+ {#each groupFields as field (field.name)}
111
+ {@const text = display(field)}
112
+ <div class="smrt-detail-screen__field">
113
+ <dt>{field.label}</dt>
114
+ <dd>
115
+ {#if text === ''}
116
+ <span class="smrt-detail-screen__empty">{t(M['ui.screens.not_set'])}</span>
117
+ {:else if field.kind === 'json' || field.kind === 'textarea'}
118
+ <pre class="smrt-detail-screen__block">{text}</pre>
119
+ {:else if safeHref(field, text)}
120
+ <a href={safeHref(field, text)}>{text}</a>
121
+ {:else if field.kind === 'reference'}
122
+ <code>{text}</code>
123
+ {:else}
124
+ {text}
125
+ {/if}
126
+ </dd>
127
+ </div>
128
+ {/each}
129
+ </dl>
130
+ </section>
131
+ {/each}
132
+ {/snippet}
133
+
134
+ {#if showHeader}
135
+ <PageHeader title={heading}>
136
+ {#snippet actions()}
137
+ {#if onback}
138
+ <Button variant="ghost" onclick={onback}>
139
+ {backLabel ?? t(M['ui.screens.back'], { name: titles.plural.toLowerCase() })}
140
+ </Button>
141
+ {/if}
142
+ {#if extraActions}{@render extraActions()}{/if}
143
+ {#if onedit}
144
+ <Button variant="secondary" onclick={onedit}>{t(M['ui.screens.edit'])}</Button>
145
+ {/if}
146
+ {#if ondelete}
147
+ <Button variant="danger" onclick={() => (confirmOpen = true)}>
148
+ {t(M['ui.screens.delete'])}
149
+ </Button>
150
+ {/if}
151
+ {/snippet}
152
+ </PageHeader>
153
+ {/if}
154
+
155
+ <article class="smrt-detail-screen" aria-label={heading}>
156
+ {#if sourceError}
157
+ <Alert variant="error">{sourceError}</Alert>
158
+ {:else}
159
+ {#if deleteError}
160
+ <Alert variant="error" dismissible ondismiss={() => (deleteError = false)}>
161
+ {t(M['ui.screens.delete_failed'])}
162
+ </Alert>
163
+ {/if}
164
+ {@render fieldList(primary)}
165
+ {#if secondary.length > 0}
166
+ <Disclosure title={t(M['ui.screens.more_details'])}>
167
+ {@render fieldList(secondary)}
168
+ </Disclosure>
169
+ {/if}
170
+ {@render children?.()}
171
+ {/if}
172
+ </article>
173
+
174
+ {#if ondelete}
175
+ <ConfirmDialog
176
+ open={confirmOpen}
177
+ title={t(M['ui.screens.delete_title'], { name: titles.singular.toLowerCase() })}
178
+ message={t(M['ui.screens.delete_message'])}
179
+ confirmLabel={t(M['ui.screens.delete'])}
180
+ cancelLabel={t(M['ui.screens.cancel'])}
181
+ destructive
182
+ loading={deleting}
183
+ onconfirm={confirmDelete}
184
+ oncancel={() => (confirmOpen = false)}
185
+ />
186
+ {/if}
187
+
188
+ <style>
189
+ .smrt-detail-screen {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: var(--smrt-spacing-lg, 1.5rem);
193
+ }
194
+ .smrt-detail-screen__group-title {
195
+ margin: 0 0 var(--smrt-spacing-sm, 0.5rem);
196
+ font-size: var(--smrt-typography-title-medium-size, 1rem);
197
+ }
198
+ .smrt-detail-screen__fields {
199
+ display: grid;
200
+ grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr));
201
+ gap: var(--smrt-spacing-md, 1rem);
202
+ margin: 0;
203
+ }
204
+ .smrt-detail-screen__field dt {
205
+ color: var(--smrt-color-on-surface-variant, #5f6368);
206
+ font-size: var(--smrt-typography-body-medium-size, 0.875rem);
207
+ }
208
+ .smrt-detail-screen__field dd {
209
+ margin: 0;
210
+ overflow-wrap: anywhere;
211
+ }
212
+ .smrt-detail-screen__empty {
213
+ color: var(--smrt-color-on-surface-variant, #5f6368);
214
+ }
215
+ .smrt-detail-screen__block {
216
+ margin: 0;
217
+ white-space: pre-wrap;
218
+ font-family: inherit;
219
+ }
220
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { DetailScreenProps } from './types.js';
2
+ declare const DetailScreen: import("svelte").Component<DetailScreenProps, {}, "">;
3
+ type DetailScreen = ReturnType<typeof DetailScreen>;
4
+ export default DetailScreen;
5
+ //# sourceMappingURL=DetailScreen.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DetailScreen.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/DetailScreen.svelte.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EACV,iBAAiB,EAKlB,MAAM,YAAY,CAAC;AAoKpB,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,313 @@
1
+ <script lang="ts">
2
+ /**
3
+ * EditForm - a create / edit form for any model, derived from its manifest
4
+ * definition and resolved field policy (#3718).
5
+ *
6
+ * Policy behavior: basic-tier fields render up front and advanced-tier fields
7
+ * sit behind an "Advanced fields" disclosure (opened automatically when one
8
+ * has a problem); hidden fields and fields outside the policy never render;
9
+ * policy labels, help and order win over the manifest; on create, resolved
10
+ * policy defaults prefill the draft. System fields are never editable.
11
+ *
12
+ * The form is transport-neutral. `onsubmit` receives a payload already
13
+ * converted to wire values (safe integers, integer minor units for money,
14
+ * ISO datetimes, parsed JSON); throw to report a failed save, or return
15
+ * `{ fieldErrors }` to attach server-side validation messages to fields.
16
+ * Remount with `{#key}` when switching to another record.
17
+ *
18
+ * For the policy gear, usage telemetry and per-field renderer registry use
19
+ * `ObjectForm` from `@happyvertical/smrt-fields/svelte` instead.
20
+ */
21
+ import { Alert } from '@happyvertical/smrt-ui/feedback';
22
+ import {
23
+ Checkbox,
24
+ Form,
25
+ FormActionBar,
26
+ FormGroup,
27
+ Input,
28
+ Textarea,
29
+ } from '@happyvertical/smrt-ui/forms';
30
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
31
+ import { PageHeader } from '@happyvertical/smrt-ui/layout';
32
+ import { Button, Disclosure } from '@happyvertical/smrt-ui/ui';
33
+ import { untrack } from 'svelte';
34
+ import { M } from '../../i18n/strings.screens.js';
35
+ import {
36
+ deriveScreenFields,
37
+ groupScreenFields,
38
+ screenSourceError,
39
+ screenTitles,
40
+ } from './fields.js';
41
+ import type {
42
+ EditFormProps,
43
+ ScreenCollectionDefinition,
44
+ ScreenDraft,
45
+ ScreenField,
46
+ ScreenFieldErrorCode,
47
+ ScreenPolicy,
48
+ ScreenRecord,
49
+ } from './types.js';
50
+ import {
51
+ DEFAULT_SCREEN_CURRENCY,
52
+ draftForCreate,
53
+ draftFromRecord,
54
+ parseDraft,
55
+ } from './values.js';
56
+
57
+ let {
58
+ definition,
59
+ policy = null,
60
+ record,
61
+ title,
62
+ fields: include,
63
+ currency = DEFAULT_SCREEN_CURRENCY,
64
+ showHeader = true,
65
+ submitting = false,
66
+ fieldErrors = {},
67
+ error = null,
68
+ onsubmit,
69
+ oncancel,
70
+ }: EditFormProps = $props();
71
+
72
+ const { t } = useI18n();
73
+
74
+ const isNew = $derived(record === undefined);
75
+ const titles = $derived(screenTitles(definition));
76
+ const sourceError = $derived(screenSourceError(definition, policy));
77
+ const heading = $derived(
78
+ title ??
79
+ t(M[isNew ? 'ui.screens.create_title' : 'ui.screens.edit_title'], {
80
+ name: titles.singular.toLowerCase(),
81
+ }),
82
+ );
83
+
84
+ const allFields = $derived(
85
+ sourceError
86
+ ? []
87
+ : deriveScreenFields(definition, policy, { mode: 'edit', include }),
88
+ );
89
+ const basicFields = $derived(allFields.filter((f) => f.tier === 'basic'));
90
+ const advancedFields = $derived(allFields.filter((f) => f.tier === 'advanced'));
91
+
92
+ function initialDraft(): ScreenDraft {
93
+ return record
94
+ ? draftFromRecord(allFields, record, { currency })
95
+ : draftForCreate(allFields, policy, { currency });
96
+ }
97
+
98
+ let draft = $state<ScreenDraft>(initialDraft());
99
+ let localErrors = $state<Record<string, ScreenFieldErrorCode>>({});
100
+ let hostErrors = $state<Record<string, string>>({});
101
+ let failed = $state(false);
102
+ let working = $state(false);
103
+ let advancedOpen = $state(false);
104
+
105
+ // Re-seed when the host swaps the record (or the field set) under a mounted
106
+ // form; typing never re-seeds because only the identity is tracked.
107
+ const sessionKey = $derived(
108
+ `${definition.objectRef}:${record ? String(record[definition.idField ?? 'id']) : 'new'}:${allFields.map((f) => f.name).join(',')}`,
109
+ );
110
+ let appliedKey = untrack(() => sessionKey);
111
+ $effect(() => {
112
+ const key = sessionKey;
113
+ if (key === appliedKey) return;
114
+ appliedKey = key;
115
+ untrack(() => {
116
+ draft = initialDraft();
117
+ localErrors = {};
118
+ hostErrors = {};
119
+ failed = false;
120
+ });
121
+ });
122
+
123
+ function text(name: string): string {
124
+ const value = draft[name];
125
+ return typeof value === 'string' ? value : '';
126
+ }
127
+
128
+ function setText(name: string, value: string | number | undefined): void {
129
+ draft[name] = String(value ?? '');
130
+ }
131
+
132
+ function messageFor(field: ScreenField): string | undefined {
133
+ const code = localErrors[field.name];
134
+ if (code) return t(M[`ui.screens.error.${code}`]);
135
+ return fieldErrors[field.name] ?? hostErrors[field.name];
136
+ }
137
+
138
+ const advancedHasProblem = $derived(
139
+ advancedFields.some(
140
+ (f) => localErrors[f.name] || fieldErrors[f.name] || hostErrors[f.name],
141
+ ),
142
+ );
143
+
144
+ function hint(field: ScreenField): string | undefined {
145
+ return field.help ?? undefined;
146
+ }
147
+
148
+ function label(field: ScreenField): string {
149
+ return field.kind === 'money' ? `${field.label} (${currency})` : field.label;
150
+ }
151
+
152
+ async function submit(): Promise<void> {
153
+ if (working || submitting) return;
154
+ failed = false;
155
+ hostErrors = {};
156
+ const parsed = parseDraft(allFields, draft, { currency, record });
157
+ localErrors = parsed.errors;
158
+ if (Object.keys(parsed.errors).length > 0) {
159
+ if (advancedFields.some((f) => parsed.errors[f.name])) advancedOpen = true;
160
+ return;
161
+ }
162
+ working = true;
163
+ try {
164
+ const result = await onsubmit(parsed.values, { isNew });
165
+ if (result?.fieldErrors) {
166
+ hostErrors = { ...result.fieldErrors };
167
+ if (advancedFields.some((f) => result.fieldErrors?.[f.name])) {
168
+ advancedOpen = true;
169
+ }
170
+ }
171
+ } catch {
172
+ failed = true;
173
+ } finally {
174
+ working = false;
175
+ }
176
+ }
177
+
178
+ const busy = $derived(working || submitting);
179
+ const formError = $derived(
180
+ error ?? (failed ? t(M['ui.screens.save_failed']) : null),
181
+ );
182
+ </script>
183
+
184
+ {#snippet control(field: ScreenField)}
185
+ {#if field.kind === 'boolean'}
186
+ <div class="smrt-edit-form__check">
187
+ <Checkbox
188
+ name={field.name}
189
+ label={field.label}
190
+ disabled={busy}
191
+ bind:checked={() => draft[field.name] === true, (v) => (draft[field.name] = v)}
192
+ />
193
+ {#if field.help}
194
+ <small class="smrt-edit-form__hint">{field.help}</small>
195
+ {/if}
196
+ {#if messageFor(field)}
197
+ <small class="smrt-edit-form__error" role="alert">{messageFor(field)}</small>
198
+ {/if}
199
+ </div>
200
+ {:else}
201
+ <FormGroup
202
+ label={label(field)}
203
+ hint={hint(field)}
204
+ error={messageFor(field)}
205
+ required={field.required}
206
+ >
207
+ {#if field.kind === 'textarea' || field.kind === 'json'}
208
+ <Textarea
209
+ name={field.name}
210
+ rows={field.kind === 'json' ? 8 : 4}
211
+ disabled={busy}
212
+ required={field.required}
213
+ bind:value={() => text(field.name), (v) => setText(field.name, v)}
214
+ />
215
+ {:else}
216
+ <Input
217
+ name={field.name}
218
+ type={field.kind === 'email'
219
+ ? 'email'
220
+ : field.kind === 'url'
221
+ ? 'url'
222
+ : field.kind === 'tel'
223
+ ? 'tel'
224
+ : field.kind === 'integer' || field.kind === 'decimal'
225
+ ? 'number'
226
+ : field.kind === 'datetime'
227
+ ? 'datetime-local'
228
+ : 'text'}
229
+ step={field.kind === 'integer' ? '1' : field.kind === 'decimal' ? 'any' : undefined}
230
+ inputmode={field.kind === 'money' ? 'decimal' : undefined}
231
+ disabled={busy}
232
+ required={field.required}
233
+ bind:value={() => text(field.name), (v) => setText(field.name, v)}
234
+ />
235
+ {/if}
236
+ </FormGroup>
237
+ {/if}
238
+ {/snippet}
239
+
240
+ {#snippet fieldList(list: ScreenField[])}
241
+ {#each groupScreenFields(list) as [group, groupFields] (group ?? '')}
242
+ <fieldset class="smrt-edit-form__group">
243
+ {#if group}<legend>{group}</legend>{/if}
244
+ {#each groupFields as field (field.name)}
245
+ {@render control(field)}
246
+ {/each}
247
+ </fieldset>
248
+ {/each}
249
+ {/snippet}
250
+
251
+ {#if showHeader}
252
+ <PageHeader title={heading} />
253
+ {/if}
254
+
255
+ <section class="smrt-edit-form" aria-label={heading}>
256
+ {#if sourceError}
257
+ <Alert variant="error">{sourceError}</Alert>
258
+ {:else}
259
+ <Form onsubmit={submit} novalidate>
260
+ {#if formError}
261
+ <Alert variant="error">{formError}</Alert>
262
+ {/if}
263
+ {@render fieldList(basicFields)}
264
+ {#if advancedFields.length > 0}
265
+ <Disclosure
266
+ title={t(M['ui.screens.advanced_fields'])}
267
+ bind:open={() => advancedOpen || advancedHasProblem, (v) => (advancedOpen = v)}
268
+ >
269
+ {@render fieldList(advancedFields)}
270
+ </Disclosure>
271
+ {/if}
272
+ <FormActionBar>
273
+ {#if oncancel}
274
+ <Button type="button" variant="ghost" disabled={busy} onclick={oncancel}>
275
+ {t(M['ui.screens.cancel'])}
276
+ </Button>
277
+ {/if}
278
+ <Button type="submit" variant="primary" loading={busy} disabled={busy}>
279
+ {t(M['ui.screens.save'])}
280
+ </Button>
281
+ </FormActionBar>
282
+ </Form>
283
+ {/if}
284
+ </section>
285
+
286
+ <style>
287
+ .smrt-edit-form {
288
+ max-width: 40rem;
289
+ }
290
+ .smrt-edit-form__group {
291
+ border: 0;
292
+ margin: 0 0 var(--smrt-spacing-md, 1rem);
293
+ padding: 0;
294
+ display: flex;
295
+ flex-direction: column;
296
+ gap: var(--smrt-spacing-md, 1rem);
297
+ }
298
+ .smrt-edit-form__group legend {
299
+ font-weight: var(--smrt-typography-weight-semibold, 600);
300
+ margin-bottom: var(--smrt-spacing-sm, 0.5rem);
301
+ }
302
+ .smrt-edit-form__check {
303
+ display: flex;
304
+ flex-direction: column;
305
+ gap: var(--smrt-spacing-xs, 0.25rem);
306
+ }
307
+ .smrt-edit-form__hint {
308
+ color: var(--smrt-color-on-surface-variant, #5f6368);
309
+ }
310
+ .smrt-edit-form__error {
311
+ color: var(--smrt-color-error, #b42318);
312
+ }
313
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { EditFormProps } from './types.js';
2
+ declare const EditForm: import("svelte").Component<EditFormProps, {}, "">;
3
+ type EditForm = ReturnType<typeof EditForm>;
4
+ export default EditForm;
5
+ //# sourceMappingURL=EditForm.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EditForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/screens/EditForm.svelte.ts"],"names":[],"mappings":"AA0CA,OAAO,KAAK,EACV,aAAa,EAOd,MAAM,YAAY,CAAC;AAmOpB,QAAA,MAAM,QAAQ,mDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}