@happyvertical/smrt-svelte 0.55.7 → 0.55.9

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,193 @@
1
+ <script lang="ts">
2
+ /**
3
+ * WidgetOptionsForm - edits one widget's options (#3727). A widget's options
4
+ * are plain fields, so this reads like the generated record forms: smrt-ui
5
+ * form controls driven by the option field descriptors (label, help,
6
+ * required, type), with the same save/cancel bar. It validates through the
7
+ * widget's schema (via `onsave`) and shows each problem beside its field.
8
+ */
9
+ import { Alert } from '@happyvertical/smrt-ui/feedback';
10
+ import {
11
+ Checkbox,
12
+ Form,
13
+ FormActionBar,
14
+ FormGroup,
15
+ Input,
16
+ Select,
17
+ Textarea,
18
+ } from '@happyvertical/smrt-ui/forms';
19
+ import { useI18n } from '@happyvertical/smrt-ui/i18n';
20
+ import { Button } from '@happyvertical/smrt-ui/ui';
21
+ import { untrack } from 'svelte';
22
+ import { M } from '../../i18n/strings.overview.js';
23
+ import type { WidgetOptionsFormProps } from './grid-types.js';
24
+ import type { OverviewOptions, WidgetOptionField } from './types.js';
25
+
26
+ let { definition, initial, models, onsave, oncancel }: WidgetOptionsFormProps =
27
+ $props();
28
+
29
+ const { t } = useI18n();
30
+
31
+ const fields = $derived(definition.options);
32
+
33
+ function seed(): {
34
+ text: Record<string, string>;
35
+ flags: Record<string, boolean>;
36
+ } {
37
+ const text: Record<string, string> = {};
38
+ const flags: Record<string, boolean> = {};
39
+ for (const field of definition.options) {
40
+ const value = initial[field.key];
41
+ if (field.type === 'boolean') flags[field.key] = value === true;
42
+ else
43
+ text[field.key] =
44
+ value === undefined || value === null ? '' : String(value);
45
+ }
46
+ return { text, flags };
47
+ }
48
+
49
+ const seeded = untrack(seed);
50
+ let text = $state<Record<string, string>>(seeded.text);
51
+ let flags = $state<Record<string, boolean>>(seeded.flags);
52
+ let issues = $state<Record<string, string>>({});
53
+ let failed = $state(false);
54
+
55
+ function toOptions(): OverviewOptions {
56
+ const options: OverviewOptions = {};
57
+ for (const field of fields) {
58
+ if (field.type === 'boolean') {
59
+ options[field.key] = flags[field.key] === true;
60
+ continue;
61
+ }
62
+ const raw = text[field.key] ?? '';
63
+ if (field.type === 'markdown') {
64
+ options[field.key] = raw;
65
+ } else if (raw.trim() === '') {
66
+ } else if (field.type === 'integer' || field.type === 'number') {
67
+ // NaN is not a primitive option: it fails validation as an invalid type.
68
+ options[field.key] = Number(raw);
69
+ } else {
70
+ options[field.key] = raw.trim();
71
+ }
72
+ }
73
+ return options;
74
+ }
75
+
76
+ const ISSUE_TEXT: Record<string, string> = {
77
+ required: M['ui.overview.issue_required'],
78
+ invalid_type: M['ui.overview.issue_invalid_type'],
79
+ too_long: M['ui.overview.issue_too_long'],
80
+ out_of_range: M['ui.overview.issue_out_of_range'],
81
+ not_in_choices: M['ui.overview.issue_not_in_choices'],
82
+ invalid_format: M['ui.overview.issue_invalid_format'],
83
+ not_allowed: M['ui.overview.issue_not_allowed'],
84
+ unknown_option: M['ui.overview.issue_unknown_option'],
85
+ };
86
+
87
+ function issueText(code: string): string {
88
+ return t(ISSUE_TEXT[code] ?? M['ui.overview.issue_invalid_type']);
89
+ }
90
+
91
+ function submit(): void {
92
+ const options = toOptions();
93
+ const problems = onsave(options);
94
+ if (problems === null) return;
95
+ const next: Record<string, string> = {};
96
+ for (const problem of problems) {
97
+ next[problem.key] ??= issueText(problem.code);
98
+ }
99
+ issues = next;
100
+ failed = true;
101
+ }
102
+
103
+ const label = (field: WidgetOptionField): string => t(field.label);
104
+ const hint = (field: WidgetOptionField): string | undefined =>
105
+ field.help ? t(field.help) : undefined;
106
+ const optionalChoice = (field: WidgetOptionField): boolean =>
107
+ !field.required && (field.default === undefined || field.default === null);
108
+ const modelValues = $derived(
109
+ new Set((models ?? []).map((model) => model.value)),
110
+ );
111
+ </script>
112
+
113
+ <Form onsubmit={submit} novalidate>
114
+ {#if failed}
115
+ <Alert variant="error">{t(M['ui.overview.options_failed'])}</Alert>
116
+ {/if}
117
+ {#if fields.length === 0}
118
+ <p class="smrt-widget-form__none">{t(M['ui.overview.options_none'])}</p>
119
+ {/if}
120
+ {#each fields as field (field.key)}
121
+ {#if field.type === 'boolean'}
122
+ <div class="smrt-widget-form__check">
123
+ <Checkbox
124
+ name={field.key}
125
+ label={label(field)}
126
+ bind:checked={() => flags[field.key] === true, (v) => (flags[field.key] = v)}
127
+ />
128
+ {#if hint(field)}<small class="smrt-widget-form__hint">{hint(field)}</small>{/if}
129
+ {#if issues[field.key]}<small class="smrt-widget-form__error" role="alert">{issues[field.key]}</small>{/if}
130
+ </div>
131
+ {:else}
132
+ <FormGroup label={label(field)} hint={hint(field)} error={issues[field.key]} required={field.required}>
133
+ {#if field.type === 'markdown'}
134
+ <Textarea
135
+ name={field.key}
136
+ rows={6}
137
+ bind:value={() => text[field.key] ?? '', (v) => (text[field.key] = v)}
138
+ />
139
+ {:else if field.type === 'enum'}
140
+ <Select
141
+ name={field.key}
142
+ bind:value={() => text[field.key] ?? '', (v) => (text[field.key] = v)}
143
+ >
144
+ {#if optionalChoice(field)}
145
+ <option value="">{t(M['ui.overview.options_choose'])}</option>
146
+ {/if}
147
+ {#each field.choices ?? [] as choice (choice.value)}
148
+ <option value={choice.value}>{t(choice.label)}</option>
149
+ {/each}
150
+ </Select>
151
+ {:else if field.type === 'model' && models && models.length > 0}
152
+ <Select
153
+ name={field.key}
154
+ bind:value={() => text[field.key] ?? '', (v) => (text[field.key] = v)}
155
+ >
156
+ <option value="">{t(M['ui.overview.options_choose'])}</option>
157
+ {#if text[field.key] && !modelValues.has(text[field.key])}
158
+ <option value={text[field.key]}>{text[field.key]}</option>
159
+ {/if}
160
+ {#each models as model (model.value)}
161
+ <option value={model.value}>{model.label}</option>
162
+ {/each}
163
+ </Select>
164
+ {:else}
165
+ <Input
166
+ name={field.key}
167
+ type={field.type === 'integer' || field.type === 'number' ? 'number' : 'text'}
168
+ step={field.type === 'integer' ? '1' : field.type === 'number' ? 'any' : undefined}
169
+ min={field.min}
170
+ max={field.max}
171
+ maxlength={field.type === 'text' ? (field.maxLength ?? 200) : undefined}
172
+ bind:value={() => text[field.key] ?? '', (v) => (text[field.key] = String(v))}
173
+ />
174
+ {/if}
175
+ </FormGroup>
176
+ {/if}
177
+ {/each}
178
+ <FormActionBar>
179
+ <Button type="button" variant="ghost" onclick={oncancel}>
180
+ {t(M['ui.overview.options_cancel'])}
181
+ </Button>
182
+ <Button type="submit" variant="primary">
183
+ {t(M['ui.overview.options_save'])}
184
+ </Button>
185
+ </FormActionBar>
186
+ </Form>
187
+
188
+ <style>
189
+ .smrt-widget-form__check { display: flex; flex-direction: column; gap: var(--smrt-spacing-1); }
190
+ .smrt-widget-form__hint { color: var(--smrt-color-on-surface-variant); }
191
+ .smrt-widget-form__error { color: var(--smrt-color-error); }
192
+ .smrt-widget-form__none { margin: 0; color: var(--smrt-color-on-surface-variant); }
193
+ </style>
@@ -0,0 +1,5 @@
1
+ import type { WidgetOptionsFormProps } from './grid-types.js';
2
+ declare const WidgetOptionsForm: import("svelte").Component<WidgetOptionsFormProps, {}, "">;
3
+ type WidgetOptionsForm = ReturnType<typeof WidgetOptionsForm>;
4
+ export default WidgetOptionsForm;
5
+ //# sourceMappingURL=WidgetOptionsForm.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WidgetOptionsForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/WidgetOptionsForm.svelte.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AA8J9D,QAAA,MAAM,iBAAiB,4DAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,92 @@
1
+ import { type WidgetRegistry } from './registry.js';
2
+ import type { LoadedOverview, LoadedWidgetErrorCode, OverviewDefinition, OverviewDocument, OverviewIssue, OverviewOptions, OverviewOverride, OverviewWidget, RegisteredWidget, WidgetOptionIssue, WidgetStatus } from './types.js';
3
+ /** Where one widget's data stands. */
4
+ export interface OverviewWidgetEntry {
5
+ status: WidgetStatus;
6
+ data?: unknown;
7
+ error?: LoadedWidgetErrorCode;
8
+ }
9
+ export type OverviewOpResult = {
10
+ ok: true;
11
+ id: string;
12
+ } | {
13
+ ok: false;
14
+ reason: 'not_allowed' | 'unknown_widget' | 'unknown_type' | 'invalid_options' | 'limit' | 'unchanged';
15
+ issues?: WidgetOptionIssue[];
16
+ };
17
+ export interface OverviewControllerOptions {
18
+ definition: OverviewDefinition;
19
+ /** Defaults to the shared registry. */
20
+ registry?: WidgetRegistry;
21
+ /**
22
+ * The stored override (untrusted JSON; `null` for none). When given the host
23
+ * is the single source of truth and must feed edits back through it.
24
+ */
25
+ override?: () => unknown;
26
+ /** Receives the canonical override after each edit; `null` = defaults. */
27
+ onchange?: (override: OverviewOverride | null) => void | Promise<void>;
28
+ /** Whether the viewer may customize (default true). Hosts derive it from roles. */
29
+ canCustomize?: () => boolean;
30
+ /** Data the page's server load already produced; seeds the entries. */
31
+ loaded?: LoadedOverview | (() => LoadedOverview | undefined);
32
+ /**
33
+ * Loads one widget's data after an add or an options change. The host runs
34
+ * the widget's `load` where its data lives: a remote function or endpoint in
35
+ * production, the in-browser source in the planner. Must return JSON data.
36
+ */
37
+ loadWidget?: (widget: OverviewWidget, signal: AbortSignal) => Promise<unknown>;
38
+ }
39
+ export declare class OverviewController {
40
+ #private;
41
+ constructor(options: OverviewControllerOptions);
42
+ get definition(): OverviewDefinition;
43
+ get registry(): WidgetRegistry;
44
+ /** The sanitized document to render. */
45
+ get document(): OverviewDocument;
46
+ /** The sanitized defaults. */
47
+ get base(): OverviewDocument;
48
+ /** The canonical override currently applied; `null` when on defaults. */
49
+ get override(): OverviewOverride | null;
50
+ /** What was dropped or reported while resolving. */
51
+ get issues(): readonly OverviewIssue[];
52
+ /** True when the viewer has changed anything from the defaults. */
53
+ get customized(): boolean;
54
+ get canCustomize(): boolean;
55
+ /** Whether another widget fits under the cap. */
56
+ get full(): boolean;
57
+ /** Registered widget types this overview allows, in registration order. */
58
+ get addable(): RegisteredWidget[];
59
+ /** Data state of a widget (loading while a load is outstanding). */
60
+ entry(widget: OverviewWidget): OverviewWidgetEntry;
61
+ /** Add a widget of `type` at the end, with the given (or default) options. */
62
+ add(type: string, options?: OverviewOptions): OverviewOpResult;
63
+ remove(id: string): OverviewOpResult;
64
+ /** Move a widget so it sits at `toIndex` once it is taken out of the list. */
65
+ move(id: string, toIndex: number): OverviewOpResult;
66
+ /** Set a widget's column span (clamped to the widget's range). */
67
+ resize(id: string, span: number): OverviewOpResult;
68
+ /** Replace a widget's options after validating them against its schema. */
69
+ setOptions(id: string, options: OverviewOptions): OverviewOpResult;
70
+ /** Put one widget back to its default (or remove it when it is not one). */
71
+ resetWidget(id: string): OverviewOpResult;
72
+ /** Drop every customization; the overview returns to its defaults. */
73
+ reset(): OverviewOpResult;
74
+ /**
75
+ * Replace the override wholesale (an assistant's Undo restores the previous
76
+ * value). The value is parsed and sanitized like any stored override.
77
+ */
78
+ restore(override: unknown): OverviewOpResult;
79
+ /** Re-run a widget's load (after an error). */
80
+ reload(id: string): void;
81
+ /**
82
+ * Request data for every widget the entries do not cover. Call from an
83
+ * effect when the document can change outside the controller (a host-owned
84
+ * override). Never called during server render.
85
+ */
86
+ sync(): void;
87
+ /** Abort outstanding loads (call when the owning component is destroyed). */
88
+ destroy(): void;
89
+ }
90
+ /** Create a controller; call during component initialization. */
91
+ export declare function createOverview(options: OverviewControllerOptions): OverviewController;
92
+ //# sourceMappingURL=controller.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/controller.svelte.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,336 @@
1
+ /**
2
+ * The overview controller (#3727): the host-owned, reactive state of one
3
+ * customizable overview. It follows the `ShellLayout` controller pattern:
4
+ * persistence belongs to the host, which stores the sparse override and
5
+ * feeds it back (`override`), and is told about every edit (`onchange`);
6
+ * with no `override` getter the controller keeps the value in memory.
7
+ *
8
+ * Every user operation (and, later, every assistant operation) is a method
9
+ * here. They validate through the same pure model the server uses, so a
10
+ * client edit can never produce a document the server would reject.
11
+ */
12
+ import { untrack } from 'svelte';
13
+ import { diffOverview, isEmptyOverride, nextWidgetId, parseOverviewOverride, resolveOverview, sanitizeOverview, } from './model.js';
14
+ import { defaultWidgetRegistry } from './registry.js';
15
+ import { defaultWidgetOptions, validateWidgetOptions } from './schema.js';
16
+ function optionsKey(widget) {
17
+ return JSON.stringify([widget.type, widget.version ?? 1, widget.options]);
18
+ }
19
+ export class OverviewController {
20
+ #options;
21
+ #registry;
22
+ #local = $state.raw(null);
23
+ #entries = $state.raw({});
24
+ #pending = new Map();
25
+ #resolved = $derived.by(() => resolveOverview(this.#options.definition, this.#options.override ? this.#options.override() : this.#local, this.#registry));
26
+ constructor(options) {
27
+ this.#options = options;
28
+ this.#registry = options.registry ?? defaultWidgetRegistry;
29
+ const loaded = typeof options.loaded === 'function' ? options.loaded() : options.loaded;
30
+ if (loaded)
31
+ this.#seed(loaded);
32
+ }
33
+ #seed(loaded) {
34
+ const entries = {};
35
+ for (const widget of loaded.widgets) {
36
+ entries[widget.id] = {
37
+ key: optionsKey(widget),
38
+ status: widget.status,
39
+ data: widget.data,
40
+ error: widget.error?.code,
41
+ };
42
+ }
43
+ this.#entries = entries;
44
+ }
45
+ get definition() {
46
+ return this.#options.definition;
47
+ }
48
+ get registry() {
49
+ return this.#registry;
50
+ }
51
+ /** The sanitized document to render. */
52
+ get document() {
53
+ return this.#resolved.document;
54
+ }
55
+ /** The sanitized defaults. */
56
+ get base() {
57
+ return this.#resolved.base;
58
+ }
59
+ /** The canonical override currently applied; `null` when on defaults. */
60
+ get override() {
61
+ return diffOverview(this.#resolved.base, this.#resolved.document);
62
+ }
63
+ /** What was dropped or reported while resolving. */
64
+ get issues() {
65
+ return this.#resolved.issues;
66
+ }
67
+ /** True when the viewer has changed anything from the defaults. */
68
+ get customized() {
69
+ return !isEmptyOverride(this.override);
70
+ }
71
+ get canCustomize() {
72
+ return this.#options.canCustomize ? this.#options.canCustomize() : true;
73
+ }
74
+ /** Whether another widget fits under the cap. */
75
+ get full() {
76
+ const cap = this.#options.definition.maxWidgets ?? 24;
77
+ return this.document.widgets.length >= cap;
78
+ }
79
+ /** Registered widget types this overview allows, in registration order. */
80
+ get addable() {
81
+ const { allowed, id } = this.#options.definition;
82
+ return this.#registry
83
+ .list()
84
+ .filter((def) => (!allowed || allowed.includes(def.type)) &&
85
+ (!def.allowedIn || def.allowedIn.includes(id)));
86
+ }
87
+ /** Data state of a widget (loading while a load is outstanding). */
88
+ entry(widget) {
89
+ const entry = this.#entries[widget.id];
90
+ if (entry && entry.key === optionsKey(widget))
91
+ return entry;
92
+ return { status: this.#options.loadWidget ? 'loading' : 'ready' };
93
+ }
94
+ #models() {
95
+ return this.#options.definition.models;
96
+ }
97
+ #gate() {
98
+ return this.canCustomize ? null : { ok: false, reason: 'not_allowed' };
99
+ }
100
+ #commit(next, id) {
101
+ const sanitized = sanitizeOverview(next, {
102
+ registry: this.#registry,
103
+ definition: this.#options.definition,
104
+ });
105
+ const override = diffOverview(this.#resolved.base, sanitized.document);
106
+ if (sameJson(override, this.override)) {
107
+ return { ok: false, reason: 'unchanged' };
108
+ }
109
+ if (!this.#options.override)
110
+ this.#local = override;
111
+ void this.#options.onchange?.(override);
112
+ // The derived document reflects a host-owned override only after the host
113
+ // feeds it back, so sync against what was just committed.
114
+ this.#syncWidgets(sanitized.document.widgets);
115
+ return { ok: true, id };
116
+ }
117
+ #reserved() {
118
+ const override = this.override;
119
+ return [
120
+ ...this.#resolved.base.widgets.map((widget) => widget.id),
121
+ ...this.document.widgets.map((widget) => widget.id),
122
+ ...(override?.removed ?? []),
123
+ ...(override?.added ?? []).map((widget) => widget.id),
124
+ ];
125
+ }
126
+ /** Add a widget of `type` at the end, with the given (or default) options. */
127
+ add(type, options = {}) {
128
+ const denied = this.#gate();
129
+ if (denied)
130
+ return denied;
131
+ const def = this.addable.find((candidate) => candidate.type === type);
132
+ if (!def)
133
+ return { ok: false, reason: 'unknown_type' };
134
+ if (this.full)
135
+ return { ok: false, reason: 'limit' };
136
+ const checked = validateWidgetOptions(def.options, { ...defaultWidgetOptions(def.options), ...options }, { models: this.#models() });
137
+ if (!checked.ok) {
138
+ return { ok: false, reason: 'invalid_options', issues: checked.issues };
139
+ }
140
+ const id = nextWidgetId(this.#reserved());
141
+ return this.#commit({
142
+ widgets: [
143
+ ...this.document.widgets,
144
+ {
145
+ id,
146
+ type,
147
+ span: def.defaultSpan,
148
+ options: checked.options,
149
+ version: def.version,
150
+ },
151
+ ],
152
+ }, id);
153
+ }
154
+ #find(id) {
155
+ return this.document.widgets.find((widget) => widget.id === id);
156
+ }
157
+ remove(id) {
158
+ const denied = this.#gate();
159
+ if (denied)
160
+ return denied;
161
+ if (!this.#find(id))
162
+ return { ok: false, reason: 'unknown_widget' };
163
+ return this.#commit({ widgets: this.document.widgets.filter((widget) => widget.id !== id) }, id);
164
+ }
165
+ /** Move a widget so it sits at `toIndex` once it is taken out of the list. */
166
+ move(id, toIndex) {
167
+ const denied = this.#gate();
168
+ if (denied)
169
+ return denied;
170
+ const widget = this.#find(id);
171
+ if (!widget)
172
+ return { ok: false, reason: 'unknown_widget' };
173
+ const rest = this.document.widgets.filter((entry) => entry.id !== id);
174
+ const index = Math.max(0, Math.min(toIndex, rest.length));
175
+ rest.splice(index, 0, widget);
176
+ return this.#commit({ widgets: rest }, id);
177
+ }
178
+ /** Set a widget's column span (clamped to the widget's range). */
179
+ resize(id, span) {
180
+ const denied = this.#gate();
181
+ if (denied)
182
+ return denied;
183
+ const widget = this.#find(id);
184
+ if (!widget)
185
+ return { ok: false, reason: 'unknown_widget' };
186
+ return this.#commit({
187
+ widgets: this.document.widgets.map((entry) => entry.id === id ? { ...entry, span: Math.round(span) } : entry),
188
+ }, id);
189
+ }
190
+ /** Replace a widget's options after validating them against its schema. */
191
+ setOptions(id, options) {
192
+ const denied = this.#gate();
193
+ if (denied)
194
+ return denied;
195
+ const widget = this.#find(id);
196
+ const def = widget ? this.#registry.get(widget.type) : undefined;
197
+ if (!widget || !def)
198
+ return { ok: false, reason: 'unknown_widget' };
199
+ const checked = validateWidgetOptions(def.options, options, {
200
+ models: this.#models(),
201
+ });
202
+ if (!checked.ok) {
203
+ return { ok: false, reason: 'invalid_options', issues: checked.issues };
204
+ }
205
+ return this.#commit({
206
+ widgets: this.document.widgets.map((entry) => entry.id === id
207
+ ? { ...entry, options: checked.options, version: def.version }
208
+ : entry),
209
+ }, id);
210
+ }
211
+ /** Put one widget back to its default (or remove it when it is not one). */
212
+ resetWidget(id) {
213
+ const denied = this.#gate();
214
+ if (denied)
215
+ return denied;
216
+ const original = this.#resolved.base.widgets.find((widget) => widget.id === id);
217
+ if (!original)
218
+ return this.remove(id);
219
+ return this.#commit({
220
+ widgets: this.document.widgets.some((widget) => widget.id === id)
221
+ ? this.document.widgets.map((widget) => widget.id === id ? { ...original } : widget)
222
+ : [...this.document.widgets, { ...original }],
223
+ }, id);
224
+ }
225
+ /** Drop every customization; the overview returns to its defaults. */
226
+ reset() {
227
+ const denied = this.#gate();
228
+ if (denied)
229
+ return denied;
230
+ if (!this.customized)
231
+ return { ok: false, reason: 'unchanged' };
232
+ if (!this.#options.override)
233
+ this.#local = null;
234
+ void this.#options.onchange?.(null);
235
+ this.#syncWidgets(this.#resolved.base.widgets);
236
+ return { ok: true, id: '' };
237
+ }
238
+ /**
239
+ * Replace the override wholesale (an assistant's Undo restores the previous
240
+ * value). The value is parsed and sanitized like any stored override.
241
+ */
242
+ restore(override) {
243
+ const denied = this.#gate();
244
+ if (denied)
245
+ return denied;
246
+ const { override: parsed } = parseOverviewOverride(override);
247
+ const next = resolveOverview(this.#options.definition, parsed, this.#registry);
248
+ const canonical = diffOverview(next.base, next.document);
249
+ if (sameJson(canonical, this.override)) {
250
+ return { ok: false, reason: 'unchanged' };
251
+ }
252
+ if (!this.#options.override)
253
+ this.#local = canonical;
254
+ void this.#options.onchange?.(canonical);
255
+ this.#syncWidgets(next.document.widgets);
256
+ return { ok: true, id: '' };
257
+ }
258
+ /** Re-run a widget's load (after an error). */
259
+ reload(id) {
260
+ const widget = this.#find(id);
261
+ if (!widget)
262
+ return;
263
+ this.#request(widget);
264
+ }
265
+ /**
266
+ * Request data for every widget the entries do not cover. Call from an
267
+ * effect when the document can change outside the controller (a host-owned
268
+ * override). Never called during server render.
269
+ */
270
+ sync() {
271
+ this.#syncWidgets(this.document.widgets);
272
+ }
273
+ #syncWidgets(widgets) {
274
+ untrack(() => {
275
+ const live = new Set(widgets.map((widget) => widget.id));
276
+ const kept = {};
277
+ for (const [id, entry] of Object.entries(this.#entries)) {
278
+ if (live.has(id))
279
+ kept[id] = entry;
280
+ }
281
+ for (const [id, controller] of this.#pending) {
282
+ if (!live.has(id)) {
283
+ controller.abort();
284
+ this.#pending.delete(id);
285
+ }
286
+ }
287
+ this.#entries = kept;
288
+ if (!this.#options.loadWidget)
289
+ return;
290
+ for (const widget of widgets) {
291
+ const entry = this.#entries[widget.id];
292
+ if (!entry || entry.key !== optionsKey(widget))
293
+ this.#request(widget);
294
+ }
295
+ });
296
+ }
297
+ #request(widget) {
298
+ const load = this.#options.loadWidget;
299
+ if (!load)
300
+ return;
301
+ const key = optionsKey(widget);
302
+ this.#pending.get(widget.id)?.abort();
303
+ const controller = new AbortController();
304
+ this.#pending.set(widget.id, controller);
305
+ this.#entries = {
306
+ ...this.#entries,
307
+ [widget.id]: { key, status: 'loading' },
308
+ };
309
+ const settle = (entry) => {
310
+ if (controller.signal.aborted)
311
+ return;
312
+ if (this.#pending.get(widget.id) === controller) {
313
+ this.#pending.delete(widget.id);
314
+ }
315
+ if (this.#entries[widget.id]?.key !== key)
316
+ return;
317
+ this.#entries = { ...this.#entries, [widget.id]: { key, ...entry } };
318
+ };
319
+ Promise.resolve()
320
+ .then(() => controller.signal.aborted ? undefined : load(widget, controller.signal))
321
+ .then((data) => settle({ status: 'ready', data }), () => settle({ status: 'error', error: 'load_failed' }));
322
+ }
323
+ /** Abort outstanding loads (call when the owning component is destroyed). */
324
+ destroy() {
325
+ for (const controller of this.#pending.values())
326
+ controller.abort();
327
+ this.#pending.clear();
328
+ }
329
+ }
330
+ function sameJson(a, b) {
331
+ return JSON.stringify(a ?? null) === JSON.stringify(b ?? null);
332
+ }
333
+ /** Create a controller; call during component initialization. */
334
+ export function createOverview(options) {
335
+ return new OverviewController(options);
336
+ }
@@ -0,0 +1,13 @@
1
+ export type WidgetValueFormat = 'integer' | 'decimal' | 'percent' | 'money';
2
+ export interface FormatWidgetValueOptions {
3
+ format?: WidgetValueFormat;
4
+ /** ISO 4217 code for `money` (default USD). */
5
+ currency?: string;
6
+ locale: string;
7
+ }
8
+ /**
9
+ * Format a metric value. `money` takes integer minor units, `percent` takes a
10
+ * fraction (0.25 is 25%). Non-finite input renders as an en dash.
11
+ */
12
+ export declare function formatWidgetValue(value: unknown, { format, currency, locale }: FormatWidgetValueOptions): string;
13
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/components/overview/format.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAYD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,EAAE,MAAkB,EAAE,QAAgB,EAAE,MAAM,EAAE,EAAE,wBAAwB,GACzE,MAAM,CA4BR"}