@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.
- package/AGENTS.md +7 -44
- package/README.md +112 -1
- package/agents/browser-ai.md +47 -0
- package/agents/command-palette.md +85 -0
- package/agents/import-export.md +62 -0
- package/agents/overview-surfaces.md +255 -0
- package/agents/screens.md +57 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts +5 -0
- package/dist/browser-ai/adapters/tts/browser-synthesis.d.ts.map +1 -1
- package/dist/browser-ai/adapters/tts/browser-synthesis.js +83 -55
- package/dist/components/command-palette/CommandPalette.svelte +75 -0
- package/dist/components/command-palette/CommandPalette.svelte.d.ts +22 -0
- package/dist/components/command-palette/CommandPalette.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte +406 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts +18 -0
- package/dist/components/command-palette/CommandPaletteDialog.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte +91 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts +17 -0
- package/dist/components/command-palette/CommandPaletteTrigger.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/context.d.ts +18 -0
- package/dist/components/command-palette/context.d.ts.map +1 -0
- package/dist/components/command-palette/context.js +38 -0
- package/dist/components/command-palette/controller.svelte.d.ts +70 -0
- package/dist/components/command-palette/controller.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/controller.svelte.js +379 -0
- package/dist/components/command-palette/hotkeys.d.ts +30 -0
- package/dist/components/command-palette/hotkeys.d.ts.map +1 -0
- package/dist/components/command-palette/hotkeys.js +161 -0
- package/dist/components/command-palette/index.d.ts +19 -0
- package/dist/components/command-palette/index.d.ts.map +1 -0
- package/dist/components/command-palette/index.js +17 -0
- package/dist/components/command-palette/match.d.ts +38 -0
- package/dist/components/command-palette/match.d.ts.map +1 -0
- package/dist/components/command-palette/match.js +172 -0
- package/dist/components/command-palette/providers.d.ts +159 -0
- package/dist/components/command-palette/providers.d.ts.map +1 -0
- package/dist/components/command-palette/providers.js +345 -0
- package/dist/components/command-palette/rank.d.ts +20 -0
- package/dist/components/command-palette/rank.d.ts.map +1 -0
- package/dist/components/command-palette/rank.js +74 -0
- package/dist/components/command-palette/types.d.ts +119 -0
- package/dist/components/command-palette/types.d.ts.map +1 -0
- package/dist/components/command-palette/types.js +10 -0
- package/dist/components/command-palette/use-provider.svelte.d.ts +14 -0
- package/dist/components/command-palette/use-provider.svelte.d.ts.map +1 -0
- package/dist/components/command-palette/use-provider.svelte.js +23 -0
- package/dist/components/import-export/ImportExport.svelte +678 -0
- package/dist/components/import-export/ImportExport.svelte.d.ts +5 -0
- package/dist/components/import-export/ImportExport.svelte.d.ts.map +1 -0
- package/dist/components/import-export/collection-adapter.d.ts +35 -0
- package/dist/components/import-export/collection-adapter.d.ts.map +1 -0
- package/dist/components/import-export/collection-adapter.js +42 -0
- package/dist/components/import-export/csv.d.ts +60 -0
- package/dist/components/import-export/csv.d.ts.map +1 -0
- package/dist/components/import-export/csv.js +192 -0
- package/dist/components/import-export/download.d.ts +4 -0
- package/dist/components/import-export/download.d.ts.map +1 -0
- package/dist/components/import-export/download.js +17 -0
- package/dist/components/import-export/export.d.ts +25 -0
- package/dist/components/import-export/export.d.ts.map +1 -0
- package/dist/components/import-export/export.js +68 -0
- package/dist/components/import-export/fields.d.ts +48 -0
- package/dist/components/import-export/fields.d.ts.map +1 -0
- package/dist/components/import-export/fields.js +103 -0
- package/dist/components/import-export/index.d.ts +12 -0
- package/dist/components/import-export/index.d.ts.map +1 -0
- package/dist/components/import-export/index.js +9 -0
- package/dist/components/import-export/mapping.d.ts +12 -0
- package/dist/components/import-export/mapping.d.ts.map +1 -0
- package/dist/components/import-export/mapping.js +38 -0
- package/dist/components/import-export/props.d.ts +34 -0
- package/dist/components/import-export/props.d.ts.map +1 -0
- package/dist/components/import-export/props.js +1 -0
- package/dist/components/import-export/report.d.ts +13 -0
- package/dist/components/import-export/report.d.ts.map +1 -0
- package/dist/components/import-export/report.js +50 -0
- package/dist/components/import-export/runner.d.ts +17 -0
- package/dist/components/import-export/runner.d.ts.map +1 -0
- package/dist/components/import-export/runner.js +39 -0
- package/dist/components/import-export/types.d.ts +100 -0
- package/dist/components/import-export/types.d.ts.map +1 -0
- package/dist/components/import-export/types.js +6 -0
- package/dist/components/import-export/validate.d.ts +39 -0
- package/dist/components/import-export/validate.d.ts.map +1 -0
- package/dist/components/import-export/validate.js +268 -0
- package/dist/components/overview/OverviewGrid.svelte +579 -0
- package/dist/components/overview/OverviewGrid.svelte.d.ts +5 -0
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -0
- package/dist/components/overview/WidgetOptionsForm.svelte +193 -0
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts +5 -0
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -0
- package/dist/components/overview/controller.svelte.d.ts +92 -0
- package/dist/components/overview/controller.svelte.d.ts.map +1 -0
- package/dist/components/overview/controller.svelte.js +336 -0
- package/dist/components/overview/format.d.ts +13 -0
- package/dist/components/overview/format.d.ts.map +1 -0
- package/dist/components/overview/format.js +45 -0
- package/dist/components/overview/grid-types.d.ts +52 -0
- package/dist/components/overview/grid-types.d.ts.map +1 -0
- package/dist/components/overview/grid-types.js +1 -0
- package/dist/components/overview/index.d.ts +24 -0
- package/dist/components/overview/index.d.ts.map +1 -0
- package/dist/components/overview/index.js +22 -0
- package/dist/components/overview/load.d.ts +30 -0
- package/dist/components/overview/load.d.ts.map +1 -0
- package/dist/components/overview/load.js +92 -0
- package/dist/components/overview/markdown.d.ts +53 -0
- package/dist/components/overview/markdown.d.ts.map +1 -0
- package/dist/components/overview/markdown.js +203 -0
- package/dist/components/overview/model.d.ts +100 -0
- package/dist/components/overview/model.d.ts.map +1 -0
- package/dist/components/overview/model.js +460 -0
- package/dist/components/overview/recipe-widgets.d.ts +77 -0
- package/dist/components/overview/recipe-widgets.d.ts.map +1 -0
- package/dist/components/overview/recipe-widgets.js +151 -0
- package/dist/components/overview/registry.d.ts +45 -0
- package/dist/components/overview/registry.d.ts.map +1 -0
- package/dist/components/overview/registry.js +112 -0
- package/dist/components/overview/resize.d.ts +25 -0
- package/dist/components/overview/resize.d.ts.map +1 -0
- package/dist/components/overview/resize.js +41 -0
- package/dist/components/overview/schema.d.ts +34 -0
- package/dist/components/overview/schema.d.ts.map +1 -0
- package/dist/components/overview/schema.js +180 -0
- package/dist/components/overview/server.d.ts +14 -0
- package/dist/components/overview/server.d.ts.map +1 -0
- package/dist/components/overview/server.js +13 -0
- package/dist/components/overview/types.d.ts +235 -0
- package/dist/components/overview/types.d.ts.map +1 -0
- package/dist/components/overview/types.js +17 -0
- package/dist/components/overview/widgets/ChartWidget.svelte +111 -0
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/MetricWidget.svelte +86 -0
- package/dist/components/overview/widgets/MetricWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/MetricWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/NoteWidget.svelte +61 -0
- package/dist/components/overview/widgets/NoteWidget.svelte.d.ts +5 -0
- package/dist/components/overview/widgets/NoteWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +91 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte +80 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/ShortcutsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +44 -0
- package/dist/components/overview/widgets/core.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.js +222 -0
- package/dist/components/overview/widgets/data.d.ts +76 -0
- package/dist/components/overview/widgets/data.d.ts.map +1 -0
- package/dist/components/overview/widgets/data.js +40 -0
- package/dist/components/screens/DetailScreen.svelte +220 -0
- package/dist/components/screens/DetailScreen.svelte.d.ts +5 -0
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/EditForm.svelte +313 -0
- package/dist/components/screens/EditForm.svelte.d.ts +5 -0
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -0
- package/dist/components/screens/ListScreen.svelte +191 -0
- package/dist/components/screens/ListScreen.svelte.d.ts +5 -0
- package/dist/components/screens/ListScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecipeScreens.svelte +237 -0
- package/dist/components/screens/RecipeScreens.svelte.d.ts +5 -0
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -0
- package/dist/components/screens/fields.d.ts +54 -0
- package/dist/components/screens/fields.d.ts.map +1 -0
- package/dist/components/screens/fields.js +229 -0
- package/dist/components/screens/index.d.ts +8 -0
- package/dist/components/screens/index.d.ts.map +1 -0
- package/dist/components/screens/index.js +6 -0
- package/dist/components/screens/types.d.ts +215 -0
- package/dist/components/screens/types.d.ts.map +1 -0
- package/dist/components/screens/types.js +1 -0
- package/dist/components/screens/values.d.ts +62 -0
- package/dist/components/screens/values.d.ts.map +1 -0
- package/dist/components/screens/values.js +289 -0
- package/dist/components/sortable/controller.svelte.d.ts +10 -0
- package/dist/components/sortable/controller.svelte.d.ts.map +1 -1
- package/dist/components/sortable/controller.svelte.js +29 -7
- package/dist/components/workspace/manifest-nav.d.ts +20 -0
- package/dist/components/workspace/manifest-nav.d.ts.map +1 -1
- package/dist/components/workspace/manifest-nav.js +42 -13
- package/dist/i18n/server.d.ts +6 -0
- package/dist/i18n/server.d.ts.map +1 -1
- package/dist/i18n/server.js +6 -0
- package/dist/i18n/strings.import-export.d.ts +88 -0
- package/dist/i18n/strings.import-export.d.ts.map +1 -0
- package/dist/i18n/strings.import-export.js +88 -0
- package/dist/i18n/strings.overview.d.ts +100 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -0
- package/dist/i18n/strings.overview.js +107 -0
- package/dist/i18n/strings.palette.d.ts +18 -0
- package/dist/i18n/strings.palette.d.ts.map +1 -0
- package/dist/i18n/strings.palette.js +18 -0
- package/dist/i18n/strings.screens.d.ts +31 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -0
- package/dist/i18n/strings.screens.js +31 -0
- 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;
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
})();
|
|
65
|
+
this.initialization = initialization;
|
|
66
|
+
try {
|
|
67
|
+
await initialization;
|
|
62
68
|
}
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
|
|
69
|
+
finally {
|
|
70
|
+
if (this.initialization === initialization)
|
|
71
|
+
this.initialization = null;
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
|
-
async loadVoices() {
|
|
69
|
-
|
|
74
|
+
async loadVoices(synthesis) {
|
|
75
|
+
this.voices = synthesis.getVoices();
|
|
76
|
+
if (this.voices.length > 0)
|
|
70
77
|
return;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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"}
|