@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,255 @@
1
+ # smrt-svelte/overview
2
+
3
+ Module semantics for `src/components/overview/` (`./overview` and
4
+ `./overview/server`, #3727). Package orientation and cross-module invariants are
5
+ in [../AGENTS.md](../AGENTS.md); the shell edit mode it joins is in
6
+ [workspace.md](workspace.md#editing-the-layout-in-place).
7
+
8
+ An overview is a **customizable surface**: a flow grid of registered widgets,
9
+ each with an options object. A page declares which widget types it allows and
10
+ its default arrangement; admins (or users, per role) add, reorder, resize,
11
+ configure and remove widgets inside that confined set, and reset returns to the
12
+ default. Phase 1 (this module) is the surface, the registry, the data model, the
13
+ server load contract and five core widgets; phase 2 adds package widgets from
14
+ recipes ("Recipe widgets"). Persistence and assistant operations are later
15
+ phases (see "Extension points").
16
+
17
+ ## Two entries
18
+
19
+ | Entry | Contents |
20
+ |---|---|
21
+ | `./overview/server` (`server.ts`) | Svelte-free: types, `validateWidgetOptions`, the model (`resolveOverview`, `checkOverviewOverride`, `applyOverviewOverride`, `diffOverview`, `sanitizeOverview`, ...), `WidgetRegistry` / `registerWidget`, `loadOverview`. Safe in a `+page.server.ts` or a save endpoint. |
22
+ | `./overview` (`index.ts`) | Everything above plus `OverviewGrid`, `createOverview` (controller), `WidgetOptionsForm`, the core widgets and `registerCoreWidgets`, `shortcutsFromNav`, `formatWidgetValue`, `parseMarkdown` / `safeHref`. |
23
+
24
+ ## Data model (`types.ts`, `model.ts`)
25
+
26
+ A saved overview is data, never code:
27
+ `{ widgets: [{ id, type, span, options, version? }] }`. `span` is 1 to 4 (a widget
28
+ type may narrow its own range); `options` is a flat object of JSON primitives;
29
+ `version` is the widget's option-schema version the options were written
30
+ against (absent means 1).
31
+
32
+ - **`OverviewDefinition`** (what a page declares): `id` (dotted ids allowed,
33
+ `events.home`), `defaults`, `allowed` (widget types; omit for every registered
34
+ type), `models` (confines `model` options, including a substituted default: a
35
+ default outside the list fails validation, so its loader never runs),
36
+ `maxWidgets` (default 24).
37
+ `defineOverview` validates ids.
38
+ - **`OverviewOverride`** is the sparse, versioned (`version: 1`) delta a host
39
+ stores: `order`, `removed`, `added`, `changed[id] = { span?, options?, version? }`.
40
+ `applyOverviewOverride(base, override)` is total: ids the base no longer has
41
+ are ignored, an added widget colliding with a base id is skipped, and widgets
42
+ the order does not list keep their slots (a default shipped later still
43
+ appears). `diffOverview(base, next)` is its inverse (round-trip tested), so
44
+ every client edit is "mutate the document, store the diff"; an edit that
45
+ returns to the defaults stores `null`.
46
+ - **Ids are stable.** `nextWidgetId` yields `w<n>` and reserves the ids of
47
+ removed defaults, so a new widget never takes a removed default's id (the
48
+ override would delete it). It is total for any stored ids: numeric suffixes
49
+ beyond `Number.MAX_SAFE_INTEGER` are ignored when picking the next number
50
+ (they would stop incrementing), and an exhausted high end restarts at `w1`.
51
+ - **Tiers.** `resolveOverview` merges one override. A host with tenant defaults
52
+ and user overrides merges the tenant tier into the definition's `defaults`
53
+ first (phase 3); the user override then applies to that.
54
+ - **Reset** is `controller.reset()` (store `null`) or `resetWidget(id)`.
55
+
56
+ ## Validation: what keeps a stored overview from running anything
57
+
58
+ Options are validated against the widget's versioned schema on **save**
59
+ (`checkOverviewOverride`, strict: any issue rejects, and the returned override is
60
+ re-derived from the sanitized result, so persist that) and on **load**
61
+ (`sanitizeOverview`, lenient: the bad widget is dropped and reported as an
62
+ `OverviewIssue`, the rest render). Nothing here calls a widget's `load` for a
63
+ widget that failed validation.
64
+
65
+ - `schema.ts` field types: `text`, `markdown`, `identifier`, `model`, `integer`,
66
+ `number`, `boolean`, `enum`. There is **no free-form query, filter expression,
67
+ URL or SQL field**. "Which data" is a `model` (a bare `Class`, `pkg:Class` or
68
+ `@scope/pkg:Class`, optionally confined to the page's `models`), an
69
+ `identifier` (a field or named-filter id, `[A-Za-z_][A-Za-z0-9_.-]{0,63}`) or a
70
+ closed `enum`. The loader maps those to queries it owns and authorizes.
71
+ - Strict: unknown keys, wrong types, out-of-range numbers, values outside a closed
72
+ set and missing required fields fail the whole options object. Missing optional
73
+ fields take their default; `null` counts as missing.
74
+ - `sanitizeOverview` also drops: unknown types, types outside the page's
75
+ `allowed` set or a widget's `allowedIn`, duplicate ids, widgets over the cap,
76
+ newer-than-known versions, and older versions without a working `migrate`.
77
+ Spans are clamped to the widget's range. It is idempotent.
78
+ - `migrate(options, fromVersion)` upgrades stored options to the current version
79
+ (return `null` to drop). It runs on untrusted input inside a try/catch, and its
80
+ result is validated like any other options.
81
+ - A widget id removed by the user and a type removed by a release both degrade to
82
+ "dropped and reported", never an error page.
83
+
84
+ ## Registry (`registry.ts`)
85
+
86
+ `registerWidget({ type, title, options, component | loadComponent, load, allowedIn,
87
+ version, migrate, defaultSpan, minSpan, maxSpan })` on the shared
88
+ `defaultWidgetRegistry`, or `createWidgetRegistry()` for an isolated one (tests,
89
+ several surfaces). Registration is code, at module scope; registering a type
90
+ twice throws unless `{ replace: true }`. `title`, `description` and option
91
+ `label` / `help` / `choices[].label` are text **or i18n keys** (the UI runs them
92
+ through `t`, which returns an unregistered string unchanged); the core widgets
93
+ use `ui.overview.*` keys from `i18n/strings.overview.ts` (registered in
94
+ `i18n/server.ts`).
95
+
96
+ `component` renders synchronously. `loadComponent` is the lazy form (dynamic import
97
+ by export ref): resolve it **before** render with `resolveWidgetComponents(types)`
98
+ in a universal load and pass the map to `OverviewGrid components`, so server and
99
+ client render the same tree. A type that cannot resolve renders as unavailable.
100
+
101
+ ## Server load contract (`load.ts`)
102
+
103
+ ```ts
104
+ // +page.server.ts
105
+ const resolved = resolveOverview(definition, await storedOverride(event), registry);
106
+ const overview = await loadOverview(resolved.document, definition, registry, {
107
+ locale, tenantId, db /* the request's permission-checked capabilities */,
108
+ });
109
+ return { overview }; // LoadedOverview: JSON, serializable
110
+ ```
111
+
112
+ - A widget's `load(options, ctx)` receives **validated options only** and a
113
+ `WidgetLoadContext` the host built from the request principal: `overviewId`, an
114
+ abort `signal`, `locale`, and any host capabilities (index keys). It must
115
+ authorize through `ctx` (the user's permissions and tenant filters), never
116
+ through option values. Reports-backed chart loaders call the runtime-report
117
+ compiler with this context; this package does not depend on it.
118
+ - Loads run in parallel with a per-widget timeout (default 8 s; the signal
119
+ aborts). A thrown error, a timeout or non-serializable / oversized data
120
+ (default 256 KB of JSON) becomes a per-widget error tile
121
+ (`error.code`: `load_failed | timeout | invalid_data`); the real error goes only
122
+ to `onError` for server logs, never to the client. One bad widget never fails
123
+ the page.
124
+ - `LoadedOverview` is what the page returns and `createOverview({ loaded })` seeds
125
+ the controller from, so the grid renders every widget at first paint with no
126
+ client-only fetching and **no hydration mismatch** (the grid has no
127
+ effect-dependent content; the only `$effect` requests data for widgets the
128
+ seeded entries do not cover, which never happens on first render).
129
+ `scripts/overview-ssr.test.mjs` renders the grid through Vite SSR.
130
+ - Formatting is deterministic per locale (`Intl` with the i18n store's locale), and
131
+ no widget formats dates or reads the clock; loaders send preformatted meta.
132
+
133
+ ## Controller (`controller.svelte.ts`)
134
+
135
+ `createOverview({ definition, registry?, override?, onchange?, canCustomize?,
136
+ loaded?, loadWidget? })`, the `ShellLayout` pattern: **persistence is host-owned**.
137
+ With an `override` getter the host is the source of truth and feeds each edit back
138
+ (`onchange(override | null)`); without it the value is kept in memory. Reactive
139
+ reads: `document`, `base`, `override`, `issues`, `customized`, `canCustomize`,
140
+ `addable`, `full`, `entry(widget)`. Operations, each returning
141
+ `{ ok: true, id } | { ok: false, reason, issues? }` and refused with
142
+ `not_allowed` when `canCustomize()` is false (the role gate; the server must check
143
+ the same on save): `add(type, options?)`, `remove`, `move(id, toIndex)` (index
144
+ after removal), `resize(id, span)`, `setOptions`, `resetWidget`, `reset`,
145
+ `restore(override)` (the Undo hook), `reload(id)`.
146
+
147
+ `loadWidget(widget, signal)` is how a client edit gets data for an added or
148
+ reconfigured widget: the host runs the widget's `load` where its data lives (a
149
+ remote function in production, the in-browser source in smrt-planner). The
150
+ controller drops results for stale option keys and aborts superseded loads.
151
+ Call `destroy()` when the owner unmounts.
152
+
153
+ ## Grid and edit mode (`OverviewGrid.svelte`)
154
+
155
+ Props in `grid-types.ts`. `editing` defaults to the shell's layout editor
156
+ (`tryUseShellLayout()?.editing`), so the same pencil edits the grid under an
157
+ `AppShell` with `layoutEditing`; pass `editing` to drive it without a shell. It is
158
+ off whenever `controller.canCustomize` is false.
159
+
160
+ - **Flow grid.** CSS grid of 4 columns, 2 below 40rem and 1 below 22rem of the
161
+ grid's own width (container queries); `data-span` / `--span` carry the span. Gaps
162
+ are tile padding, not grid gap, so a drag never crosses a dead zone.
163
+ - **Reorder** uses the headless `createSortable` engine in its `flow` mode (a
164
+ single wrapping list): the handle is the existing grip (Space picks up, any
165
+ arrow steps along reading order, mirrored in RTL, Enter drops, Escape cancels;
166
+ pointer and native drag use the horizontal midpoint for tiles sharing a row).
167
+ - **Resize** is an end-edge `role="slider"` (arrows, Home, End; pointer drag snaps
168
+ to a column boundary via the pure `spanFromPointer`, committed on release).
169
+ - **Chrome** is icon-only `ShellIconButton`s (configure, remove) plus the toolbar
170
+ (add, reset). Widgets render as on the site inside an `inert` wrapper. Add and
171
+ configure open `Modal`s; `WidgetOptionsForm` is generated from the option field
172
+ descriptors with smrt-ui `Form` / `FormGroup` / `Input` / `Select` / `Textarea` /
173
+ `Checkbox` (the descriptors mirror the generated screens' field shape) and shows
174
+ each schema issue beside its field. A widget with a required option and no
175
+ default opens the form before it is added.
176
+ - A polite live region announces moves, resizes, adds, removals and resets.
177
+ `OverviewGrid.test.ts` runs axe in view and edit mode.
178
+
179
+ ## Core widgets (`widgets/`)
180
+
181
+ `registerCoreWidgets(registry, loaders)` registers `note` always and each
182
+ data-backed widget only when its loader is given (structural injection: this
183
+ package depends on no data package). Widgets render only when the grid has
184
+ `ready` data (it draws loading and error tiles) and treat `data` as untrusted
185
+ shape. Data shapes are in `widgets/data.ts`.
186
+
187
+ | Type | Options | Data |
188
+ |---|---|---|
189
+ | `metric` | `title`, `model`*, `measure`, `field`, `filter` | `{ value, format?, currency?, label?, change?, href? }` (money = integer minor units) |
190
+ | `chart` | `title`, `model`*, `groupBy`, `measure`, `field`, `period`, `style` (bar/line), `filter` | `{ points: [{ label, value }], format?, currency?, href? }`, one measure of a runtime report |
191
+ | `records` | `title`, `model`*, `sort`, `direction`, `limit`, `filter` | `{ rows: [{ id, title, subtitle?, meta?, href? }], total?, href? }`; the loader chooses the title field and the visible records |
192
+ | `shortcuts` | `title`, `section` | `{ items: [{ id, label, href, icon?, description? }] }`; `shortcutsFromNav(sections, sectionId)` maps `resolveShellNavModel` / `useShellLayout().sections` (visible entries, user renames) |
193
+ | `note` | `title`, `body` (Markdown) | none |
194
+
195
+ `note` renders `parseMarkdown` output as elements (headings, paragraphs, lists,
196
+ quotes, fenced code, bold/italic/code/links). There is no `{@html}`; raw HTML is
197
+ literal text and `safeHref` limits links to http(s), mailto, same-site paths and
198
+ fragments. Record and shortcut hrefs pass `safeHref` too. Charts carry their
199
+ numbers as text (inline for bars, a visually hidden table for the line).
200
+
201
+ ## Recipe widgets (`recipe-widgets.ts`)
202
+
203
+ A package contributes a widget with a `widget` surface on a recipe
204
+ ([core recipes.md](../../core/agents/recipes.md#widget-surfaces-3727) has the
205
+ declaration and its scan-time rules). `registerRecipeWidgets(registry, recipes,
206
+ resolveExport, { replace?, enabled? })` (async, Svelte-free, on both entries)
207
+ turns the manifest's widget surfaces into `registry.register(...)` calls:
208
+
209
+ | Surface field | Registration |
210
+ |---|---|
211
+ | `type`, `label`, `description`, `icon`, `version` | `type`, `title`, `description`, `icon`, `version` |
212
+ | `options` | `options` (plain JSON; `registry.register` re-validates the schema) |
213
+ | `export` | `loadComponent` (lazy; the export may be a component or a `{ default }` module) |
214
+ | `data.load` | `load` (resolved on first use; a missing or non-function loader is that widget's error tile) |
215
+ | `migrate` | `migrate`, **resolved eagerly** because the registry's `migrate` is synchronous |
216
+ | `allowedIn`, `defaultSpan`, `minSpan`, `maxSpan` | same names |
217
+ | `data.models` | not registered: documentation of what the loader reads; confine with the page's `models` |
218
+
219
+ - **The module resolver is injected.** `resolveExport(specifier, exportName)` is
220
+ the host's: a static map of dynamic imports in a bundled app, a stub in tests.
221
+ Nothing is imported when the manifest is read, and the registration itself
222
+ imports nothing except `migrate` exports. Relative or absolute specifiers
223
+ are refused.
224
+ - **Never throws for a bad surface.** The result is `{ registered, skipped,
225
+ dispose }`; a skip carries `reason` `duplicate_type` (a core widget or another
226
+ recipe owns the type; `replace` overrides), `invalid` (the registry refused the
227
+ definition) or `unresolved` (a `migrate` export is missing). One broken package
228
+ cannot take down the overview.
229
+ - **`enabled(recipeId)`** keeps widgets of recipes that are off out of the add
230
+ list. Call the helper again (after `dispose()`) when the set changes.
231
+ - Widget types share one registry namespace with the core widgets
232
+ (`metric`, `chart`, `records`, `shortcuts`, `note`). Pages still confine what
233
+ can appear with `defineOverview({ allowed })`, and a widget with `allowedIn`
234
+ only validates inside those overview ids.
235
+ - `src/components/overview/__tests__/recipe-widgets.test.ts` covers lazy
236
+ resolution, the loader and migrate paths, allowed placements and the skip
237
+ reasons. The scanner's copy of the option vocabulary and patterns is checked
238
+ against `types.ts` / `schema.ts` by `packages/scanner/src/__tests__/recipe-widgets.test.ts`,
239
+ and a type-level assertion here keeps `RecipeWidgetOptionType` equal to
240
+ `WidgetOptionType`.
241
+
242
+ ## Extension points left for later phases
243
+
244
+ - **Phase 2, package widgets (#3727, done).** See "Recipe widgets" below.
245
+ - **Phase 3, production persistence.** Implement the `override` getter /
246
+ `onchange` pair against the `_smrt_` table (tenant tier merged into
247
+ `definition.defaults`, user tier as the override), validate saves with
248
+ `checkOverviewOverride` and persist its canonical `override`, run
249
+ `loadOverview` in the page load, and supply `loadWidget` as a remote function
250
+ that calls the same `load`. Role gating = `canCustomize` plus the same check in
251
+ the save endpoint. No migration or table lives in this package.
252
+ - **Phase 4, assistant.** The controller methods are the structured operations;
253
+ `restore(override)` plus the `override` getter give Undo (snapshot before,
254
+ restore after). `OverviewOpResult.issues` is the validation feedback to return.
255
+ - **Not built:** the activity widget (Track B, #3710) and agenda.
@@ -0,0 +1,57 @@
1
+ # smrt-svelte/screens
2
+
3
+ Module semantics for `src/components/screens/` (`./screens`). Package orientation, the cross-module
4
+ invariants, and the traps that apply before editing anything live in
5
+ [../AGENTS.md](../AGENTS.md) — read that first.
6
+
7
+ ## Manifest-derived record screens (`./screens`, #3718)
8
+
9
+ `RecipeScreens` is the default screen set for a package that ships models but no
10
+ consumer UI (tags, facts, secrets, profiles, reports, prompts). It composes
11
+ `ListScreen`, `DetailScreen` and `EditForm`, each of which can also be used on
12
+ its own. Every screen takes a generated web `definition`
13
+ (`ScreenCollectionDefinition`, structurally a `SmrtWebCollectionDefinition`) and
14
+ an optional resolved `policy` (`ScreenPolicy`, structurally smrt-fields'
15
+ `ResolvedObjectFieldPolicy`). The module imports neither package: smrt-svelte
16
+ stays below the domain layer, and the app passes both in as props.
17
+
18
+ - **Field selection is `fields.ts`, pure and node-testable.** `deriveScreenFields`
19
+ is the single rule for list / view / edit. A field renders only when it is in
20
+ BOTH the definition and the policy, so sensitive, transient and
21
+ read-permission-gated fields (which the policy resolver omits) can never appear
22
+ on a generated screen. Hidden fields are dropped; system fields (`id`,
23
+ `tenantId`, `createdBy`, `updatedBy`, `createdAt`, `updatedAt`) are never
24
+ editable, and the two timestamps appear on a view as read-only advanced meta.
25
+ Do not add a second selection rule in a component.
26
+ - **Without a policy the code seed applies**: `ui.basic` / `ui.group` / `ui.order`
27
+ hints and the cold-start rule (no `basic` markers => everything basic; any
28
+ marker => unmarked fields advanced). Order is `order`, then declaration order.
29
+ - **Tiers**: list shows basic scalar columns only (no long text, JSON or raw
30
+ reference columns; capped at `DEFAULT_MAX_LIST_COLUMNS`, an explicit `columns`
31
+ list is never capped); view and edit show advanced fields behind a disclosure
32
+ (the edit one opens itself when an advanced field has a problem). Policy
33
+ `label`, `help` and `required` win over the manifest; on create, a resolved
34
+ policy default (an explicit `null` included) beats the manifest default.
35
+ `locked` governs who may change the POLICY, never the record value, so it is
36
+ not applied here.
37
+ - **Values are `values.ts`**: money (`ui.widget: 'currency'` on an INTEGER field
38
+ only) is integer minor units end to end. Editing shows major units but converts
39
+ with exact string math (`majorStringToMinorUnits`), never `* 100`; integers are
40
+ rejected unless JavaScript-safe; datetimes go out as ISO strings (an unchanged datetime keeps the
41
+ loaded record's full precision via `parseDraft`'s `record` context; the control
42
+ only shows minutes); JSON as parsed values. Empty optional text becomes `null` when the field is nullable and `''`
43
+ otherwise (unset text persists as `''`); every other empty optional field is
44
+ `null`. Errors are codes, mapped to messages by the component via `useI18n`.
45
+ - **Transport-neutral.** `RecipeScreens` drives a `RecipeScreensSource`
46
+ (`list` / `get` / `create` / `update` / `delete`); an operation the source lacks
47
+ (or `can` turns off) is simply not offered, so a read-only source is a
48
+ read-only screen set. `view` and `recordId` are bindable and `onnavigate`
49
+ fires on every user navigation, so an app can mirror them into its router.
50
+ `EditForm.onsubmit` throws to report a failed save or returns `{ fieldErrors }`.
51
+ Remount `EditForm` with `{#key}` when switching records (`RecipeScreens` does).
52
+ - **Not the policy-editing surface.** The policy gear, field-usage telemetry and
53
+ the per-field renderer registry live in `ObjectForm`
54
+ (`@happyvertical/smrt-fields/svelte`); `EditForm` is the dependency-light
55
+ default and does not replace it. Not wired into recipe surfaces yet (#3708).
56
+ - Component props live in `types.ts`, not in the `.svelte` files, so the barrel
57
+ never re-exports types from a component.
@@ -16,6 +16,11 @@ export declare class BrowserSynthesisTTSAdapter implements TTSAdapter {
16
16
  private options;
17
17
  private voices;
18
18
  private currentUtterance;
19
+ private operationGeneration;
20
+ private initializationGeneration;
21
+ private initialization;
22
+ private cancelVoiceLoad;
23
+ private settleCurrentSpeech;
19
24
  private startListeners;
20
25
  private endListeners;
21
26
  private errorListeners;
@@ -1 +1 @@
1
- {"version":3,"file":"browser-synthesis.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/tts/browser-synthesis.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,0BAA0B,EAC1B,UAAU,EACV,eAAe,EACf,UAAU,EACV,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,qBAAa,0BAA2B,YAAW,UAAU;IAC3D,QAAQ,CAAC,IAAI,EAAG,mBAAmB,CAAU;IAE7C,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAA8B;IAE5C,OAAO,CAAC,gBAAgB,CAAyC;IAGjE,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,iBAAiB,CAErB;gBAEQ,OAAO,GAAE,0BAA+B;IAUpD,IAAI,SAAS,IAAI,SAAS,CAEzB;IAEK,iBAAiB,CAAC,WAAW,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;YAqClD,UAAU;IA+BxB,eAAe,IAAI,eAAe;IAUlC,SAAS,IAAI,QAAQ,EAAE;IAUvB,OAAO,CAAC,SAAS;IAkCX,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAiElE,IAAI,IAAI,IAAI;IAOZ,KAAK,IAAI,IAAI;IAMb,MAAM,IAAI,IAAI;IAMd,UAAU,IAAI,OAAO;IAIrB,QAAQ,IAAI,OAAO;IAInB,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKzC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKvC,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKrD,UAAU,CACR,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,GACxD,MAAM,IAAI;IAKP,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAU/B"}
1
+ {"version":3,"file":"browser-synthesis.d.ts","sourceRoot":"","sources":["../../../../src/browser-ai/adapters/tts/browser-synthesis.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,KAAK,EACV,0BAA0B,EAC1B,UAAU,EACV,eAAe,EACf,UAAU,EACV,QAAQ,EACT,MAAM,YAAY,CAAC;AAEpB;;GAEG;AACH,qBAAa,0BAA2B,YAAW,UAAU;IAC3D,QAAQ,CAAC,IAAI,EAAG,mBAAmB,CAAU;IAE7C,OAAO,CAAC,UAAU,CAA8B;IAChD,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,gBAAgB,CAAyC;IACjE,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,wBAAwB,CAAK;IACrC,OAAO,CAAC,cAAc,CAA8B;IACpD,OAAO,CAAC,eAAe,CAA6B;IACpD,OAAO,CAAC,mBAAmB,CAA6B;IAGxD,OAAO,CAAC,cAAc,CAAyB;IAC/C,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,cAAc,CAAqC;IAC3D,OAAO,CAAC,iBAAiB,CAErB;gBAEQ,OAAO,GAAE,0BAA+B;IAUpD,IAAI,SAAS,IAAI,SAAS,CAEzB;IAEK,iBAAiB,CAAC,WAAW,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;YAmClD,UAAU;IAwBxB,eAAe,IAAI,eAAe;IAUlC,SAAS,IAAI,QAAQ,EAAE;IAUvB,OAAO,CAAC,SAAS;IAkCX,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,IAAI,CAAC;IAgFlE,IAAI,IAAI,IAAI;IAOZ,KAAK,IAAI,IAAI;IAMb,MAAM,IAAI,IAAI;IAMd,UAAU,IAAI,OAAO;IAIrB,QAAQ,IAAI,OAAO;IAInB,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKzC,KAAK,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAKvC,OAAO,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,MAAM,IAAI;IAKrD,UAAU,CACR,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,GACxD,MAAM,IAAI;IAKP,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAa/B"}
@@ -14,8 +14,12 @@ export class BrowserSynthesisTTSAdapter {
14
14
  synthesis = null;
15
15
  options;
16
16
  voices = [];
17
- // biome-ignore lint/correctness/noUnusedPrivateClassMembers: Used in speak/stop/pause
18
17
  currentUtterance = null;
18
+ operationGeneration = 0;
19
+ initializationGeneration = 0;
20
+ initialization = null;
21
+ cancelVoiceLoad = null;
22
+ settleCurrentSpeech = null;
19
23
  // Event listeners
20
24
  startListeners = new Set();
21
25
  endListeners = new Set();
@@ -36,58 +40,60 @@ export class BrowserSynthesisTTSAdapter {
36
40
  async ensureInitialized(_onProgress) {
37
41
  if (this._initState === 'ready')
38
42
  return;
39
- if (this._initState === 'initializing') {
40
- // Wait for existing initialization
41
- return new Promise((resolve, reject) => {
42
- const check = () => {
43
- if (this._initState === 'ready')
44
- resolve();
45
- else if (this._initState === 'error')
46
- reject(new Error('Initialization failed'));
47
- else
48
- setTimeout(check, 50);
49
- };
50
- check();
51
- });
52
- }
43
+ if (this.initialization)
44
+ return this.initialization;
45
+ const generation = this.initializationGeneration;
53
46
  this._initState = 'initializing';
54
- try {
55
- if (typeof window === 'undefined' || !window.speechSynthesis) {
56
- throw new CapabilityNotAvailableError('Web Speech Synthesis API', 'browser-synthesis');
47
+ const initialization = (async () => {
48
+ try {
49
+ if (typeof window === 'undefined' || !window.speechSynthesis) {
50
+ throw new CapabilityNotAvailableError('Web Speech Synthesis API', 'browser-synthesis');
51
+ }
52
+ this.synthesis = window.speechSynthesis;
53
+ await this.loadVoices(this.synthesis);
54
+ if (generation === this.initializationGeneration) {
55
+ this._initState = 'ready';
56
+ }
57
+ }
58
+ catch (error) {
59
+ if (generation === this.initializationGeneration) {
60
+ this._initState = 'error';
61
+ }
62
+ throw error;
57
63
  }
58
- this.synthesis = window.speechSynthesis;
59
- // Load voices - they may load asynchronously
60
- await this.loadVoices();
61
- this._initState = 'ready';
64
+ })();
65
+ this.initialization = initialization;
66
+ try {
67
+ await initialization;
62
68
  }
63
- catch (error) {
64
- this._initState = 'error';
65
- throw error;
69
+ finally {
70
+ if (this.initialization === initialization)
71
+ this.initialization = null;
66
72
  }
67
73
  }
68
- async loadVoices() {
69
- if (!this.synthesis)
74
+ async loadVoices(synthesis) {
75
+ this.voices = synthesis.getVoices();
76
+ if (this.voices.length > 0)
70
77
  return;
71
- // Get voices - may be empty initially in some browsers
72
- this.voices = this.synthesis.getVoices();
73
- if (this.voices.length === 0) {
74
- // Wait for voices to load
75
- await new Promise((resolve) => {
76
- const handleVoicesChanged = () => {
77
- this.voices = this.synthesis?.getVoices() ?? [];
78
- if (this.voices.length > 0) {
79
- this.synthesis?.removeEventListener('voiceschanged', handleVoicesChanged);
80
- resolve();
81
- }
82
- };
83
- this.synthesis?.addEventListener('voiceschanged', handleVoicesChanged);
84
- // Timeout after 2 seconds - some browsers may not fire the event
85
- setTimeout(() => {
86
- this.voices = this.synthesis?.getVoices() ?? [];
87
- resolve();
88
- }, 2000);
89
- });
90
- }
78
+ await new Promise((resolve) => {
79
+ const finish = () => {
80
+ clearTimeout(timeout);
81
+ synthesis.removeEventListener('voiceschanged', handleVoicesChanged);
82
+ this.cancelVoiceLoad = null;
83
+ resolve();
84
+ };
85
+ const handleVoicesChanged = () => {
86
+ this.voices = synthesis.getVoices();
87
+ if (this.voices.length > 0)
88
+ finish();
89
+ };
90
+ const timeout = setTimeout(() => {
91
+ this.voices = synthesis.getVoices();
92
+ finish();
93
+ }, 2000);
94
+ this.cancelVoiceLoad = finish;
95
+ synthesis.addEventListener('voiceschanged', handleVoicesChanged);
96
+ });
91
97
  }
92
98
  getCapabilities() {
93
99
  return {
@@ -133,12 +139,15 @@ export class BrowserSynthesisTTSAdapter {
133
139
  return this.voices.find((v) => v.default) || this.voices[0];
134
140
  }
135
141
  async speak(text, options = {}) {
142
+ // Invalidate previous requests before the asynchronous voices wait.
143
+ this.stop();
144
+ const generation = this.operationGeneration;
136
145
  await this.ensureInitialized();
146
+ if (generation !== this.operationGeneration)
147
+ return;
137
148
  if (!this.synthesis) {
138
149
  throw new Error('Synthesis not initialized');
139
150
  }
140
- // Cancel any current speech
141
- this.stop();
142
151
  return new Promise((resolve, reject) => {
143
152
  const utterance = new SpeechSynthesisUtterance(text);
144
153
  // Set voice
@@ -154,21 +163,34 @@ export class BrowserSynthesisTTSAdapter {
154
163
  utterance.rate = options.rate ?? this.options.defaultRate ?? 1;
155
164
  utterance.pitch = options.pitch ?? this.options.defaultPitch ?? 1;
156
165
  utterance.volume = options.volume ?? this.options.defaultVolume ?? 1;
166
+ const isCurrent = () => generation === this.operationGeneration &&
167
+ this.currentUtterance === utterance;
168
+ const settle = () => {
169
+ this.currentUtterance = null;
170
+ this.settleCurrentSpeech = null;
171
+ resolve();
172
+ };
157
173
  // Set up event handlers
158
174
  utterance.onstart = () => {
175
+ if (!isCurrent())
176
+ return;
159
177
  for (const cb of this.startListeners) {
160
178
  cb();
161
179
  }
162
180
  };
163
181
  utterance.onend = () => {
164
- this.currentUtterance = null;
182
+ if (!isCurrent())
183
+ return;
184
+ settle();
165
185
  for (const cb of this.endListeners) {
166
186
  cb();
167
187
  }
168
- resolve();
169
188
  };
170
189
  utterance.onerror = (event) => {
190
+ if (!isCurrent())
191
+ return;
171
192
  this.currentUtterance = null;
193
+ this.settleCurrentSpeech = null;
172
194
  const error = new Error(`Speech synthesis error: ${event.error}`);
173
195
  for (const cb of this.errorListeners) {
174
196
  cb(error);
@@ -176,19 +198,22 @@ export class BrowserSynthesisTTSAdapter {
176
198
  reject(error);
177
199
  };
178
200
  utterance.onboundary = (event) => {
201
+ if (!isCurrent())
202
+ return;
179
203
  for (const cb of this.boundaryListeners) {
180
204
  cb(event.charIndex, event.charLength || 1);
181
205
  }
182
206
  };
183
207
  this.currentUtterance = utterance;
208
+ this.settleCurrentSpeech = settle;
184
209
  this.synthesis?.speak(utterance);
185
210
  });
186
211
  }
187
212
  stop() {
188
- if (this.synthesis) {
189
- this.synthesis.cancel();
190
- this.currentUtterance = null;
191
- }
213
+ ++this.operationGeneration;
214
+ // Settle before native cancel: it may emit synchronous callbacks or none.
215
+ this.settleCurrentSpeech?.();
216
+ this.synthesis?.cancel();
192
217
  }
193
218
  pause() {
194
219
  if (this.synthesis) {
@@ -224,6 +249,9 @@ export class BrowserSynthesisTTSAdapter {
224
249
  }
225
250
  async dispose() {
226
251
  this.stop();
252
+ ++this.initializationGeneration;
253
+ this.cancelVoiceLoad?.();
254
+ this.initialization = null;
227
255
  this.synthesis = null;
228
256
  this.voices = [];
229
257
  this.startListeners.clear();
@@ -0,0 +1,75 @@
1
+ <script lang="ts">
2
+ /**
3
+ * CommandPalette - the trigger and the dialog together: the one component a
4
+ * host drops into a shell slot.
5
+ *
6
+ * ```svelte
7
+ * <AppShell slotItems={[{ id: 'item:command-palette', label: 'Search',
8
+ * slot: 'header.center', render: search }]} ... />
9
+ * ```
10
+ *
11
+ * The palette is the `palette` prop, else the nearest `setCommandPalette()`
12
+ * context, else one created here from `providers` and `navigate` (then only
13
+ * this component can reach it). Mount it once: it owns the global shortcut.
14
+ */
15
+ import { onDestroy, untrack } from 'svelte';
16
+ import CommandPaletteDialog from './CommandPaletteDialog.svelte';
17
+ import CommandPaletteTrigger from './CommandPaletteTrigger.svelte';
18
+ import { tryUseCommandPalette } from './context.js';
19
+ import {
20
+ CommandPaletteController,
21
+ type CommandPaletteOptions,
22
+ } from './controller.svelte.js';
23
+ import { DEFAULT_PALETTE_HOTKEYS } from './hotkeys.js';
24
+ import type { PaletteProvider } from './types.js';
25
+
26
+ interface Props {
27
+ /** The palette to use; default: context, else a new one. */
28
+ palette?: CommandPaletteController;
29
+ /** Providers for the palette created here (ignored when one is supplied). */
30
+ providers?: readonly PaletteProvider[];
31
+ /** Navigation seam for the palette created here. */
32
+ navigate?: CommandPaletteOptions['navigate'];
33
+ /** Shortcuts that toggle the palette; `false` for none. */
34
+ hotkeys?: readonly string[] | false;
35
+ /** Trigger text and dialog name (default "Search"). */
36
+ label?: string;
37
+ /** Show only the glyph in the trigger. */
38
+ compact?: boolean;
39
+ /** Render the dialog and shortcut without a trigger. */
40
+ hideTrigger?: boolean;
41
+ }
42
+
43
+ let {
44
+ palette: paletteProp,
45
+ providers = [],
46
+ navigate,
47
+ hotkeys = DEFAULT_PALETTE_HOTKEYS,
48
+ label,
49
+ compact = false,
50
+ hideTrigger = false,
51
+ }: Props = $props();
52
+
53
+ // The palette made here (when none is supplied) is built once from the initial
54
+ // `providers` and `navigate`; register later providers through the controller.
55
+ const fromContext = tryUseCommandPalette();
56
+ const owned: CommandPaletteController | null = untrack(() =>
57
+ paletteProp || fromContext
58
+ ? null
59
+ : new CommandPaletteController({ providers, navigate }),
60
+ );
61
+ onDestroy(() => owned?.destroy());
62
+ const palette = $derived(
63
+ paletteProp ?? fromContext ?? (owned as CommandPaletteController),
64
+ );
65
+ </script>
66
+
67
+ {#if !hideTrigger}
68
+ <CommandPaletteTrigger
69
+ {palette}
70
+ {label}
71
+ {compact}
72
+ hotkeys={hotkeys === false ? [] : hotkeys}
73
+ />
74
+ {/if}
75
+ <CommandPaletteDialog {palette} {hotkeys} {label} />
@@ -0,0 +1,22 @@
1
+ import { CommandPaletteController, type CommandPaletteOptions } from './controller.svelte.js';
2
+ import type { PaletteProvider } from './types.js';
3
+ interface Props {
4
+ /** The palette to use; default: context, else a new one. */
5
+ palette?: CommandPaletteController;
6
+ /** Providers for the palette created here (ignored when one is supplied). */
7
+ providers?: readonly PaletteProvider[];
8
+ /** Navigation seam for the palette created here. */
9
+ navigate?: CommandPaletteOptions['navigate'];
10
+ /** Shortcuts that toggle the palette; `false` for none. */
11
+ hotkeys?: readonly string[] | false;
12
+ /** Trigger text and dialog name (default "Search"). */
13
+ label?: string;
14
+ /** Show only the glyph in the trigger. */
15
+ compact?: boolean;
16
+ /** Render the dialog and shortcut without a trigger. */
17
+ hideTrigger?: boolean;
18
+ }
19
+ declare const CommandPalette: import("svelte").Component<Props, {}, "">;
20
+ type CommandPalette = ReturnType<typeof CommandPalette>;
21
+ export default CommandPalette;
22
+ //# sourceMappingURL=CommandPalette.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CommandPalette.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/command-palette/CommandPalette.svelte.ts"],"names":[],"mappings":"AAoBA,OAAO,EACL,wBAAwB,EACxB,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAGlD,UAAU,KAAK;IACb,4DAA4D;IAC5D,OAAO,CAAC,EAAE,wBAAwB,CAAC;IACnC,6EAA6E;IAC7E,SAAS,CAAC,EAAE,SAAS,eAAe,EAAE,CAAC;IACvC,oDAAoD;IACpD,QAAQ,CAAC,EAAE,qBAAqB,CAAC,UAAU,CAAC,CAAC;IAC7C,2DAA2D;IAC3D,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,KAAK,CAAC;IACpC,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0CAA0C;IAC1C,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AA2CD,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}