@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,460 @@
1
+ /**
2
+ * The pure overview data model (#3727): parse untrusted JSON, merge a stored
3
+ * override onto the page defaults, derive the sparse override back from an
4
+ * edited document, and sanitize a document against the widget registry.
5
+ *
6
+ * Everything here is total and never throws on bad input: unknown, invalid
7
+ * or disallowed widgets are dropped and reported as {@link OverviewIssue}s,
8
+ * never executed. Svelte-free; runs on the server on save and on load.
9
+ */
10
+ import { optionsEqual, validateWidgetOptions } from './schema.js';
11
+ import { OVERVIEW_DEFAULT_MAX_WIDGETS, OVERVIEW_ID_PATTERN, OVERVIEW_MAX_SPAN, OVERVIEW_MIN_SPAN, OVERVIEW_PAGE_ID_PATTERN, } from './types.js';
12
+ function isRecord(value) {
13
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+ function isSpan(value) {
16
+ return (typeof value === 'number' &&
17
+ Number.isInteger(value) &&
18
+ value >= OVERVIEW_MIN_SPAN &&
19
+ value <= OVERVIEW_MAX_SPAN);
20
+ }
21
+ function primitiveOptions(raw) {
22
+ if (raw === undefined)
23
+ return {};
24
+ if (!isRecord(raw))
25
+ return null;
26
+ const out = {};
27
+ for (const [key, value] of Object.entries(raw)) {
28
+ if (value === null ||
29
+ typeof value === 'string' ||
30
+ typeof value === 'boolean' ||
31
+ (typeof value === 'number' && Number.isFinite(value))) {
32
+ out[key] = value;
33
+ }
34
+ else {
35
+ return null;
36
+ }
37
+ }
38
+ return out;
39
+ }
40
+ /** Shape-check one widget; `null` when it is not a widget at all. */
41
+ function parseWidget(raw, issues) {
42
+ const id = isRecord(raw) && typeof raw.id === 'string' ? raw.id : null;
43
+ const type = isRecord(raw) && typeof raw.type === 'string' ? raw.type : null;
44
+ const malformed = (message) => {
45
+ issues.push({ widgetId: id, type, code: 'malformed', message });
46
+ return null;
47
+ };
48
+ if (!isRecord(raw))
49
+ return malformed('widget is not an object');
50
+ if (id === null || !OVERVIEW_ID_PATTERN.test(id)) {
51
+ return malformed('widget id is missing or invalid');
52
+ }
53
+ if (type === null)
54
+ return malformed('widget type is missing');
55
+ if (!isSpan(raw.span)) {
56
+ issues.push({
57
+ widgetId: id,
58
+ type,
59
+ code: 'invalid_span',
60
+ message: 'span must be an integer from 1 to 4',
61
+ });
62
+ return null;
63
+ }
64
+ const options = primitiveOptions(raw.options);
65
+ if (options === null) {
66
+ issues.push({
67
+ widgetId: id,
68
+ type,
69
+ code: 'invalid_options',
70
+ message: 'options must be an object of primitive values',
71
+ });
72
+ return null;
73
+ }
74
+ const widget = { id, type, span: raw.span, options };
75
+ if (raw.version !== undefined) {
76
+ if (typeof raw.version !== 'number' ||
77
+ !Number.isSafeInteger(raw.version) ||
78
+ raw.version < 1) {
79
+ return malformed('version must be a positive integer');
80
+ }
81
+ widget.version = raw.version;
82
+ }
83
+ return widget;
84
+ }
85
+ /** Parse untrusted JSON into a document. Malformed widgets are dropped. */
86
+ export function parseOverviewDocument(input) {
87
+ const issues = [];
88
+ const widgets = [];
89
+ const list = isRecord(input) && Array.isArray(input.widgets) ? input.widgets : null;
90
+ if (list === null) {
91
+ if (input !== undefined && input !== null) {
92
+ issues.push({
93
+ widgetId: null,
94
+ type: null,
95
+ code: 'malformed',
96
+ message: 'overview must be an object with a widgets array',
97
+ });
98
+ }
99
+ return { document: { widgets }, issues };
100
+ }
101
+ const seen = new Set();
102
+ for (const raw of list) {
103
+ const widget = parseWidget(raw, issues);
104
+ if (!widget)
105
+ continue;
106
+ if (seen.has(widget.id)) {
107
+ issues.push({
108
+ widgetId: widget.id,
109
+ type: widget.type,
110
+ code: 'duplicate_id',
111
+ message: 'widget id appears more than once',
112
+ });
113
+ continue;
114
+ }
115
+ seen.add(widget.id);
116
+ widgets.push(widget);
117
+ }
118
+ return { document: { widgets }, issues };
119
+ }
120
+ /**
121
+ * Parse an untrusted stored override. Never throws; an unknown version, or
122
+ * anything that is not an object, yields `null` (no override).
123
+ */
124
+ export function parseOverviewOverride(input) {
125
+ const issues = [];
126
+ if (input === undefined || input === null)
127
+ return { override: null, issues };
128
+ if (!isRecord(input) || input.version !== 1) {
129
+ issues.push({
130
+ widgetId: null,
131
+ type: null,
132
+ code: 'malformed',
133
+ message: 'override must be an object with version 1',
134
+ });
135
+ return { override: null, issues };
136
+ }
137
+ const override = { version: 1 };
138
+ const ids = (value) => {
139
+ if (!Array.isArray(value))
140
+ return undefined;
141
+ const out = value.filter((id) => typeof id === 'string' && OVERVIEW_ID_PATTERN.test(id));
142
+ return [...new Set(out)];
143
+ };
144
+ const order = ids(input.order);
145
+ if (order?.length)
146
+ override.order = order;
147
+ const removed = ids(input.removed);
148
+ if (removed?.length)
149
+ override.removed = removed;
150
+ if (Array.isArray(input.added)) {
151
+ const { document } = parseOverviewDocument({ widgets: input.added });
152
+ if (document.widgets.length)
153
+ override.added = document.widgets;
154
+ const dropped = input.added.length - document.widgets.length;
155
+ if (dropped > 0) {
156
+ issues.push({
157
+ widgetId: null,
158
+ type: null,
159
+ code: 'malformed',
160
+ message: `${dropped} added widget(s) were malformed and dropped`,
161
+ });
162
+ }
163
+ }
164
+ if (isRecord(input.changed)) {
165
+ const changed = {};
166
+ for (const [id, raw] of Object.entries(input.changed)) {
167
+ if (!OVERVIEW_ID_PATTERN.test(id) || !isRecord(raw))
168
+ continue;
169
+ const patch = {};
170
+ if (isSpan(raw.span))
171
+ patch.span = raw.span;
172
+ const options = raw.options === undefined ? undefined : primitiveOptions(raw.options);
173
+ if (options)
174
+ patch.options = options;
175
+ if (typeof raw.version === 'number' &&
176
+ Number.isSafeInteger(raw.version) &&
177
+ raw.version >= 1) {
178
+ patch.version = raw.version;
179
+ }
180
+ if (Object.keys(patch).length)
181
+ changed[id] = patch;
182
+ }
183
+ if (Object.keys(changed).length)
184
+ override.changed = changed;
185
+ }
186
+ return { override: isEmptyOverride(override) ? null : override, issues };
187
+ }
188
+ /** True when an override changes nothing. */
189
+ export function isEmptyOverride(override) {
190
+ return (override === null ||
191
+ (!override.order?.length &&
192
+ !override.removed?.length &&
193
+ !override.added?.length &&
194
+ !Object.keys(override.changed ?? {}).length));
195
+ }
196
+ function cloneWidget(widget) {
197
+ const copy = {
198
+ id: widget.id,
199
+ type: widget.type,
200
+ span: widget.span,
201
+ options: { ...widget.options },
202
+ };
203
+ if (widget.version !== undefined)
204
+ copy.version = widget.version;
205
+ return copy;
206
+ }
207
+ /**
208
+ * Merge an override onto a base document. Total: ids the base no longer has
209
+ * are ignored, added widgets whose id collides with the base are skipped, and
210
+ * widgets the order does not list keep their slots (listed ids permute among
211
+ * the slots they occupy), so a default shipped later still appears.
212
+ */
213
+ export function applyOverviewOverride(base, override) {
214
+ if (isEmptyOverride(override) || override === null) {
215
+ return { widgets: base.widgets.map(cloneWidget) };
216
+ }
217
+ const removed = new Set(override.removed ?? []);
218
+ const baseIds = new Set(base.widgets.map((widget) => widget.id));
219
+ const widgets = [];
220
+ for (const widget of base.widgets) {
221
+ if (removed.has(widget.id))
222
+ continue;
223
+ const next = cloneWidget(widget);
224
+ const patch = override.changed?.[widget.id];
225
+ if (patch) {
226
+ if (patch.span !== undefined)
227
+ next.span = patch.span;
228
+ if (patch.options !== undefined)
229
+ next.options = { ...patch.options };
230
+ if (patch.version !== undefined)
231
+ next.version = patch.version;
232
+ }
233
+ widgets.push(next);
234
+ }
235
+ for (const widget of override.added ?? []) {
236
+ if (baseIds.has(widget.id))
237
+ continue;
238
+ widgets.push(cloneWidget(widget));
239
+ }
240
+ const order = override.order;
241
+ if (order?.length) {
242
+ const byId = new Map(widgets.map((widget) => [widget.id, widget]));
243
+ const listed = order.filter((id, index) => byId.has(id) && order.indexOf(id) === index);
244
+ const listedSet = new Set(listed);
245
+ const queue = listed.map((id) => byId.get(id));
246
+ let cursor = 0;
247
+ return {
248
+ widgets: widgets.map((widget) => listedSet.has(widget.id) ? queue[cursor++] : widget),
249
+ };
250
+ }
251
+ return { widgets };
252
+ }
253
+ /**
254
+ * The sparse override that turns `base` into `next` (the inverse of
255
+ * {@link applyOverviewOverride}); `null` when they are the same. Both
256
+ * documents should already be sanitized so versions compare like for like.
257
+ */
258
+ export function diffOverview(base, next) {
259
+ const baseById = new Map(base.widgets.map((widget) => [widget.id, widget]));
260
+ const nextIds = new Set(next.widgets.map((widget) => widget.id));
261
+ const override = { version: 1 };
262
+ const removed = base.widgets
263
+ .filter((widget) => !nextIds.has(widget.id))
264
+ .map((widget) => widget.id);
265
+ if (removed.length)
266
+ override.removed = removed;
267
+ const added = next.widgets
268
+ .filter((widget) => !baseById.has(widget.id))
269
+ .map(cloneWidget);
270
+ if (added.length)
271
+ override.added = added;
272
+ const changed = {};
273
+ for (const widget of next.widgets) {
274
+ const original = baseById.get(widget.id);
275
+ if (!original)
276
+ continue;
277
+ const patch = {};
278
+ if (widget.span !== original.span)
279
+ patch.span = widget.span;
280
+ const versionChanged = (widget.version ?? 1) !== (original.version ?? 1);
281
+ if (!optionsEqual(widget.options, original.options) || versionChanged) {
282
+ patch.options = { ...widget.options };
283
+ if (widget.version !== undefined)
284
+ patch.version = widget.version;
285
+ }
286
+ if (Object.keys(patch).length)
287
+ changed[widget.id] = patch;
288
+ }
289
+ if (Object.keys(changed).length)
290
+ override.changed = changed;
291
+ const natural = applyOverviewOverride(base, override).widgets.map((w) => w.id);
292
+ const wanted = next.widgets.map((widget) => widget.id);
293
+ if (natural.some((id, index) => id !== wanted[index]))
294
+ override.order = wanted;
295
+ return isEmptyOverride(override) ? null : override;
296
+ }
297
+ /**
298
+ * The next unused widget id (`w1`, `w2`, ...) given every reserved id.
299
+ *
300
+ * Total for any reserved ids, including hostile stored ones such as
301
+ * `w9007199254740992`: only numeric suffixes that are safe integers below
302
+ * `Number.MAX_SAFE_INTEGER` raise the starting point (beyond it `n + 1` stops
303
+ * changing the number, which would loop forever), and when the high end is
304
+ * exhausted the scan restarts at `w1`. The reserved set is finite, so a free
305
+ * id is found within `size + 1` steps of the restart.
306
+ */
307
+ export function nextWidgetId(reserved) {
308
+ const taken = new Set(reserved);
309
+ let n = 1;
310
+ for (const id of taken) {
311
+ const match = /^w(\d+)$/.exec(id);
312
+ if (!match)
313
+ continue;
314
+ const value = Number(match[1]);
315
+ if (Number.isSafeInteger(value) && value < Number.MAX_SAFE_INTEGER) {
316
+ n = Math.max(n, value + 1);
317
+ }
318
+ }
319
+ while (n < Number.MAX_SAFE_INTEGER && taken.has(`w${n}`))
320
+ n += 1;
321
+ if (!taken.has(`w${n}`))
322
+ return `w${n}`;
323
+ // The high end is exhausted: take the first free id counting up from 1.
324
+ for (n = 1; taken.has(`w${n}`); n += 1)
325
+ ;
326
+ return `w${n}`;
327
+ }
328
+ /**
329
+ * Sanitize a document against the registry and the page's confined set:
330
+ * drops (and reports) unknown types, types the page does not allow or the
331
+ * widget refuses (`allowedIn`), future versions, failed migrations, invalid
332
+ * options, duplicate ids and widgets over the cap; migrates older option
333
+ * versions; clamps spans to the widget's range. Idempotent. Nothing here
334
+ * calls a widget's `load`.
335
+ */
336
+ export function sanitizeOverview(document, { registry, definition }) {
337
+ const issues = [];
338
+ const widgets = [];
339
+ const seen = new Set();
340
+ const cap = definition.maxWidgets ?? OVERVIEW_DEFAULT_MAX_WIDGETS;
341
+ const drop = (widget, code, message, extra = {}) => {
342
+ issues.push({
343
+ widgetId: widget.id,
344
+ type: widget.type,
345
+ code,
346
+ message,
347
+ ...extra,
348
+ });
349
+ };
350
+ for (const widget of document.widgets) {
351
+ if (seen.has(widget.id)) {
352
+ drop(widget, 'duplicate_id', 'widget id appears more than once');
353
+ continue;
354
+ }
355
+ seen.add(widget.id);
356
+ const def = registry.get(widget.type);
357
+ if (!def) {
358
+ drop(widget, 'unknown_type', 'widget type is not registered');
359
+ continue;
360
+ }
361
+ if ((definition.allowed && !definition.allowed.includes(widget.type)) ||
362
+ (def.allowedIn && !def.allowedIn.includes(definition.id))) {
363
+ drop(widget, 'type_not_allowed', 'widget type is not allowed in this overview');
364
+ continue;
365
+ }
366
+ if (widgets.length >= cap) {
367
+ drop(widget, 'too_many', `an overview holds at most ${cap} widgets`);
368
+ continue;
369
+ }
370
+ const stored = widget.version ?? 1;
371
+ let raw = widget.options;
372
+ if (stored > def.version) {
373
+ drop(widget, 'future_version', 'options were written by a newer widget version');
374
+ continue;
375
+ }
376
+ if (stored < def.version) {
377
+ let migrated = null;
378
+ try {
379
+ migrated = def.migrate
380
+ ? def.migrate({ ...widget.options }, stored)
381
+ : null;
382
+ }
383
+ catch {
384
+ migrated = null;
385
+ }
386
+ if (!migrated || !isRecord(migrated)) {
387
+ drop(widget, 'migration_failed', `no migration from version ${stored}`);
388
+ continue;
389
+ }
390
+ raw = migrated;
391
+ }
392
+ const checked = validateWidgetOptions(def.options, raw, {
393
+ models: definition.models,
394
+ });
395
+ if (!checked.ok) {
396
+ drop(widget, 'invalid_options', 'options do not match the widget schema', {
397
+ options: checked.issues,
398
+ });
399
+ continue;
400
+ }
401
+ widgets.push({
402
+ id: widget.id,
403
+ type: widget.type,
404
+ span: Math.min(def.maxSpan, Math.max(def.minSpan, widget.span)),
405
+ options: checked.options,
406
+ version: def.version,
407
+ });
408
+ }
409
+ return { document: { widgets }, issues };
410
+ }
411
+ /**
412
+ * Resolve an overview: sanitize the page defaults, parse the stored override
413
+ * (untrusted JSON), merge, and sanitize the result. This is the one call a
414
+ * page's server load makes before loading widget data.
415
+ */
416
+ export function resolveOverview(definition, override, registry) {
417
+ const defaults = sanitizeOverview({ widgets: [...definition.defaults] }, {
418
+ registry,
419
+ definition,
420
+ });
421
+ const parsed = parseOverviewOverride(override);
422
+ const merged = applyOverviewOverride(defaults.document, parsed.override);
423
+ const final = sanitizeOverview(merged, { registry, definition });
424
+ return {
425
+ base: defaults.document,
426
+ document: final.document,
427
+ override: parsed.override,
428
+ issues: [...defaults.issues, ...parsed.issues, ...final.issues],
429
+ overrideIssues: [...parsed.issues, ...final.issues],
430
+ };
431
+ }
432
+ /**
433
+ * Validate an override a client wants to save. Strict where load is lenient:
434
+ * any issue rejects the save. On success the returned override is canonical
435
+ * (re-derived from the sanitized result), so persist that, not the input.
436
+ */
437
+ export function checkOverviewOverride(definition, input, registry) {
438
+ const resolved = resolveOverview(definition, input, registry);
439
+ if (resolved.overrideIssues.length > 0) {
440
+ return { ok: false, issues: resolved.overrideIssues };
441
+ }
442
+ return { ok: true, override: diffOverview(resolved.base, resolved.document) };
443
+ }
444
+ /** Validate and freeze a page's overview declaration. */
445
+ export function defineOverview(definition) {
446
+ if (!OVERVIEW_PAGE_ID_PATTERN.test(definition.id)) {
447
+ throw new Error(`defineOverview: invalid overview id "${definition.id}"`);
448
+ }
449
+ const ids = new Set();
450
+ for (const widget of definition.defaults) {
451
+ if (!OVERVIEW_ID_PATTERN.test(widget.id) || ids.has(widget.id)) {
452
+ throw new Error(`defineOverview: invalid or duplicate widget id "${widget.id}"`);
453
+ }
454
+ ids.add(widget.id);
455
+ }
456
+ return Object.freeze({
457
+ ...definition,
458
+ defaults: Object.freeze([...definition.defaults]),
459
+ });
460
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Recipe widgets (#3727, phase 2): turn the `widget` surfaces of manifest
3
+ * recipes into widget-registry registrations.
4
+ *
5
+ * The manifest names a widget's component, server loader and migration as
6
+ * `'<module specifier>#<ExportName>'` references and never imports them. This
7
+ * helper receives the module resolver from the host, so nothing is imported at
8
+ * scan time or when the manifest is read; the host decides how a specifier
9
+ * becomes a module (a static map of dynamic imports in a bundled app).
10
+ *
11
+ * Svelte-free apart from type imports, so a server load can call it.
12
+ */
13
+ import type { RecipeDefinition, RecipeExportRef } from '@happyvertical/smrt-types';
14
+ import type { WidgetRegistry } from './registry.js';
15
+ /**
16
+ * Resolves one export of a module: `resolveExport('@acme/shop/svelte',
17
+ * 'SalesTotal')`. The host owns this (import map, dynamic import, or a test
18
+ * stub); it may reject or return `undefined` for something it does not know.
19
+ */
20
+ export type RecipeExportResolver = (specifier: string, exportName: string) => unknown | Promise<unknown>;
21
+ /** The part of a recipe this helper reads; a manifest `RecipeDefinition` fits. */
22
+ export type RecipeWidgetSource = Pick<RecipeDefinition, 'id'> & Partial<Pick<RecipeDefinition, 'surfaces'>>;
23
+ export interface RegisterRecipeWidgetsOptions {
24
+ /** Replace a type that is already registered instead of skipping it. */
25
+ replace?: boolean;
26
+ /**
27
+ * Whether a recipe is on in this app. Widgets of a recipe that is off are
28
+ * not registered, so they never appear in an add list. Default: all on.
29
+ */
30
+ enabled?: (recipeId: string) => boolean;
31
+ }
32
+ export type RecipeWidgetSkipReason =
33
+ /** The type is already registered (a core widget or another recipe). */
34
+ 'duplicate_type'
35
+ /** The surface does not form a valid widget definition. */
36
+ | 'invalid'
37
+ /** A module export the registration needs eagerly could not be resolved. */
38
+ | 'unresolved';
39
+ /** A widget surface that was not registered, and why. */
40
+ export interface RecipeWidgetSkip {
41
+ recipeId: string;
42
+ type: string;
43
+ reason: RecipeWidgetSkipReason;
44
+ /** Developer-facing detail; not localized. */
45
+ message: string;
46
+ }
47
+ export interface RecipeWidgetRegistration {
48
+ /** Registered widget types, in registration order. */
49
+ registered: string[];
50
+ skipped: RecipeWidgetSkip[];
51
+ /** Removes exactly the registrations this call made. */
52
+ dispose: () => void;
53
+ }
54
+ /** Split `'<specifier>#<Name>'`; refuses relative and absolute specifiers. */
55
+ export declare function parseRecipeExportRef(ref: RecipeExportRef | string): {
56
+ specifier: string;
57
+ exportName: string;
58
+ } | null;
59
+ /**
60
+ * Register every `widget` surface of `recipes` on `registry`.
61
+ *
62
+ * - Components resolve lazily (`loadComponent`); a page resolves them before
63
+ * render with `resolveWidgetComponents`, and one that cannot resolve renders
64
+ * as unavailable.
65
+ * - The server loader resolves on the first `load`; a failure becomes that
66
+ * widget's error tile.
67
+ * - `migrate` is synchronous in the registry, so its export is resolved here,
68
+ * before registering; if that fails the widget is skipped.
69
+ * - Nothing here validates option values: `registry.register` validates the
70
+ * schema and spans, and a stored overview is validated against the result
71
+ * as for any widget.
72
+ *
73
+ * Never throws for a bad surface: it is reported in `skipped` so one broken
74
+ * package cannot take down the overview.
75
+ */
76
+ export declare function registerRecipeWidgets(registry: WidgetRegistry, recipes: Iterable<RecipeWidgetSource>, resolveExport: RecipeExportResolver, options?: RegisterRecipeWidgetsOptions): Promise<RecipeWidgetRegistration>;
77
+ //# sourceMappingURL=recipe-widgets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recipe-widgets.d.ts","sourceRoot":"","sources":["../../../src/components/overview/recipe-widgets.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EACV,gBAAgB,EAChB,eAAe,EAEhB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAWpD;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAAG,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,KACf,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC,kFAAkF;AAClF,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC,GAC3D,OAAO,CAAC,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,CAAC,CAAC;AAE9C,MAAM,WAAW,4BAA4B;IAC3C,wEAAwE;IACxE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;CACzC;AAED,MAAM,MAAM,sBAAsB;AAChC,wEAAwE;AACtE,gBAAgB;AAClB,2DAA2D;GACzD,SAAS;AACX,4EAA4E;GAC1E,YAAY,CAAC;AAEjB,yDAAyD;AACzD,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,sBAAsB,CAAC;IAC/B,8CAA8C;IAC9C,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IACvC,sDAAsD;IACtD,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,wDAAwD;IACxD,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,GAAG,MAAM,GAC5B;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAUlD;AAqFD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,qBAAqB,CACzC,QAAQ,EAAE,cAAc,EACxB,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACrC,aAAa,EAAE,oBAAoB,EACnC,OAAO,GAAE,4BAAiC,GACzC,OAAO,CAAC,wBAAwB,CAAC,CAyDnC"}
@@ -0,0 +1,151 @@
1
+ /** Split `'<specifier>#<Name>'`; refuses relative and absolute specifiers. */
2
+ export function parseRecipeExportRef(ref) {
3
+ const at = ref.lastIndexOf('#');
4
+ if (at <= 0 || at === ref.length - 1)
5
+ return null;
6
+ const specifier = ref.slice(0, at);
7
+ const exportName = ref.slice(at + 1);
8
+ if (specifier.startsWith('.') || specifier.startsWith('/'))
9
+ return null;
10
+ if (/\s/.test(specifier) || !/^[A-Za-z_$][\w$]*$/.test(exportName)) {
11
+ return null;
12
+ }
13
+ return { specifier, exportName };
14
+ }
15
+ function resolveRef(ref, resolveExport) {
16
+ const parsed = parseRecipeExportRef(ref);
17
+ if (!parsed)
18
+ return Promise.reject(new Error(`invalid export ref "${ref}"`));
19
+ return Promise.resolve(resolveExport(parsed.specifier, parsed.exportName)).then((value) => {
20
+ if (value === undefined || value === null) {
21
+ throw new Error(`export "${ref}" was not found`);
22
+ }
23
+ return value;
24
+ });
25
+ }
26
+ /** Resolve once and share the result (and a failure) between callers. */
27
+ function once(make) {
28
+ let pending;
29
+ return () => {
30
+ pending ??= make();
31
+ return pending;
32
+ };
33
+ }
34
+ function isModuleWithDefault(value) {
35
+ return (typeof value === 'object' &&
36
+ value !== null &&
37
+ 'default' in value &&
38
+ !!value.default);
39
+ }
40
+ function toDefinition(surface, resolveExport, migrate) {
41
+ const component = once(() => resolveRef(surface.export, resolveExport));
42
+ const loadFn = surface.data?.load
43
+ ? once(() => resolveRef(surface.data?.load, resolveExport))
44
+ : undefined;
45
+ const def = {
46
+ type: surface.type,
47
+ title: surface.label,
48
+ options: surface.options ?? [],
49
+ loadComponent: async () => {
50
+ const value = await component();
51
+ return (isModuleWithDefault(value) ? value.default : value);
52
+ },
53
+ };
54
+ if (surface.description !== undefined)
55
+ def.description = surface.description;
56
+ if (surface.icon !== undefined)
57
+ def.icon = surface.icon;
58
+ if (surface.version !== undefined)
59
+ def.version = surface.version;
60
+ if (surface.allowedIn)
61
+ def.allowedIn = surface.allowedIn;
62
+ if (surface.defaultSpan !== undefined)
63
+ def.defaultSpan = surface.defaultSpan;
64
+ if (surface.minSpan !== undefined)
65
+ def.minSpan = surface.minSpan;
66
+ if (surface.maxSpan !== undefined)
67
+ def.maxSpan = surface.maxSpan;
68
+ if (migrate)
69
+ def.migrate = migrate;
70
+ if (loadFn) {
71
+ const load = async (options, ctx) => {
72
+ const fn = await loadFn();
73
+ if (typeof fn !== 'function') {
74
+ throw new Error(`loader "${surface.data?.load}" is not a function`);
75
+ }
76
+ return fn(options, ctx);
77
+ };
78
+ def.load = load;
79
+ }
80
+ return def;
81
+ }
82
+ /**
83
+ * Register every `widget` surface of `recipes` on `registry`.
84
+ *
85
+ * - Components resolve lazily (`loadComponent`); a page resolves them before
86
+ * render with `resolveWidgetComponents`, and one that cannot resolve renders
87
+ * as unavailable.
88
+ * - The server loader resolves on the first `load`; a failure becomes that
89
+ * widget's error tile.
90
+ * - `migrate` is synchronous in the registry, so its export is resolved here,
91
+ * before registering; if that fails the widget is skipped.
92
+ * - Nothing here validates option values: `registry.register` validates the
93
+ * schema and spans, and a stored overview is validated against the result
94
+ * as for any widget.
95
+ *
96
+ * Never throws for a bad surface: it is reported in `skipped` so one broken
97
+ * package cannot take down the overview.
98
+ */
99
+ export async function registerRecipeWidgets(registry, recipes, resolveExport, options = {}) {
100
+ const registered = [];
101
+ const skipped = [];
102
+ const disposers = [];
103
+ for (const recipe of recipes) {
104
+ if (options.enabled && !options.enabled(recipe.id))
105
+ continue;
106
+ for (const surface of recipe.surfaces ?? []) {
107
+ if (surface.kind !== 'widget')
108
+ continue;
109
+ const skip = (reason, message) => skipped.push({
110
+ recipeId: recipe.id,
111
+ type: surface.type,
112
+ reason,
113
+ message,
114
+ });
115
+ if (registry.has(surface.type) && !options.replace) {
116
+ skip('duplicate_type', `"${surface.type}" is already registered`);
117
+ continue;
118
+ }
119
+ let migrate;
120
+ if (surface.migrate) {
121
+ try {
122
+ const fn = await resolveRef(surface.migrate, resolveExport);
123
+ if (typeof fn !== 'function')
124
+ throw new Error('not a function');
125
+ migrate = fn;
126
+ }
127
+ catch (error) {
128
+ skip('unresolved', `migrate "${surface.migrate}": ${error instanceof Error ? error.message : String(error)}`);
129
+ continue;
130
+ }
131
+ }
132
+ try {
133
+ disposers.push(registry.register(toDefinition(surface, resolveExport, migrate), {
134
+ replace: options.replace,
135
+ }));
136
+ registered.push(surface.type);
137
+ }
138
+ catch (error) {
139
+ skip('invalid', error instanceof Error ? error.message : String(error));
140
+ }
141
+ }
142
+ }
143
+ return {
144
+ registered,
145
+ skipped,
146
+ dispose: () => {
147
+ for (const dispose of disposers.splice(0))
148
+ dispose();
149
+ },
150
+ };
151
+ }