@happyvertical/smrt-svelte 0.55.10 → 0.55.11
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 +1 -0
- package/agents/cookbook-shell.md +176 -0
- package/agents/overview-presentation.md +8 -0
- package/agents/overview-surfaces.md +11 -0
- package/agents/screens.md +5 -0
- package/dist/components/cookbook/CookbookApp.svelte +236 -0
- package/dist/components/cookbook/CookbookApp.svelte.d.ts +5 -0
- package/dist/components/cookbook/CookbookApp.svelte.d.ts.map +1 -0
- package/dist/components/cookbook/CookbookSectionOverview.svelte +71 -0
- package/dist/components/cookbook/CookbookSectionOverview.svelte.d.ts +20 -0
- package/dist/components/cookbook/CookbookSectionOverview.svelte.d.ts.map +1 -0
- package/dist/components/cookbook/CookbookSectionPage.svelte +133 -0
- package/dist/components/cookbook/CookbookSectionPage.svelte.d.ts +5 -0
- package/dist/components/cookbook/CookbookSectionPage.svelte.d.ts.map +1 -0
- package/dist/components/cookbook/brand-theme.d.ts +11 -0
- package/dist/components/cookbook/brand-theme.d.ts.map +1 -0
- package/dist/components/cookbook/brand-theme.js +24 -0
- package/dist/components/cookbook/build.d.ts +8 -0
- package/dist/components/cookbook/build.d.ts.map +1 -0
- package/dist/components/cookbook/build.js +277 -0
- package/dist/components/cookbook/catalog.d.ts +37 -0
- package/dist/components/cookbook/catalog.d.ts.map +1 -0
- package/dist/components/cookbook/catalog.js +99 -0
- package/dist/components/cookbook/context.d.ts +23 -0
- package/dist/components/cookbook/context.d.ts.map +1 -0
- package/dist/components/cookbook/context.js +15 -0
- package/dist/components/cookbook/index.d.ts +22 -0
- package/dist/components/cookbook/index.d.ts.map +1 -0
- package/dist/components/cookbook/index.js +20 -0
- package/dist/components/cookbook/page.d.ts +23 -0
- package/dist/components/cookbook/page.d.ts.map +1 -0
- package/dist/components/cookbook/page.js +32 -0
- package/dist/components/cookbook/registry.d.ts +37 -0
- package/dist/components/cookbook/registry.d.ts.map +1 -0
- package/dist/components/cookbook/registry.js +38 -0
- package/dist/components/cookbook/sections.d.ts +60 -0
- package/dist/components/cookbook/sections.d.ts.map +1 -0
- package/dist/components/cookbook/sections.js +88 -0
- package/dist/components/cookbook/server.d.ts +61 -0
- package/dist/components/cookbook/server.d.ts.map +1 -0
- package/dist/components/cookbook/server.js +108 -0
- package/dist/components/cookbook/theme.d.ts +21 -0
- package/dist/components/cookbook/theme.d.ts.map +1 -0
- package/dist/components/cookbook/theme.js +53 -0
- package/dist/components/cookbook/types.d.ts +204 -0
- package/dist/components/cookbook/types.d.ts.map +1 -0
- package/dist/components/cookbook/types.js +1 -0
- package/dist/components/overview/OverviewGrid.svelte +5 -2
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
- package/dist/components/overview/grid-types.d.ts +6 -2
- package/dist/components/overview/grid-types.d.ts.map +1 -1
- package/dist/i18n/strings.overview.d.ts +3 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -1
- package/dist/i18n/strings.overview.js +4 -0
- package/package.json +25 -14
package/AGENTS.md
CHANGED
|
@@ -19,6 +19,7 @@ subpath you are editing. This file keeps what holds in every module.
|
|
|
19
19
|
| `src/components/app/` (`./app`) | `AppShell` (Provider + ThemeProvider + theme CSS + AdminShell + nav/dock slots), `OwnerSetupForm` (props-driven first-run owner form), `ShellSettingsPage`, `RuntimeDiagnosticsWebMcp`. Browser-only; no `$app/*` or server imports | [agents/workspace.md](agents/workspace.md#app-shell-and-owner-setup-app) |
|
|
20
20
|
| `src/components/workspace/` (`./workspace` + `./web`) | the AdminShell family and its principles, the legacy ToolsDock surface, the `./web` activity-feed and `updateAvailable` adapters, and server-side dock gates | [agents/workspace.md](agents/workspace.md) |
|
|
21
21
|
| `src/components/command-palette/` (`./command-palette`) | `CommandPalette` trigger + dialog, `PaletteProvider` contract, nav and manifest providers (#3713) | [agents/command-palette.md](agents/command-palette.md) |
|
|
22
|
+
| `src/components/cookbook/` (`./cookbook` + `./cookbook/server`) | cookbook-driven app shell: pure `buildCookbookShell` (cookbook + recipe catalog to `AppShell` sections, entries, layout, theme, section overviews, recipe surfaces), `CookbookApp` / `CookbookSectionPage`, and the server `loadCookbookApp` (file + installed manifests + validation); ported from smrt-planner (#3749) | [agents/cookbook-shell.md](agents/cookbook-shell.md) |
|
|
22
23
|
| `src/components/overview/` (`./overview` + `./overview/server`) | customizable overview surfaces: `OverviewGrid` flow grid (span 1-4, keyboard reorder/resize) joined to the shell's layout edit mode, host-owned `createOverview` controller, widget registry with versioned option schemas validated on save and load, the server `load(options, ctx)` contract (`loadOverview`), the metric/chart/records/shortcuts/note core widgets, and `registerRecipeWidgets` (recipe `widget` surfaces to registry registrations, injected module resolver) (#3727); production persistence is `@happyvertical/smrt-preferences` (kind `overview`) | [agents/overview-surfaces.md](agents/overview-surfaces.md) |
|
|
23
24
|
| `src/web/remote-query.svelte.ts` | Svelte 5 binding for query-shaped remote pages: rows, page, totals, loading/refreshing/stale/error, retry, last-updated, and query-scoped live subscriptions (#2445) | — |
|
|
24
25
|
| `src/web/list-data-surface.svelte.ts` (`mountListDataSurface`) | Registers an existing non-`DataTable` list (mirroring a `DataTableController`) as a mounted `DataSurfaceDescriptor`; a same-behavior port of `registerContentListDataSurface`, not yet unified behind smrt-ui (#2917). The module doc comment is the authoritative list of divergences (#2906) | — |
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# smrt-svelte/cookbook
|
|
2
|
+
|
|
3
|
+
Module semantics for `src/components/cookbook/` (`./cookbook` and
|
|
4
|
+
`./cookbook/server`, #3749). Package orientation and cross-module invariants are
|
|
5
|
+
in [../AGENTS.md](../AGENTS.md); the document it reads is the cookbook v1
|
|
6
|
+
contract ([core cookbook.md](../../core/agents/cookbook.md)).
|
|
7
|
+
|
|
8
|
+
A **cookbook-driven app shell**: given `smrt.cookbook.json` and the installed
|
|
9
|
+
packages' recipes, render the navigation sections, recipe entries, layout, theme
|
|
10
|
+
and section overview pages that smrt-planner showed, so a generated app and the
|
|
11
|
+
planner preview draw the same shell. Logic ported from smrt-planner
|
|
12
|
+
(`+layout.svelte`, `recipes/sections.ts`, `sections/*`, `overviews/*`,
|
|
13
|
+
`theme/*`); the planner-only editor, option/help pages, package browser and
|
|
14
|
+
sample data source are not part of it.
|
|
15
|
+
|
|
16
|
+
## Entries
|
|
17
|
+
|
|
18
|
+
| Entry | Contents |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `./cookbook/server` | Svelte-free and Node-safe. `loadCookbookApp` (file + manifests + validation), `readInstalledManifests`, `hasCookbook`, and the pure builder/catalog/theme functions and types. Use it in `+layout.server.ts`. |
|
|
21
|
+
| `./cookbook` | Everything above that is browser-safe, plus `CookbookApp`, `CookbookSectionPage`, `applyCookbookTheme`, `createCookbookRegistry`, `createSectionOverview`. |
|
|
22
|
+
|
|
23
|
+
## Pure core (no DOM, Node-testable)
|
|
24
|
+
|
|
25
|
+
- **`catalogFromManifests(manifests)`** turns package `manifest.json` objects
|
|
26
|
+
into a `CookbookCatalog` (`recipes` trimmed to what the shell reads, `models`
|
|
27
|
+
with package id, description and `exposed`). Recipe order is manifest order;
|
|
28
|
+
a repeated id keeps the first. `narrowCatalog(catalog, cookbook)` keeps only
|
|
29
|
+
the selected recipes and the models they or the cookbook's `features` name, so
|
|
30
|
+
a load serializes a few KB instead of the whole workspace.
|
|
31
|
+
- **`buildCookbookShell({ cookbook, catalog, ... })`** returns a `CookbookShell`:
|
|
32
|
+
`navGroups` for `AppShell` (`navMode: 'sections'`, one group per suggested
|
|
33
|
+
section, icon and section-page `href`), `layout` (the cookbook's, passed to
|
|
34
|
+
the shell, which applies order/hidden/labels itself), `sections` and `entries`
|
|
35
|
+
(every entry by stable id), `theme`, `overviews`, recipe surfaces
|
|
36
|
+
(`shellWidgets`, `routes`, `settingsPanels`, `widgets`), `policies`, and
|
|
37
|
+
`issues`. It never throws for cookbook content: an unknown recipe, a nav model
|
|
38
|
+
the catalog lacks or a feature that is not exposed becomes an `issues` entry
|
|
39
|
+
and is left out.
|
|
40
|
+
- **Ids are the contract.** Section layout ids are `section:<id>` (a recipe's
|
|
41
|
+
`section`, else its `group`, else the recipe) or `custom:<slug>` (the layout's
|
|
42
|
+
own); entries are `item:<package>:<Model>[:<key>]`. They never name labels or
|
|
43
|
+
hrefs, so renames and route changes keep a saved layout valid. Do not change
|
|
44
|
+
`navItemId`/`buildNavSections` without the planner: both read the same
|
|
45
|
+
cookbooks.
|
|
46
|
+
- **Section order** follows the first recipe of each section in **catalog
|
|
47
|
+
order** (not the cookbook's sorted list), then the cookbook layout's
|
|
48
|
+
`sectionOrder`. Pass `recipeOrder` to pin it (the planner's overlay order).
|
|
49
|
+
- **Entries**: label, icon (`fileText` when the recipe names none), description
|
|
50
|
+
(recipe text, else the model description, else a plain fallback), `noun` and
|
|
51
|
+
`filter` carried through. Feature models share the `More` section after the
|
|
52
|
+
recipes' sections.
|
|
53
|
+
- **Paths** are options: `entryPath` (default `/m/<package>/<Model>/[<key>/]`),
|
|
54
|
+
`sectionPath` (default `/s/<slug>/`, slug = layout id with the first `:`
|
|
55
|
+
turned into `-`), and `href` (a prefix or query helper applied to both).
|
|
56
|
+
- **Overviews**: one definition per suggested section and per layout custom
|
|
57
|
+
section (`defineOverview`), id = the section's layout id, so a cookbook's
|
|
58
|
+
`overviews` key survives a section rename. Defaults are a `shortcuts` widget;
|
|
59
|
+
with `dataWidgets: true` they add a `metric` count and `records` list of the
|
|
60
|
+
section's lead model (first nav model of its first recipe), which need the
|
|
61
|
+
host's loaders. The cookbook's stored override is carried untouched
|
|
62
|
+
(untrusted JSON, `null` when absent or empty) for `resolveOverview` /
|
|
63
|
+
`createOverview` to validate; an override for an unknown page, or a malformed
|
|
64
|
+
one, is an `issues` entry.
|
|
65
|
+
- **Theme**: `resolveCookbookTheme` is lenient (a bad colour or font is ignored;
|
|
66
|
+
`validateCookbook` is where it is rejected). A brand colour becomes the preset
|
|
67
|
+
id `cookbook-brand-<hex>[-<font>]`; the colour/font pair is encoded in the id
|
|
68
|
+
because `ThemeProvider` looks a registered theme up once per id.
|
|
69
|
+
- **Recipe surfaces**: `shell-widget` surfaces are returned with their slot (a
|
|
70
|
+
smrt-svelte `ShellSlot`; the equality with `RecipeShellSlot` is a typechecked
|
|
71
|
+
assertion), `route` and `settings-panel` as data, `widget` surfaces grouped by
|
|
72
|
+
recipe for `registerRecipeWidgets`. The builder never imports a component.
|
|
73
|
+
|
|
74
|
+
## `CookbookApp`
|
|
75
|
+
|
|
76
|
+
`AppShell` with the builder's output. Props: `cookbook`, `catalog`, any
|
|
77
|
+
`AppShell` prop (title, logo, `storageKey`, `user`, `permissions`, `dock`,
|
|
78
|
+
`slotItems`, `settingsHref`, ...), and:
|
|
79
|
+
|
|
80
|
+
- `loaders` (`metric` / `records` / `chart` data loaders; with `metric` and
|
|
81
|
+
`records` the section pages add the count and latest records),
|
|
82
|
+
- `widgetContext` (host capabilities for browser-side widget loads),
|
|
83
|
+
- `resolveExport` (a static map of dynamic imports; turns a recipe export
|
|
84
|
+
reference into a component, used for `shell-widget` slot items and recipe
|
|
85
|
+
`widget` registration; without it those surfaces are ignored),
|
|
86
|
+
- `oncookbookchange(cookbook)` called with the full cookbook after a layout or
|
|
87
|
+
overview edit (layout edits come from the shell's pencil toggle, enabled with
|
|
88
|
+
`layoutEditing`). Without it edits live in memory. Persisting them is the
|
|
89
|
+
host's job; feeding the result back as `cookbook` is safe.
|
|
90
|
+
|
|
91
|
+
Layout and overview edits are kept in local `$state.raw` copies that follow the
|
|
92
|
+
`cookbook` prop whenever it changes. The widget registry is rebuilt only when
|
|
93
|
+
the recipes' widget surfaces change by content, not on every edit.
|
|
94
|
+
|
|
95
|
+
Theme: `applyCookbookTheme` registers a brand colour with smrt-ui
|
|
96
|
+
(`createThemeFromColor` + `registerTheme`) and returns the preset for
|
|
97
|
+
`ThemeProvider`; an unknown preset name falls back to `smrt`.
|
|
98
|
+
|
|
99
|
+
**SSR**: the shell, nav, title and theme render on the server. The widget
|
|
100
|
+
registry, recipe widgets and slot items are resolved in effects (a bundled
|
|
101
|
+
`resolveExport` is async), so a section page's overview appears after hydration,
|
|
102
|
+
not in the server HTML.
|
|
103
|
+
|
|
104
|
+
## `CookbookSectionPage`
|
|
105
|
+
|
|
106
|
+
A section's own page: title, icon and description (the user's renames win over
|
|
107
|
+
the suggestion), the overview grid, and, in the shell's layout edit mode, the
|
|
108
|
+
section's entries as a sortable menu. A section without an overview (a custom id
|
|
109
|
+
outside the overview id pattern) shows its menu as cards. Mount it from one
|
|
110
|
+
route: `s/[slug]/+page.svelte` renders
|
|
111
|
+
`<CookbookSectionPage slug={page.params.slug} />`. It must be inside
|
|
112
|
+
`CookbookApp`.
|
|
113
|
+
|
|
114
|
+
## Using it in an app
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
// +layout.server.ts
|
|
118
|
+
import { loadCookbookApp } from '@happyvertical/smrt-svelte/cookbook/server';
|
|
119
|
+
export const load = async () => loadCookbookApp(); // { cookbook, catalog, warnings }
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```svelte
|
|
123
|
+
<!-- +layout.svelte -->
|
|
124
|
+
<script lang="ts">
|
|
125
|
+
import { CookbookApp } from '@happyvertical/smrt-svelte/cookbook';
|
|
126
|
+
let { data, children } = $props();
|
|
127
|
+
</script>
|
|
128
|
+
<CookbookApp cookbook={data.cookbook} catalog={data.catalog} layoutEditing>
|
|
129
|
+
{@render children()}
|
|
130
|
+
</CookbookApp>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`loadCookbookApp` reads `smrt.cookbook.json`, reads the `manifest.json` of the
|
|
134
|
+
project's dependencies that export one, validates with `validateCookbook` from
|
|
135
|
+
`@happyvertical/smrt-core/cookbook` (resolved at call time: the app depends on
|
|
136
|
+
core; smrt-svelte does not, `validate` can be injected), and throws one error
|
|
137
|
+
listing every problem, including a recipe no installed package declares.
|
|
138
|
+
|
|
139
|
+
## Not done here
|
|
140
|
+
|
|
141
|
+
- **Field policies** (`cookbook.policies`) are returned as `policies` but not
|
|
142
|
+
applied; applying them to the runtime is `smrt cookbook apply` and the
|
|
143
|
+
runtime's job. The shell does not render a settings page; pass `settingsHref`.
|
|
144
|
+
- **Entry pages** (`/m/<package>/<Model>/`) are the app's routes; `./screens`
|
|
145
|
+
(`RecipeScreens`) is the default content for them. Their data comes from the
|
|
146
|
+
package model's generated REST route and its `@smrt/web` definition (#3749).
|
|
147
|
+
The app does nothing beyond listing the package in `smrt.config.ts`
|
|
148
|
+
`consumer.packages`: the `smrt()` Vite preset hosts the models of the
|
|
149
|
+
cookbook's recipes and `features` (core
|
|
150
|
+
[build-knowledge.md](../../core/agents/build-knowledge.md), "Package models
|
|
151
|
+
over REST"), skipping `api: false` models and any the cookbook's `exposure`
|
|
152
|
+
turns off for `api`. In the entry route, take
|
|
153
|
+
`collectionDefinitions[<collection>]` from `@smrt/web` (not
|
|
154
|
+
`@happyvertical/smrt-virt-web`, which holds only the app's own models) as the
|
|
155
|
+
`definition`, and build the `RecipeScreensSource` over
|
|
156
|
+
`GET /api/<collection>` and `/api/<collection>/<id>` (offer writes only when
|
|
157
|
+
the definition's `actions` include them). A model with no definition is not
|
|
158
|
+
exposed over the API; show the notice.
|
|
159
|
+
- **Options/Help gear** on a recipe's first entry (the planner's `action`) is
|
|
160
|
+
planner-only and not emitted.
|
|
161
|
+
- **Legacy id migrations** (`migrateLegacySections`/`NavItemIds`) stay in the
|
|
162
|
+
planner; they depend on its recipe catalog.
|
|
163
|
+
|
|
164
|
+
## Tests
|
|
165
|
+
|
|
166
|
+
`src/components/cookbook/__tests__/`: `build.test.ts` builds the four fixture
|
|
167
|
+
cookbooks in `packages/core/src/cookbook/__fixtures__/` against the real
|
|
168
|
+
workspace recipes (every package that declares a `SmrtRecipe`, scanned and
|
|
169
|
+
converted to manifests the way the build does, in `workspace-catalog.ts`, so no
|
|
170
|
+
sibling `dist/` is needed), asserts no issues, that every layout id resolves,
|
|
171
|
+
and the exact bakery sections and labels. `contract-types.ts` (not a `*.test.ts`:
|
|
172
|
+
tsc excludes test files) holds the type assertions that smrt-types'
|
|
173
|
+
`CookbookLayout`/`CookbookOverviewOverride` equal `ShellLayout`/
|
|
174
|
+
`OverviewOverride`. `CookbookApp.test.ts` mounts the shell with the bakery
|
|
175
|
+
cookbook (nav, section page, brand theme, edit callback); `server.test.ts` runs
|
|
176
|
+
the loader over a temp project with a fake installed package.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# SMRT #3759 test design
|
|
2
|
+
| Behavior | Trigger | Positive | Negative/default | Actor/context | Executor | Runtime | Edge | Level | Command |
|
|
3
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
4
|
+
| Accessible host heading | heading snippet | linked h3 names article | omitted snippet retains title | browser reader | N/A presentation | Svelte5/browser+SSR | optional snippet | component+a11y | pnpm --filter @happyvertical/smrt-svelte exec vitest run src/components/overview/__tests__/OverviewGrid.test.ts |
|
|
5
|
+
| Plain view, editable frame | presentation plain/editing toggle | plain attribute, toolbar and inert edit preview | default card unchanged | authorized editor | N/A presentation | Svelte5/browser | optional enum typed | component | same |
|
|
6
|
+
No persistence/auth/executor changes. Existing grid suite covers denied editor, widget failures, keyboard edits. New behavior is additive, no bug-fix base comparison required.
|
|
7
|
+
|
|
8
|
+
Validation: OverviewGrid component suite passed on this change (includes linked article headings, a11y, edit chrome and existing default regressions). Package typecheck and aggregate release checks are recorded in release evidence.
|
|
@@ -358,3 +358,14 @@ and the person can undo the change.
|
|
|
358
358
|
`createOverviewAssistantSurface` is wired to the stored user tier through
|
|
359
359
|
`createOverviewStore(...).save` as shown there.
|
|
360
360
|
- **Not built:** the activity widget (Track B, #3710) and agenda.
|
|
361
|
+
|
|
362
|
+
## Host section presentation
|
|
363
|
+
|
|
364
|
+
`OverviewGrid` defaults to framed cards. Pass `presentation="plain"` for
|
|
365
|
+
unframed page sections; editing restores the visible tile frame and controls.
|
|
366
|
+
The optional `heading(widget, title)` snippet supplies heading content, such as
|
|
367
|
+
an application-owned link. The grid still renders its `headingLevel` element
|
|
368
|
+
and generated id used by the article's accessible name; the snippet must not
|
|
369
|
+
supply another heading. Use trusted application routes for links. Widget bodies
|
|
370
|
+
should omit duplicate section headings. Default titles remain unchanged when
|
|
371
|
+
no snippet is supplied.
|
package/agents/screens.md
CHANGED
|
@@ -55,3 +55,8 @@ stays below the domain layer, and the app passes both in as props.
|
|
|
55
55
|
default and does not replace it. Not wired into recipe surfaces yet (#3708).
|
|
56
56
|
- Component props live in `types.ts`, not in the `.svelte` files, so the barrel
|
|
57
57
|
never re-exports types from a component.
|
|
58
|
+
- **Where a package model's definition and data come from (#3749).** A cookbook
|
|
59
|
+
app's `smrt()` preset hosts the cookbook's package models; the definition is
|
|
60
|
+
`collectionDefinitions[<collection>]` from `@smrt/web` and the source is the
|
|
61
|
+
model's `/api/<collection>` route (see [cookbook-shell.md](cookbook-shell.md)).
|
|
62
|
+
A model the API does not expose has neither, so the entry page shows its notice.
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* `CookbookApp` (#3749): `AppShell` driven by a cookbook. A generated app
|
|
4
|
+
* needs a layout that renders this with `smrt.cookbook.json` (validated) and
|
|
5
|
+
* the catalog the server helper loads, plus one route that renders
|
|
6
|
+
* `CookbookSectionPage`.
|
|
7
|
+
*
|
|
8
|
+
* It builds the navigation, layout, theme and section overviews with
|
|
9
|
+
* `buildCookbookShell`, keeps layout and overview edits in memory, and hands
|
|
10
|
+
* each edit to `oncookbookchange` for the host to persist.
|
|
11
|
+
*/
|
|
12
|
+
import { createRawSnippet, mount, unmount, untrack } from 'svelte';
|
|
13
|
+
import AppShell from '../app/AppShell.svelte';
|
|
14
|
+
import type { ShellSlotItem } from '../app/slot-item.js';
|
|
15
|
+
import { parseRecipeExportRef } from '../overview/recipe-widgets.js';
|
|
16
|
+
import {
|
|
17
|
+
resolveWidgetComponents,
|
|
18
|
+
type WidgetComponents,
|
|
19
|
+
type WidgetRegistry,
|
|
20
|
+
} from '../overview/registry.js';
|
|
21
|
+
import type { OverviewOverride } from '../overview/types.js';
|
|
22
|
+
import {
|
|
23
|
+
isShellLayoutEmpty,
|
|
24
|
+
type ShellLayout,
|
|
25
|
+
} from '../workspace/admin-shell/layout.js';
|
|
26
|
+
import { applyCookbookTheme } from './brand-theme.js';
|
|
27
|
+
import { buildCookbookShell, humanize, pluralize } from './build.js';
|
|
28
|
+
import { type CookbookContext, setCookbookContext } from './context.js';
|
|
29
|
+
import { createCookbookRegistry } from './registry.js';
|
|
30
|
+
import type { CookbookAppProps } from './types.js';
|
|
31
|
+
|
|
32
|
+
let {
|
|
33
|
+
cookbook,
|
|
34
|
+
catalog,
|
|
35
|
+
loaders,
|
|
36
|
+
widgetContext,
|
|
37
|
+
resolveExport,
|
|
38
|
+
oncookbookchange,
|
|
39
|
+
recipeOrder,
|
|
40
|
+
entryPath,
|
|
41
|
+
sectionPath,
|
|
42
|
+
href,
|
|
43
|
+
title,
|
|
44
|
+
slotItems = [],
|
|
45
|
+
children,
|
|
46
|
+
...shellProps
|
|
47
|
+
}: CookbookAppProps = $props();
|
|
48
|
+
|
|
49
|
+
const shell = $derived(
|
|
50
|
+
buildCookbookShell({
|
|
51
|
+
cookbook,
|
|
52
|
+
catalog,
|
|
53
|
+
...(title ? { title } : {}),
|
|
54
|
+
...(recipeOrder ? { recipeOrder } : {}),
|
|
55
|
+
...(entryPath ? { entryPath } : {}),
|
|
56
|
+
...(sectionPath ? { sectionPath } : {}),
|
|
57
|
+
...(href ? { href } : {}),
|
|
58
|
+
dataWidgets: Boolean(loaders?.metric && loaders?.records),
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Local copies the user edits; they follow the cookbook whenever it changes.
|
|
63
|
+
let layout = $state.raw<ShellLayout | null>(untrack(() => shell.layout));
|
|
64
|
+
let overrides = $state.raw<Record<string, unknown>>(
|
|
65
|
+
untrack(() => storedOverrides()),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
function storedOverrides(): Record<string, unknown> {
|
|
69
|
+
return Object.fromEntries(
|
|
70
|
+
Object.entries(shell.overviews)
|
|
71
|
+
.filter(([, page]) => page.override != null)
|
|
72
|
+
.map(([id, page]) => [id, page.override]),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
$effect(() => {
|
|
77
|
+
layout = shell.layout;
|
|
78
|
+
overrides = storedOverrides();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
function emit() {
|
|
82
|
+
if (!oncookbookchange) return;
|
|
83
|
+
const next = { ...cookbook };
|
|
84
|
+
if (isShellLayoutEmpty(layout)) delete next.layout;
|
|
85
|
+
else next.layout = layout as ShellLayout;
|
|
86
|
+
if (Object.keys(overrides).length > 0) {
|
|
87
|
+
next.overviews = overrides as NonNullable<typeof next.overviews>;
|
|
88
|
+
} else delete next.overviews;
|
|
89
|
+
oncookbookchange(next);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const preset = $derived(applyCookbookTheme(shell.theme));
|
|
93
|
+
|
|
94
|
+
// Widget registry: core widgets plus the recipes' `widget` surfaces.
|
|
95
|
+
let registry = $state.raw<WidgetRegistry | null>(null);
|
|
96
|
+
let components = $state.raw<WidgetComponents>(new Map());
|
|
97
|
+
|
|
98
|
+
// Keyed by content: an edit rebuilds `shell` but must not rebuild the registry.
|
|
99
|
+
const widgetsKey = $derived(JSON.stringify(shell.widgets));
|
|
100
|
+
const shellWidgetsKey = $derived(JSON.stringify(shell.shellWidgets));
|
|
101
|
+
|
|
102
|
+
$effect(() => {
|
|
103
|
+
void widgetsKey;
|
|
104
|
+
const widgets = untrack(() => shell.widgets);
|
|
105
|
+
const resolve = resolveExport;
|
|
106
|
+
const dataLoaders = loaders;
|
|
107
|
+
let cancelled = false;
|
|
108
|
+
void (async () => {
|
|
109
|
+
const created = await createCookbookRegistry({
|
|
110
|
+
...(dataLoaders ? { loaders: dataLoaders } : {}),
|
|
111
|
+
widgets,
|
|
112
|
+
...(resolve ? { resolveExport: resolve } : {}),
|
|
113
|
+
});
|
|
114
|
+
const resolved = await resolveWidgetComponents(
|
|
115
|
+
created.registry.list().map((def) => def.type),
|
|
116
|
+
created.registry,
|
|
117
|
+
);
|
|
118
|
+
if (cancelled) return;
|
|
119
|
+
components = resolved;
|
|
120
|
+
registry = created.registry;
|
|
121
|
+
})();
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Recipe `shell-widget` surfaces become movable slot items.
|
|
128
|
+
let recipeSlotItems = $state.raw<ShellSlotItem[]>([]);
|
|
129
|
+
|
|
130
|
+
$effect(() => {
|
|
131
|
+
void shellWidgetsKey;
|
|
132
|
+
const widgets = untrack(() => shell.shellWidgets);
|
|
133
|
+
const resolve = resolveExport;
|
|
134
|
+
if (!resolve || widgets.length === 0) {
|
|
135
|
+
recipeSlotItems = [];
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let cancelled = false;
|
|
139
|
+
void (async () => {
|
|
140
|
+
const items: ShellSlotItem[] = [];
|
|
141
|
+
for (const widget of widgets) {
|
|
142
|
+
const ref = parseRecipeExportRef(widget.export);
|
|
143
|
+
if (!ref) continue;
|
|
144
|
+
try {
|
|
145
|
+
const loaded = (await resolve(ref.specifier, ref.exportName)) as
|
|
146
|
+
| { default?: unknown }
|
|
147
|
+
| ((...args: never[]) => unknown)
|
|
148
|
+
| undefined;
|
|
149
|
+
const component =
|
|
150
|
+
loaded && typeof loaded === 'object' && 'default' in loaded
|
|
151
|
+
? loaded.default
|
|
152
|
+
: loaded;
|
|
153
|
+
if (typeof component !== 'function') continue;
|
|
154
|
+
items.push({
|
|
155
|
+
id: `recipe:${widget.recipeId}:${ref.exportName}`,
|
|
156
|
+
label: widget.label,
|
|
157
|
+
slot: widget.slot,
|
|
158
|
+
render: createRawSnippet(() => ({
|
|
159
|
+
render: () => '<span data-cookbook-slot-widget></span>',
|
|
160
|
+
setup: (node: Element) => {
|
|
161
|
+
const instance = mount(component as never, {
|
|
162
|
+
target: node as HTMLElement,
|
|
163
|
+
});
|
|
164
|
+
return () => {
|
|
165
|
+
void unmount(instance);
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
})),
|
|
169
|
+
});
|
|
170
|
+
} catch {
|
|
171
|
+
// A widget that cannot load is left out; the shell still renders.
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (!cancelled) recipeSlotItems = items;
|
|
175
|
+
})();
|
|
176
|
+
return () => {
|
|
177
|
+
cancelled = true;
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const models = $derived.by(() => {
|
|
182
|
+
const seen = new Set<string>();
|
|
183
|
+
const choices: { value: string; label: string }[] = [];
|
|
184
|
+
for (const entry of shell.entries.values()) {
|
|
185
|
+
if (seen.has(entry.model.id)) continue;
|
|
186
|
+
seen.add(entry.model.id);
|
|
187
|
+
choices.push({
|
|
188
|
+
value: entry.model.id,
|
|
189
|
+
label: pluralize(humanize(entry.model.name)),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return choices;
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
setCookbookContext({
|
|
196
|
+
get shell() {
|
|
197
|
+
return shell;
|
|
198
|
+
},
|
|
199
|
+
get registry() {
|
|
200
|
+
return registry;
|
|
201
|
+
},
|
|
202
|
+
get components() {
|
|
203
|
+
return components;
|
|
204
|
+
},
|
|
205
|
+
get models() {
|
|
206
|
+
return models;
|
|
207
|
+
},
|
|
208
|
+
overrideFor: (id) => overrides[id] ?? null,
|
|
209
|
+
setOverride: (id, override: OverviewOverride | null) => {
|
|
210
|
+
const next = { ...overrides };
|
|
211
|
+
if (override === null) delete next[id];
|
|
212
|
+
else next[id] = override;
|
|
213
|
+
overrides = next;
|
|
214
|
+
emit();
|
|
215
|
+
},
|
|
216
|
+
widgetContext: () => widgetContext?.() ?? {},
|
|
217
|
+
} satisfies CookbookContext);
|
|
218
|
+
</script>
|
|
219
|
+
|
|
220
|
+
<AppShell
|
|
221
|
+
{...shellProps}
|
|
222
|
+
title={shell.title}
|
|
223
|
+
navMode="sections"
|
|
224
|
+
navGroups={shell.navGroups}
|
|
225
|
+
{layout}
|
|
226
|
+
onlayoutchange={(next) => {
|
|
227
|
+
layout = next;
|
|
228
|
+
emit();
|
|
229
|
+
}}
|
|
230
|
+
{preset}
|
|
231
|
+
colorScheme={shell.theme.colorScheme}
|
|
232
|
+
sectionHref={(id) => shell.sectionPath(id)}
|
|
233
|
+
slotItems={[...slotItems, ...recipeSlotItems]}
|
|
234
|
+
>
|
|
235
|
+
{@render children()}
|
|
236
|
+
</AppShell>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CookbookApp.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/cookbook/CookbookApp.svelte.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAsNnD,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A section page's editable overview (#3749): `OverviewGrid` over the
|
|
4
|
+
* section's definition. The cookbook owns the override (the controller reads
|
|
5
|
+
* it and writes each edit back). Mount it under `{#key}` on the section id.
|
|
6
|
+
*/
|
|
7
|
+
import { onDestroy, untrack } from 'svelte';
|
|
8
|
+
import type { OverviewModelChoice } from '../overview/grid-types.js';
|
|
9
|
+
import OverviewGrid from '../overview/OverviewGrid.svelte';
|
|
10
|
+
import type { WidgetComponents, WidgetRegistry } from '../overview/registry.js';
|
|
11
|
+
import type { OverviewDefinition } from '../overview/types.js';
|
|
12
|
+
import { useShellLayout } from '../workspace/admin-shell/layout-context.js';
|
|
13
|
+
import { type CookbookOverviewStore, createSectionOverview } from './page.js';
|
|
14
|
+
import type { CookbookEntry } from './types.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
definition: OverviewDefinition;
|
|
18
|
+
registry: WidgetRegistry;
|
|
19
|
+
store: CookbookOverviewStore;
|
|
20
|
+
entries: ReadonlyMap<string, CookbookEntry>;
|
|
21
|
+
components: WidgetComponents;
|
|
22
|
+
models: readonly OverviewModelChoice[];
|
|
23
|
+
/** Host capabilities for widget loaders. */
|
|
24
|
+
hostContext: () => Record<string, unknown>;
|
|
25
|
+
label: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
definition,
|
|
30
|
+
registry,
|
|
31
|
+
store,
|
|
32
|
+
entries,
|
|
33
|
+
components,
|
|
34
|
+
models,
|
|
35
|
+
hostContext,
|
|
36
|
+
label,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
const layout = useShellLayout();
|
|
40
|
+
|
|
41
|
+
const controller = createSectionOverview(
|
|
42
|
+
untrack(() => definition),
|
|
43
|
+
untrack(() => registry),
|
|
44
|
+
untrack(() => store),
|
|
45
|
+
() => ({
|
|
46
|
+
...hostContext(),
|
|
47
|
+
sections: layout.sections,
|
|
48
|
+
entries,
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// The shortcuts follow the menu (renames, hides, moves); the grid already
|
|
53
|
+
// requested the first load, so skip that run.
|
|
54
|
+
let first = true;
|
|
55
|
+
$effect(() => {
|
|
56
|
+
void JSON.stringify(layout.sections);
|
|
57
|
+
if (first) {
|
|
58
|
+
first = false;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
untrack(() => {
|
|
62
|
+
for (const widget of controller.document.widgets) {
|
|
63
|
+
controller.reload(widget.id);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
onDestroy(() => controller.destroy());
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<OverviewGrid {controller} {components} {models} {label} />
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { OverviewModelChoice } from '../overview/grid-types.js';
|
|
2
|
+
import type { WidgetComponents, WidgetRegistry } from '../overview/registry.js';
|
|
3
|
+
import type { OverviewDefinition } from '../overview/types.js';
|
|
4
|
+
import { type CookbookOverviewStore } from './page.js';
|
|
5
|
+
import type { CookbookEntry } from './types.js';
|
|
6
|
+
interface Props {
|
|
7
|
+
definition: OverviewDefinition;
|
|
8
|
+
registry: WidgetRegistry;
|
|
9
|
+
store: CookbookOverviewStore;
|
|
10
|
+
entries: ReadonlyMap<string, CookbookEntry>;
|
|
11
|
+
components: WidgetComponents;
|
|
12
|
+
models: readonly OverviewModelChoice[];
|
|
13
|
+
/** Host capabilities for widget loaders. */
|
|
14
|
+
hostContext: () => Record<string, unknown>;
|
|
15
|
+
label: string;
|
|
16
|
+
}
|
|
17
|
+
declare const CookbookSectionOverview: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type CookbookSectionOverview = ReturnType<typeof CookbookSectionOverview>;
|
|
19
|
+
export default CookbookSectionOverview;
|
|
20
|
+
//# sourceMappingURL=CookbookSectionOverview.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CookbookSectionOverview.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/cookbook/CookbookSectionOverview.svelte.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAChF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE/D,OAAO,EAAE,KAAK,qBAAqB,EAAyB,MAAM,WAAW,CAAC;AAC9E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD,UAAU,KAAK;IACb,UAAU,EAAE,kBAAkB,CAAC;IAC/B,QAAQ,EAAE,cAAc,CAAC;IACzB,KAAK,EAAE,qBAAqB,CAAC;IAC7B,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IAC5C,UAAU,EAAE,gBAAgB,CAAC;IAC7B,MAAM,EAAE,SAAS,mBAAmB,EAAE,CAAC;IACvC,4CAA4C;IAC5C,WAAW,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,KAAK,EAAE,MAAM,CAAC;CACf;AA4DD,QAAA,MAAM,uBAAuB,2CAAwC,CAAC;AACtE,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC1E,eAAe,uBAAuB,CAAC"}
|