@happyvertical/smrt-svelte 0.55.23 → 0.56.0
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 +2 -2
- package/README.md +2 -2
- package/agents/overview-surfaces.md +117 -6
- package/agents/screens.md +171 -5
- package/dist/components/command-palette/CommandPaletteDialog.svelte +1 -1
- package/dist/components/overview/OverviewGrid.svelte +8 -4
- package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
- package/dist/components/overview/WidgetOptionsForm.svelte +13 -2
- package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -1
- package/dist/components/overview/format.d.ts +3 -12
- package/dist/components/overview/format.d.ts.map +1 -1
- package/dist/components/overview/format.js +2 -45
- package/dist/components/overview/grid-types.d.ts +6 -11
- package/dist/components/overview/grid-types.d.ts.map +1 -1
- package/dist/components/overview/index.d.ts +5 -1
- package/dist/components/overview/index.d.ts.map +1 -1
- package/dist/components/overview/index.js +3 -0
- package/dist/components/overview/recipe-widgets.d.ts +6 -3
- package/dist/components/overview/recipe-widgets.d.ts.map +1 -1
- package/dist/components/overview/recipe-widgets.js +9 -0
- package/dist/components/overview/record.d.ts +51 -0
- package/dist/components/overview/record.d.ts.map +1 -0
- package/dist/components/overview/record.js +146 -0
- package/dist/components/overview/registry.d.ts +3 -0
- package/dist/components/overview/registry.d.ts.map +1 -1
- package/dist/components/overview/registry.js +25 -0
- package/dist/components/overview/server.d.ts +5 -1
- package/dist/components/overview/server.d.ts.map +1 -1
- package/dist/components/overview/server.js +5 -1
- package/dist/components/overview/types.d.ts +29 -0
- package/dist/components/overview/types.d.ts.map +1 -1
- package/dist/components/overview/widgets/ChartWidget.svelte +5 -265
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +1 -1
- package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -1
- package/dist/components/overview/widgets/LineItemsWidget.svelte +144 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RecordListWidget.svelte +67 -3
- package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -1
- package/dist/components/overview/widgets/RecordTable.svelte +167 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts +21 -0
- package/dist/components/overview/widgets/RecordTable.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte +117 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts +6 -0
- package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts.map +1 -0
- package/dist/components/overview/widgets/core.d.ts +13 -2
- package/dist/components/overview/widgets/core.d.ts.map +1 -1
- package/dist/components/overview/widgets/core.js +43 -2
- package/dist/components/overview/widgets/data.d.ts +97 -8
- package/dist/components/overview/widgets/data.d.ts.map +1 -1
- package/dist/components/overview/widgets/line-totals.d.ts +48 -0
- package/dist/components/overview/widgets/line-totals.d.ts.map +1 -0
- package/dist/components/overview/widgets/line-totals.js +91 -0
- package/dist/components/overview/widgets/record-loaders.d.ts +63 -0
- package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-loaders.js +150 -0
- package/dist/components/overview/widgets/record-table.d.ts +45 -0
- package/dist/components/overview/widgets/record-table.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-table.js +201 -0
- package/dist/components/overview/widgets/record-widgets.d.ts +27 -0
- package/dist/components/overview/widgets/record-widgets.d.ts.map +1 -0
- package/dist/components/overview/widgets/record-widgets.js +100 -0
- package/dist/components/screens/DetailScreen.svelte +105 -6
- package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -1
- package/dist/components/screens/EditForm.svelte +39 -3
- package/dist/components/screens/EditForm.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecipeScreens.svelte +18 -18
- package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -1
- package/dist/components/screens/RecordHeader.svelte +238 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts +26 -0
- package/dist/components/screens/RecordHeader.svelte.d.ts.map +1 -0
- package/dist/components/screens/RecordScreen.svelte +511 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts +5 -0
- package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -0
- package/dist/components/screens/columns.d.ts +29 -0
- package/dist/components/screens/columns.d.ts.map +1 -0
- package/dist/components/screens/columns.js +115 -0
- package/dist/components/screens/fields.d.ts +12 -2
- package/dist/components/screens/fields.d.ts.map +1 -1
- package/dist/components/screens/fields.js +37 -5
- package/dist/components/screens/index.d.ts +7 -2
- package/dist/components/screens/index.d.ts.map +1 -1
- package/dist/components/screens/index.js +6 -1
- package/dist/components/screens/record-header.d.ts +51 -0
- package/dist/components/screens/record-header.d.ts.map +1 -0
- package/dist/components/screens/record-header.js +118 -0
- package/dist/components/screens/relations.d.ts +49 -0
- package/dist/components/screens/relations.d.ts.map +1 -0
- package/dist/components/screens/relations.js +98 -0
- package/dist/components/screens/roles.d.ts +56 -0
- package/dist/components/screens/roles.d.ts.map +1 -0
- package/dist/components/screens/roles.js +174 -0
- package/dist/components/screens/types.d.ts +182 -1
- package/dist/components/screens/types.d.ts.map +1 -1
- package/dist/components/screens/values.d.ts +23 -1
- package/dist/components/screens/values.d.ts.map +1 -1
- package/dist/components/screens/values.js +80 -4
- package/dist/components/workspace/admin-shell/AdminShell.svelte +4 -4
- package/dist/components/workspace/admin-shell/ShellIconButton.svelte +2 -2
- package/dist/components/workspace/admin-shell/TenantNav.svelte +8 -8
- package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +3 -3
- package/dist/i18n/strings.overview.d.ts +25 -0
- package/dist/i18n/strings.overview.d.ts.map +1 -1
- package/dist/i18n/strings.overview.js +25 -0
- package/dist/i18n/strings.screens.d.ts +8 -0
- package/dist/i18n/strings.screens.d.ts.map +1 -1
- package/dist/i18n/strings.screens.js +8 -0
- package/package.json +16 -16
package/AGENTS.md
CHANGED
|
@@ -15,12 +15,12 @@ subpath you are editing. This file keeps what holds in every module.
|
|
|
15
15
|
| `src/test-support/` + `__tests__/` | the golden-test harness and pattern for Svelte component tests | [agents/testing.md](agents/testing.md) |
|
|
16
16
|
| `src/components/import-export/` (`./import-export`) | `ImportExport` CSV/TSV import (column mapping, validation preview, error report) and export (column selection) driven by collection definitions + resolved field policy, over a pure Node-tested CSV layer (#3715) | [agents/import-export.md](agents/import-export.md) |
|
|
17
17
|
| `src/components/settings/` (`./settings`) | `SettingsCatalog`, `paginateSettingsCatalog`, and the summary-vs-detail scalability contract | [agents/settings.md](agents/settings.md) |
|
|
18
|
-
| `src/components/screens/` (`./screens`) | `RecipeScreens` / `ListScreen` / `DetailScreen` / `EditForm`: list, view, create and edit derived from a generated web definition plus resolved field policy; pure `deriveScreenFields` selection, minor-unit money handling, transport-neutral source (#3718) | [agents/screens.md](agents/screens.md) |
|
|
18
|
+
| `src/components/screens/` (`./screens`) | `RecipeScreens` / `ListScreen` / `RecordScreen` / `DetailScreen` / `EditForm`: list, view, create and edit derived from a generated web definition plus resolved field policy; pure `deriveScreenFields` selection, `deriveFieldRoles` (title / identifier / status / money / totals roles) and `deriveScreenRelations` (children / items / reference); the record page's header (identifier, status, key facts, overflow Delete), details card and overview, select / derived fields and an items slot in inline (desktop) or sheet (phone) editing; minor-unit money handling, transport-neutral source (#3718) | [agents/screens.md](agents/screens.md) |
|
|
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
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) |
|
|
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
|
+
| `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 (records as a role-column table with a `DataTable` summaries footer), the record-page `related` (tabbed) and `line-items` (totals block) 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) |
|
|
24
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) | — |
|
|
25
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) | — |
|
|
26
26
|
| `src/web/webmcp-provider.ts` | Provider config for generated data/model WebMCP tools: definitions, effect policy, namespace, budget, legacy/canonical filters, and fetcher seams (#2520) | — |
|
package/README.md
CHANGED
|
@@ -708,13 +708,13 @@ importable, even if it appears in `dist/`.
|
|
|
708
708
|
| `@happyvertical/smrt-svelte/forms` | Form inputs (TextInput, Select, MoneyInput, DateTimeInput, Toggle, etc.) |
|
|
709
709
|
| `@happyvertical/smrt-svelte/import-export` | `ImportExport` (CSV/TSV import with column mapping, validation preview and error report; export with column selection), `createCollectionImportExport`, `fieldsFromCollectionDefinition`, and the pure CSV layer |
|
|
710
710
|
| `@happyvertical/smrt-svelte/settings` | Server-paged settings search, selection, and list/detail layout (`SettingsCatalog`, `paginateSettingsCatalog`) |
|
|
711
|
-
| `@happyvertical/smrt-svelte/screens` | List, view, create and edit screens derived from a generated web definition and resolved field policy (`RecipeScreens`, `ListScreen`, `DetailScreen`, `EditForm`) |
|
|
711
|
+
| `@happyvertical/smrt-svelte/screens` | List, view, create and edit screens derived from a generated web definition and resolved field policy (`RecipeScreens`, `ListScreen`, `RecordScreen`, `DetailScreen`, `EditForm`); `RecordScreen` is the record view with a customizable related-records overview and inline (desktop) or sheet (phone) editing, `deriveScreenRelations` its relation rule |
|
|
712
712
|
| `@happyvertical/smrt-svelte/workspace` | AdminShell, ShellState, tenant nav, focus tools, settings, activities, and system/app panels |
|
|
713
713
|
| `@happyvertical/smrt-svelte/app` | `AppShell` (Provider + themes + AdminShell + nav/dock slots, `dockToggles` buttons and host `slots` for the header/footer/sidebar regions), `OwnerSetupForm` (first-run owner setup), `ShellSettingsPage`, `RuntimeDiagnosticsWebMcp` |
|
|
714
714
|
| `@happyvertical/smrt-svelte/app/runtime-diagnostics` | Svelte-free diagnostics WebMCP registration and its tool name/endpoint constants, importable from server routes |
|
|
715
715
|
| `@happyvertical/smrt-svelte/command-palette` | Global search and command palette: `CommandPalette` (shell-slot trigger plus dialog), `createCommandPalette`, the provider contract, and the navigation and manifest-driven model providers |
|
|
716
716
|
| `@happyvertical/smrt-svelte/overview` | Customizable overview surfaces: `OverviewGrid` (flow grid with keyboard reorder and resize), `createOverview` (host-owned controller), the widget registry (`registerWidget`), `registerCoreWidgets` (metric, chart, record list, shortcuts, note) and `shortcutsFromNav` |
|
|
717
|
-
| `@happyvertical/smrt-svelte/overview/server` | Svelte-free half of the overview surface: the data model (`resolveOverview`, `checkOverviewOverride`), option-schema validation, `WidgetRegistry`, and `loadOverview` for a page's server load |
|
|
717
|
+
| `@happyvertical/smrt-svelte/overview/server` | Svelte-free half of the overview surface: the data model (`resolveOverview`, `checkOverviewOverride`), option-schema validation, `WidgetRegistry`, and `loadOverview` for a page's server load; `recordOverview` / `recordOverviewId` (the record view's default overview, per model kind) |
|
|
718
718
|
| `@happyvertical/smrt-svelte/workspace/legacy` | Opt-in ToolsDock compatibility surface for applications migrating to AdminShell |
|
|
719
719
|
| `@happyvertical/smrt-svelte/workspace/server` | Server-side workspace helpers (Node only) |
|
|
720
720
|
| `@happyvertical/smrt-svelte/workspace/live` | `systemFeed` — the AdminShell system scope (jobs/schedules/dispatch) polled from an app status endpoint; deliberately carries no `smrt-web` dependency |
|
|
@@ -22,6 +22,11 @@ assistant operations with Undo ("Assistant operations").
|
|
|
22
22
|
| `./overview/server` (`server.ts`) | Svelte-free: types, `validateWidgetOptions`, the model (`resolveOverview`, `checkOverviewOverride`, `applyOverviewOverride`, `diffOverview`, `sanitizeOverview`, ...), `WidgetRegistry` / `registerWidget`, `loadOverview`, the operations (`planOverviewOperations`, `describeOverview`) and `createOverviewAssistantSurface`. Safe in a `+page.server.ts` or a save endpoint. |
|
|
23
23
|
| `./overview` (`index.ts`) | Everything above plus `OverviewGrid`, `createOverview` (controller), `WidgetOptionsForm`, the core widgets and `registerCoreWidgets`, `shortcutsFromNav`, `formatWidgetValue`, `parseMarkdown` / `safeHref`, `createOverviewAssistant` / `useOverviewAssistantTools` / `OverviewAssistantUndo`. |
|
|
24
24
|
|
|
25
|
+
`ChartWidget` delegates to `@happyvertical/smrt-ui/charts` so domain packages
|
|
26
|
+
can reuse the same renderer without depending on this integration package.
|
|
27
|
+
`formatWidgetValue` remains a compatibility re-export of the Svelte-free
|
|
28
|
+
`@happyvertical/smrt-ui/charts/format` entry. Existing translation keys stay stable.
|
|
29
|
+
|
|
25
30
|
## Data model (`types.ts`, `model.ts`)
|
|
26
31
|
|
|
27
32
|
A saved overview is data, never code:
|
|
@@ -113,10 +118,26 @@ return { overview }; // LoadedOverview: JSON, serializable
|
|
|
113
118
|
|
|
114
119
|
- A widget's `load(options, ctx)` receives **validated options only** and a
|
|
115
120
|
`WidgetLoadContext` the host built from the request principal: `overviewId`, an
|
|
116
|
-
abort `signal`, `locale`,
|
|
121
|
+
abort `signal`, `locale`, the optional core `record` key (below), and any host
|
|
122
|
+
capabilities (index keys). It must
|
|
117
123
|
authorize through `ctx` (the user's permissions and tenant filters), never
|
|
118
124
|
through option values. Reports-backed chart loaders call the runtime-report
|
|
119
125
|
compiler with this context; this package does not depend on it.
|
|
126
|
+
- **`ctx.record = { model, id }`** is a core capability key: the record a
|
|
127
|
+
record overview is rendered on (`RecordScreen` sets it from the screen's
|
|
128
|
+
definition `objectRef` and the record's id; a host's server load sets it the
|
|
129
|
+
same way for a record page). With it comes the **parent-filter vocabulary**
|
|
130
|
+
of the core `records` and `metric` loaders: `filter: 'parent'` plus `field`
|
|
131
|
+
(the FK field on `options.model`) mean "rows of `model` whose `field` equals
|
|
132
|
+
`ctx.record.id`" — a count for `metric`, the newest rows for `records`
|
|
133
|
+
(`sort` / `direction` / `limit` as usual). A `metric` with `parentField`
|
|
134
|
+
uses that as the FK and measures `field` (`measure: 'sum'`, `field:
|
|
135
|
+
'totalAmount'`, `parentField: 'customerId'`: the record's order total). A
|
|
136
|
+
`records` load may carry `columns` (comma-separated keys; fill each row's
|
|
137
|
+
`cells`). A loader given `filter: 'parent'`
|
|
138
|
+
without `ctx.record` or `field` must return an empty result, never the
|
|
139
|
+
unfiltered model. The shell's own loaders need no data access: hosts
|
|
140
|
+
implement it, the same way they implement every other named filter.
|
|
120
141
|
- Loads run in parallel with a per-widget timeout (default 8 s; the signal
|
|
121
142
|
aborts). A thrown error, a timeout or non-serializable / oversized data
|
|
122
143
|
(default 256 KB of JSON) becomes a per-widget error tile
|
|
@@ -197,9 +218,9 @@ shape. Data shapes are in `widgets/data.ts`.
|
|
|
197
218
|
|
|
198
219
|
| Type | Options | Data |
|
|
199
220
|
|---|---|---|
|
|
200
|
-
| `metric` | `title`, `model`*, `measure`, `field`, `filter` | `{ value, format?, currency?, label?, change?, href? }` (money = integer minor units) |
|
|
221
|
+
| `metric` | `title`, `model`*, `measure`, `field`, `filter`, `parentField` | `{ value, format?, currency?, label?, change?, href? }` (money = integer minor units) |
|
|
201
222
|
| `chart` | `title`, `model`*, `groupBy`, `measure`, `field`, `period`, `style` (bar/line/table/pie/stat), `filter` | `{ points: [{ label, value }], format?, currency?, href?, delta? }`, one measure of a runtime report |
|
|
202
|
-
| `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 |
|
|
223
|
+
| `records` | `title`, `model`*, `sort`, `field`, `direction`, `limit`, `filter`, `columns`, `summaries` | `{ rows: [{ id, title, subtitle?, meta?, href?, cells? }], total?, href?, columns?, currency?, timeZone?, summaries? }`; the loader chooses the title field and the visible records; `field` is the FK for `filter: 'parent'`; with columns (the option, else `data.columns`) the rows are a table — see "Records tables" |
|
|
203
224
|
| `shortcuts` | `title`, `section` | `{ items: [{ id, label, href, icon?, description?, count? }] }` (`count` draws as "N records"; widgets also receive the grid's `presentation` and the shortcuts widget drops its entry frame under `plain`); `shortcutsFromNav(sections, sectionId)` maps `resolveShellNavModel` / `useShellLayout().sections` (visible entries, user renames) |
|
|
204
225
|
| `note` | `title`, `body` (Markdown) | none |
|
|
205
226
|
|
|
@@ -219,6 +240,64 @@ else derived from the last two points). Motion honours `prefers-reduced-motion`.
|
|
|
219
240
|
The chart option schema is **version 2**: v1 (`style` bar or line, or no version)
|
|
220
241
|
migrates by identity (`migrateChartOptions`), so saved overviews keep validating.
|
|
221
242
|
|
|
243
|
+
## Records tables (`widgets/RecordTable.svelte`, `widgets/record-table.ts`)
|
|
244
|
+
|
|
245
|
+
The `records` widget with columns, each `related` tab and `line-items` render
|
|
246
|
+
one internal `RecordTable`: smrt-ui `DataTable` (dense, `phoneLayout="cards"`)
|
|
247
|
+
with cells by `RecordListColumn.role` — `identifier` monospace, `status` a
|
|
248
|
+
`Badge` (`statusTone`), `date` date-only in `data.timeZone` (default UTC, so
|
|
249
|
+
SSR and the browser agree; a non-ISO string is shown as sent), `money` from
|
|
250
|
+
integer minor units in `data.currency`, `quantity` a number — and the first
|
|
251
|
+
column linking to the row's `href`. The footer is `DataTable`'s `summaries`
|
|
252
|
+
(no per-widget footer code): the count (`total`, else the rows) in the first
|
|
253
|
+
column and the sum of every `money` / `quantity` column unless
|
|
254
|
+
`summarize: false` (a unit price). **A partial page is never summed**: when
|
|
255
|
+
`total` exceeds the rows, a sum cell is empty unless the loader sent
|
|
256
|
+
`summaries` (totals over every matching record, money in minor units). The
|
|
257
|
+
`columns` option is a comma-separated list because options are flat JSON
|
|
258
|
+
primitives (`parseColumnsOption` / `formatColumnsOption`, at most 6 keys);
|
|
259
|
+
labels and roles come from `data.columns`, else a humanized key. Pure,
|
|
260
|
+
Node-tested helpers: `formatRecordCell`, `statusTone` /
|
|
261
|
+
`DEFAULT_STATUS_TONES`, `statusLabel`, `resolveRecordColumns`,
|
|
262
|
+
`readRecordRows`, `isSummedColumn`.
|
|
263
|
+
|
|
264
|
+
## Record-page widgets (`related`, `line-items`; `widgets/record-loaders.ts`)
|
|
265
|
+
|
|
266
|
+
Built from the record's relations, so they register per record view —
|
|
267
|
+
`registerRecordWidgets(registry, { relations, records, viewAllHref?,
|
|
268
|
+
totals?, currency? })`; `RecordScreen` does it on its own registry when
|
|
269
|
+
`loaders.records` is given. **Loader contract: no new loader.** Each tab /
|
|
270
|
+
the lines are one call of the host's core `records` loader with the
|
|
271
|
+
parent-filter vocabulary (`model`, `field`, `filter: 'parent'`, `sort`,
|
|
272
|
+
`direction`, `limit`, `columns`), so a host that implements parent-filtered
|
|
273
|
+
`records` implements both. Loaders are Svelte-free on both entries
|
|
274
|
+
(`createRelatedLoader`, `createLineItemsLoader`, `relatedRelations`,
|
|
275
|
+
`relatedTabOptions`, `lineItemsRelation`, `lineItemsOptions`).
|
|
276
|
+
|
|
277
|
+
| Type | Options | Data |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| `related` | `title`, `relations` (relation keys `Order-customerId`, comma-separated; empty = every `children` relation, at most 20), `limit` (5) | `{ tabs: [{ key, label, rows, total?, columns, href?, currency?, timeZone?, summaries? }] }` |
|
|
280
|
+
| `line-items` | `title`, `relation` (an `items` relation key; empty = the first), `limit` (50, the max) | `{ rows, columns, total?, totals?: { subtotal?, discount?, tax?, total? }, currency?, summaries?, href? }` |
|
|
281
|
+
|
|
282
|
+
- `related`: one tab per relation with the count in the tab ("Orders 6"),
|
|
283
|
+
each a records table with a totals footer and that tab's one "View all"
|
|
284
|
+
(`viewAllHref`, else the loaded `href`). Relations with no rows are hidden;
|
|
285
|
+
all empty is one empty state. A failing tab is dropped; all failing fails
|
|
286
|
+
the widget. Never lists `items` relations.
|
|
287
|
+
- `line-items` (the contract's `lineItems`; widget type ids are lowercase
|
|
288
|
+
kebab): the lines in `sort` order, then the totals block
|
|
289
|
+
(`lineItemTotals`, pure). The record's own subtotal / discount / tax /
|
|
290
|
+
total are authoritative and shown as they are; one it lacks is computed
|
|
291
|
+
from the lines (subtotal = sum of the line amounts, total = subtotal -
|
|
292
|
+
discount + tax) and marked "(from the lines)"; discount and tax show when
|
|
293
|
+
present and non-zero. When the record's subtotal (or, without one, its
|
|
294
|
+
total) disagrees with the lines, a visible flag says "Lines add up to $X."
|
|
295
|
+
beside the recorded figure — never a silent override. A partial page of
|
|
296
|
+
lines is not compared.
|
|
297
|
+
- Tests: `__tests__/record-table.test.ts`, `__tests__/record-widgets.test.ts`,
|
|
298
|
+
`__tests__/line-items.test.ts` (axe on both components), the records cases
|
|
299
|
+
in `__tests__/widgets.test.ts`.
|
|
300
|
+
|
|
222
301
|
## Recipe widgets (`recipe-widgets.ts`)
|
|
223
302
|
|
|
224
303
|
A package contributes a widget with a `widget` surface on a recipe
|
|
@@ -232,6 +311,7 @@ turns the manifest's widget surfaces into `registry.register(...)` calls:
|
|
|
232
311
|
| `type`, `label`, `description`, `icon`, `version` | `type`, `title`, `description`, `icon`, `version` |
|
|
233
312
|
| `options` | `options` (plain JSON; `registry.register` re-validates the schema) |
|
|
234
313
|
| `export` | `loadComponent` (lazy; the export may be a component or a `{ default }` module) |
|
|
314
|
+
| `optionsEditor` | `loadOptionsEditor` (lazy; lets dependent choices use a package-owned editor without importing UI on the server) |
|
|
235
315
|
| `data.load` | `load` (resolved on first use; a missing or non-function loader is that widget's error tile) |
|
|
236
316
|
| `migrate` | `migrate`, **resolved eagerly** because the registry's `migrate` is synchronous |
|
|
237
317
|
| `allowedIn`, `defaultSpan`, `minSpan`, `maxSpan` | same names |
|
|
@@ -240,8 +320,9 @@ turns the manifest's widget surfaces into `registry.register(...)` calls:
|
|
|
240
320
|
- **The module resolver is injected.** `resolveExport(specifier, exportName)` is
|
|
241
321
|
the host's: a static map of dynamic imports in a bundled app, a stub in tests.
|
|
242
322
|
Nothing is imported when the manifest is read, and the registration itself
|
|
243
|
-
imports nothing except `migrate` exports.
|
|
244
|
-
|
|
323
|
+
imports nothing except `migrate` exports. UI hosts resolve lazy components
|
|
324
|
+
with `resolveWidgetComponents(...)` and custom editors with
|
|
325
|
+
`resolveWidgetOptionsEditors(...)`. Relative or absolute specifiers are refused.
|
|
245
326
|
- **Never throws for a bad surface.** The result is `{ registered, skipped,
|
|
246
327
|
dispose }`; a skip carries `reason` `duplicate_type` (a core widget or another
|
|
247
328
|
recipe owns the type; `replace` overrides), `invalid` (the registry refused the
|
|
@@ -250,7 +331,8 @@ turns the manifest's widget surfaces into `registry.register(...)` calls:
|
|
|
250
331
|
- **`enabled(recipeId)`** keeps widgets of recipes that are off out of the add
|
|
251
332
|
list. Call the helper again (after `dispose()`) when the set changes.
|
|
252
333
|
- Widget types share one registry namespace with the core widgets
|
|
253
|
-
(`metric`, `chart`, `records`, `shortcuts`, `note`)
|
|
334
|
+
(`metric`, `chart`, `records`, `shortcuts`, `note`) and the record-page
|
|
335
|
+
widgets (`related`, `line-items`). Pages still confine what
|
|
254
336
|
can appear with `defineOverview({ allowed })`, and a widget with `allowedIn`
|
|
255
337
|
only validates inside those overview ids.
|
|
256
338
|
- `src/components/overview/__tests__/recipe-widgets.test.ts` covers lazy
|
|
@@ -374,6 +456,35 @@ and the person can undo the change.
|
|
|
374
456
|
`createOverviewStore(...).save` as shown there.
|
|
375
457
|
- **Not built:** the activity widget (Track B, #3710) and agenda.
|
|
376
458
|
|
|
459
|
+
## Record overviews (`record.ts`)
|
|
460
|
+
|
|
461
|
+
`recordOverview(definition, relations, { id?, metrics?, maxMetrics?,
|
|
462
|
+
related? })` (both entries) is the default overview of a record page:
|
|
463
|
+
|
|
464
|
+
1. **Summary strip** (`metrics`, default on): `metric` tiles of span 1, a
|
|
465
|
+
count per `children` relation (`field` = the FK) and, where its columns
|
|
466
|
+
have a money column, the sum of it (`measure: 'sum'`, `field` = the
|
|
467
|
+
amount, `parentField` = the FK), at most `maxMetrics`
|
|
468
|
+
(`RECORD_OVERVIEW_MAX_METRICS` = 4). Ordinary widgets: removable.
|
|
469
|
+
2. **`line-items`** (span 4) for the first `items` relation.
|
|
470
|
+
3. **`related`** (span 4, id `related`) over the `children` relations;
|
|
471
|
+
`related: 'panels'` instead declares one `records` panel per relation
|
|
472
|
+
(span `RECORD_OVERVIEW_PANEL_SPAN` = 2, newest five, `columns` from the
|
|
473
|
+
relation) — the previous default.
|
|
474
|
+
|
|
475
|
+
Default widget ids come from the relation (`count-Order-customerId`,
|
|
476
|
+
`sum-...`, `items-OrderLine-orderId`, `records-...`; a `-2` suffix tells apart
|
|
477
|
+
two that fold alike), not their position, so a host that narrows the defaults
|
|
478
|
+
keeps every id, and added widgets (`w<n>`) never collide with a hidden
|
|
479
|
+
default; `models` confines added widgets to the child models and `allowed` is
|
|
480
|
+
`RECORD_OVERVIEW_ALLOWED` (`related`, `line-items`, `records`, `metric`,
|
|
481
|
+
`chart`, `note`). The id defaults to `recordOverviewId(definition.objectRef)`
|
|
482
|
+
— `detail:<model>` with `@`, `/` and `:` folded to dots
|
|
483
|
+
(`detail:acme.shop.Customer`), because a qualified model id is not a valid
|
|
484
|
+
overview id — so the layout persists **per model kind**, not per record.
|
|
485
|
+
`RecordScreen` renders it with `presentation="plain"` and extends the widget
|
|
486
|
+
context with `record`.
|
|
487
|
+
|
|
377
488
|
## Host section presentation
|
|
378
489
|
|
|
379
490
|
`OverviewGrid` defaults to framed cards. Pass `presentation="plain"` for
|
package/agents/screens.md
CHANGED
|
@@ -8,8 +8,9 @@ invariants, and the traps that apply before editing anything live in
|
|
|
8
8
|
|
|
9
9
|
`RecipeScreens` is the default screen set for a package that ships models but no
|
|
10
10
|
consumer UI (tags, facts, secrets, profiles, reports, prompts). It composes
|
|
11
|
-
`ListScreen`, `
|
|
12
|
-
|
|
11
|
+
`ListScreen`, `RecordScreen` (the record view, which itself composes
|
|
12
|
+
`DetailScreen`, `OverviewGrid` and `EditForm`) and `EditForm` (create), each of
|
|
13
|
+
which can also be used on its own. Every screen takes a generated web `definition`
|
|
13
14
|
(`ScreenCollectionDefinition`, structurally a `SmrtWebCollectionDefinition`) and
|
|
14
15
|
an optional resolved `policy` (`ScreenPolicy`, structurally smrt-fields'
|
|
15
16
|
`ResolvedObjectFieldPolicy`). The module imports neither package: smrt-svelte
|
|
@@ -34,14 +35,25 @@ stays below the domain layer, and the app passes both in as props.
|
|
|
34
35
|
policy default (an explicit `null` included) beats the manifest default.
|
|
35
36
|
`locked` governs who may change the POLICY, never the record value, so it is
|
|
36
37
|
not applied here.
|
|
37
|
-
- **Values are `values.ts`**: money (
|
|
38
|
-
|
|
38
|
+
- **Values are `values.ts`**: money (an INTEGER field with the `money` role —
|
|
39
|
+
`ui.format: 'money'`, the legacy `ui.widget: 'currency'`, an explicit role,
|
|
40
|
+
or a money name; see "Field roles") is integer minor units end to end. Editing shows major units but converts
|
|
39
41
|
with exact string math (`majorStringToMinorUnits`), never `* 100`; integers are
|
|
40
42
|
rejected unless JavaScript-safe; datetimes go out as ISO strings (an unchanged datetime keeps the
|
|
41
43
|
loaded record's full precision via `parseDraft`'s `record` context; the control
|
|
42
|
-
only shows minutes); JSON as parsed values.
|
|
44
|
+
only shows minutes); JSON as parsed values. For display an empty JSON
|
|
45
|
+
object/array (or one whose values are all blank) formats as `''`, the
|
|
46
|
+
"Not set" placeholder, and `DetailScreen` lists a non-empty object as
|
|
47
|
+
key/value rows (`jsonScreenEntries`). Empty optional text becomes `null` when the field is nullable and `''`
|
|
43
48
|
otherwise (unset text persists as `''`); every other empty optional field is
|
|
44
49
|
`null`. Errors are codes, mapped to messages by the component via `useI18n`.
|
|
50
|
+
- **Reference fields link (`relations`, `hrefFor`, `labels`).** `DetailScreen`
|
|
51
|
+
renders a `foreignKey` / `crossPackageRef` value as `<a href={hrefFor(field,
|
|
52
|
+
id)}>{labels[model](id)}</a>`; without an href the label alone, without a
|
|
53
|
+
label a shortened id (`shortScreenId`: first eight characters and `…`) in
|
|
54
|
+
`<code>` with the full id as its tooltip, inside the link when there is one. `labels` is keyed by the **target model id**,
|
|
55
|
+
which the screen finds through the `reference` relations (below), so pass
|
|
56
|
+
`relations` whenever you pass `labels`.
|
|
45
57
|
- **Transport-neutral.** `RecipeScreens` drives a `RecipeScreensSource`
|
|
46
58
|
(`list` / `get` / `create` / `update` / `delete`); an operation the source lacks
|
|
47
59
|
(or `can` turns off) is simply not offered, so a read-only source is a
|
|
@@ -49,6 +61,13 @@ stays below the domain layer, and the app passes both in as props.
|
|
|
49
61
|
fires on every user navigation, so an app can mirror them into its router.
|
|
50
62
|
`EditForm.onsubmit` throws to report a failed save or returns `{ fieldErrors }`.
|
|
51
63
|
Remount `EditForm` with `{#key}` when switching records (`RecipeScreens` does).
|
|
64
|
+
- **`view` and `edit` are `RecordScreen`** (below); `create` stays the bare
|
|
65
|
+
`EditForm`. `onnavigate` is unchanged (`edit` is still a view of its own, so
|
|
66
|
+
a router maps it to `.../<id>/edit`). Pass `definitions` (every exposed
|
|
67
|
+
collection, keyed by name) for the related-records panels and `record`
|
|
68
|
+
(loaders, widget context, overview override and its `onoverviewchange`,
|
|
69
|
+
`hrefFor`, `labels`, `viewAllHref`) for the rest of the record view; without
|
|
70
|
+
`definitions` there are no relations and the view is the detail alone.
|
|
52
71
|
- **Not the policy-editing surface.** The policy gear, field-usage telemetry and
|
|
53
72
|
the per-field renderer registry live in `ObjectForm`
|
|
54
73
|
(`@happyvertical/smrt-fields/svelte`); `EditForm` is the dependency-light
|
|
@@ -60,3 +79,150 @@ stays below the domain layer, and the app passes both in as props.
|
|
|
60
79
|
`collectionDefinitions[<collection>]` from `@smrt/web` and the source is the
|
|
61
80
|
model's `/api/<collection>` route (see [cookbook-shell.md](cookbook-shell.md)).
|
|
62
81
|
A model the API does not expose has neither, so the entry page shows its notice.
|
|
82
|
+
|
|
83
|
+
## Field roles (`roles.ts`)
|
|
84
|
+
|
|
85
|
+
`deriveFieldRoles(definition)` is the pure, Node-tested rule for what a field
|
|
86
|
+
MEANS on a record page: `FieldRole` = `title | identifier | status | money |
|
|
87
|
+
quantity | date | party | subtotal | tax | discount | total`, several per field
|
|
88
|
+
(`totalAmount` -> `['money', 'total']`), one key per declared field. Sources in
|
|
89
|
+
order:
|
|
90
|
+
|
|
91
|
+
1. Explicit metadata on `ScreenFieldDefinition`: `role` (authoritative — name
|
|
92
|
+
heuristics never add to it), `format` (`money | date | datetime | percent`,
|
|
93
|
+
wins over `ui.format`), `display: true` (the `title`), `enum`.
|
|
94
|
+
2. Manifest hints: `ui.format`, and `ui.widget: 'currency'` on an INTEGER (on
|
|
95
|
+
a text field it is the currency-code picker, never money;
|
|
96
|
+
`currencyCodeField` finds it, and `RecordScreen` formats amounts in the
|
|
97
|
+
record's own currency).
|
|
98
|
+
3. Name heuristics: `*Amount|*Total|price|cost` -> money on integers only;
|
|
99
|
+
`subtotal|tax|discount|total` (not `*Rate`); `status|state` WITH an enum;
|
|
100
|
+
`number|reference|code` but not `phoneNumber` / `postalCode` /
|
|
101
|
+
`currencyCode`; `*At|*Date|*On|*Until` datetimes; `quantity|qty`; an FK
|
|
102
|
+
whose name or target collection is party-like (customer, vendor, profile,
|
|
103
|
+
...). Without a `display` field the first text `name|title|label|
|
|
104
|
+
displayName` is the title.
|
|
105
|
+
|
|
106
|
+
`deriveScreenFields` uses them: a text field with an `enum` is kind `select`
|
|
107
|
+
(`options`), money comes from the roles, and every `ScreenField` carries
|
|
108
|
+
`roles`. `formatScreenValue` shows `format: 'date'` without a time and
|
|
109
|
+
`format: 'percent'` as a percentage. Helpers: `fieldsWithRole`,
|
|
110
|
+
`fieldWithRole`, `isDocumentLike` (identifier plus party or total),
|
|
111
|
+
`hasLineItemRoles` (quantity plus money). Columns by role are
|
|
112
|
+
`screens/columns.ts`: `relatedColumns` (identifier or title, status, lead
|
|
113
|
+
date, party, amount) and `lineItemColumns` (item, quantity, unit price —
|
|
114
|
+
`summarize: false` —, amount).
|
|
115
|
+
|
|
116
|
+
## Relations (`relations.ts`)
|
|
117
|
+
|
|
118
|
+
`deriveScreenRelations(definitions, model)` is the pure, Node-tested rule for
|
|
119
|
+
what a record view shows around a record. `definitions` is every exposed
|
|
120
|
+
collection definition keyed by collection name (`collectionDefinitions` from
|
|
121
|
+
`@smrt/web`); `model` is a qualified `objectRef` (or a key of the map). It
|
|
122
|
+
reads the generated `relationships` edges (`ScreenRelationshipDefinition`:
|
|
123
|
+
`field`, `kind`, `relatedCollection` — a collection **name**, resolved by map
|
|
124
|
+
key, then by `name`), so a definition without `relationships` has no
|
|
125
|
+
relations. A `ScreenRelation` is `{ model, field, label, kind, columns?, sort? }`
|
|
126
|
+
(`relationKey(relation)` = `Order-customerId`):
|
|
127
|
+
|
|
128
|
+
| `kind` | Meaning | `model` | `field` | `label` |
|
|
129
|
+
|---|---|---|---|---|
|
|
130
|
+
| `children` | another collection with a `foreignKey` / `crossPackageRef` edge targeting this model | the child's `objectRef` | the FK on the child | the child's plural title; two FKs to the same parent disambiguate as `Orders (Approver)` |
|
|
131
|
+
| `items` | a `children` edge marked `relationships[].role: 'items'` (core `@foreignKey(T, { role: 'items' })`), or a child with line-item roles and one FK here | as `children` | as `children` | as `children` |
|
|
132
|
+
| `reference` | one of this model's own FK edges | the target's `objectRef` | the FK on this model | the field label (`ownerId` -> `Owner`) |
|
|
133
|
+
|
|
134
|
+
`items` come first, then `children`, each sorted by label, then `reference`
|
|
135
|
+
in declaration order. `children` carry `relatedColumns`, `items`
|
|
136
|
+
`lineItemColumns` and a line `sort` (`sortOrder`, `position`, `lineNumber`,
|
|
137
|
+
`sequence`, else `createdAt`).
|
|
138
|
+
Edges whose target or source is not in `definitions` are skipped (an unexposed
|
|
139
|
+
model has no route to link to), as are `oneToMany` / `manyToMany` edges and
|
|
140
|
+
edges whose field the definition does not carry.
|
|
141
|
+
|
|
142
|
+
## RecordScreen (`RecordScreen.svelte`, `./screens`)
|
|
143
|
+
|
|
144
|
+
The record page: a header, a details card, a customizable `OverviewGrid`
|
|
145
|
+
below, and the edit form as a **mode** of the same screen. Props are
|
|
146
|
+
`RecordScreenProps` in `types.ts`.
|
|
147
|
+
|
|
148
|
+
- **Header anatomy (`RecordHeader.svelte`, model `deriveRecordHeader` in
|
|
149
|
+
`record-header.ts`).** A small back link above the title; the heading is
|
|
150
|
+
`title`, else a document's kind and identifier ("Order GRA-3159",
|
|
151
|
+
identifier monospace — never the party), else the title-role field, else
|
|
152
|
+
the identifier, else the class name; a non-document's identifier shows as
|
|
153
|
+
a monospace chip; the status is a `Badge` toned by value (`statusTones`,
|
|
154
|
+
else `DEFAULT_STATUS_TONES`, else neutral); two to four key facts by role
|
|
155
|
+
(party link, lead date shown date-only, total money, then email / phone).
|
|
156
|
+
It reads only the view fields, so a field the policy omits never reaches
|
|
157
|
+
the header. Actions: Edit is the one primary button (hidden while
|
|
158
|
+
editing); a "More actions" popover holds `extraActions` and Delete, which
|
|
159
|
+
still confirms.
|
|
160
|
+
- **Details card.** `DetailScreen` with `variant="card"`, `hideEmpty` (empty
|
|
161
|
+
fields behind "Show empty fields (n)") and `exclude`: what the header
|
|
162
|
+
shows, the currency-code field, and — when the model has an `items`
|
|
163
|
+
relation — the subtotal / discount / tax / total role fields, which the
|
|
164
|
+
line-items totals block shows instead. `children` render inside the card.
|
|
165
|
+
- **Body = `recordOverview(definition, relations)`** unless an `overview` is
|
|
166
|
+
passed: the summary strip, `line-items`, then `related` (see
|
|
167
|
+
[overview-surfaces.md](overview-surfaces.md#record-overviews-recordts)),
|
|
168
|
+
persisted **per model kind** under `recordOverviewId(objectRef)`
|
|
169
|
+
(`detail:acme.shop.Customer`). The grid renders `presentation="plain"`,
|
|
170
|
+
follows the shell's layout editor unless `layoutEditing` is passed, and
|
|
171
|
+
offers the child models in its add dialog. `override` / `onoverviewchange`
|
|
172
|
+
are the host-owned persistence pair of `createOverview`
|
|
173
|
+
(`@happyvertical/smrt-preferences`, kind `overview`, in production); omit
|
|
174
|
+
`override` to keep edits in memory. With `loaders.records` the screen also
|
|
175
|
+
registers `related` and `line-items` (`registerRecordWidgets`) with the
|
|
176
|
+
record's totals (`recordTotals`) and currency.
|
|
177
|
+
- **Loaders run in the browser with the record context.** `loaders`
|
|
178
|
+
(`CoreWidgetLoaders`) are registered on a registry of the screen's own
|
|
179
|
+
(or pass `registry` with recipe widgets on it); each `load` receives
|
|
180
|
+
`widgetContext` extended with `overviewId`, the `signal`, `locale` and
|
|
181
|
+
`record: { model: definition.objectRef, id }`, and honours `filter:
|
|
182
|
+
'parent'` + `field` as overview-surfaces.md describes. A widget without a
|
|
183
|
+
loader is not registered, so a default needing it is dropped (reported,
|
|
184
|
+
never an error); without `loaders` the grid is empty. A record change
|
|
185
|
+
reloads every panel; the registry and overview are fixed at mount, so
|
|
186
|
+
remount (`{#key}`) to change the model kind — `RecipeScreens` keys on
|
|
187
|
+
`definition.objectRef`.
|
|
188
|
+
- **One heading per related panel.** A `records` widget whose `model` +
|
|
189
|
+
`field` + `filter: 'parent'` match a `children` relation (user-added ones
|
|
190
|
+
included) is headed `Orders · 3` (the loader's `total`, else its row count)
|
|
191
|
+
with the panel's only "View all": `viewAllHref(relation)`, else the loaded
|
|
192
|
+
`href`. The screen strips `total` / `href` from that widget's data, so the
|
|
193
|
+
body never repeats them. A `metric` widget keeps the plain title and no
|
|
194
|
+
link (its number links when its data has an `href`).
|
|
195
|
+
- **Edit form.** `EditForm` edits a `select` field with a smrt-ui `Select`
|
|
196
|
+
(humanized choices, "Not set" unless required, an out-of-list stored value
|
|
197
|
+
kept). `derivedFields` render read-only and formatted with "Worked out
|
|
198
|
+
from the line items." and are never in the payload; `RecordScreen` passes
|
|
199
|
+
its `derivedFields` prop, else the totals role fields when an `items`
|
|
200
|
+
relation exists. `items?: Snippet` (the host's line-items editor) renders
|
|
201
|
+
under the form, in the edit column on desktop and inside the edit sheet on
|
|
202
|
+
phones.
|
|
203
|
+
- **`mode: 'edit'`** keeps the detail rendered and adds `EditForm`
|
|
204
|
+
(`showHeader={false}`, keyed by record id, `fieldErrors` / `error` /
|
|
205
|
+
`submitting` / `onsave` as `EditFormProps`): on desktop (`min-width: 48rem`,
|
|
206
|
+
a `matchMedia` the screen watches; the server renders desktop) a sticky
|
|
207
|
+
`<aside>` in a `minmax(0, 1fr) minmax(20rem, 28rem)` grid beside the header,
|
|
208
|
+
sliding in with `--smrt-duration-medium1` / `--smrt-easing-standard` and
|
|
209
|
+
still under `prefers-reduced-motion`; on phones a `PhoneSheet
|
|
210
|
+
variant="page"` in a fixed full-screen `<section>` the screen owns (an
|
|
211
|
+
`AdminShell`'s `overlays` snippet is a shell prop a screen cannot reach, so
|
|
212
|
+
the screen stacks its own at `--smrt-z-index-modal`). The sheet stays
|
|
213
|
+
mounted while closed so it animates open; the form inside mounts only in
|
|
214
|
+
edit mode, so Cancel never keeps a stale draft. Cancel, the sheet's close
|
|
215
|
+
button and Escape all call `oncancel`; the detail's Edit action hides while
|
|
216
|
+
editing. `RecipeScreens` maps `onedit` to `navigate('edit', id)` and
|
|
217
|
+
`oncancel` / a saved `onsave` back to `view`.
|
|
218
|
+
- Strings: `ui.screens.view_all`, `ui.screens.related`, `more_actions`,
|
|
219
|
+
`details`, `show_empty`, `derived_hint`, `select_none`, `items`, and the
|
|
220
|
+
existing `ui.screens.edit_title` for the aside and sheet heading.
|
|
221
|
+
- Tests: `__tests__/roles.test.ts`, `__tests__/record-header.test.ts`,
|
|
222
|
+
`__tests__/relations.test.ts`, `__tests__/RecordScreen.test.ts` (header
|
|
223
|
+
anatomy, overflow Delete, details exclusions, line items and the mismatch
|
|
224
|
+
flag, derived fields and the items slot on both layouts, axe
|
|
225
|
+
in view and both edit layouts, the record context reaching every loader,
|
|
226
|
+
"View all" hrefs, host-owned override edits, record change reload) and the
|
|
227
|
+
link cases in `__tests__/DetailScreen.test.ts`; `overview/__tests__/record.test.ts`
|
|
228
|
+
and the parent-context case in `overview/__tests__/load.test.ts`.
|
|
@@ -306,7 +306,7 @@ const inputLabel = $derived(label ?? t(M['ui.command_palette.input_label']));
|
|
|
306
306
|
display: flex;
|
|
307
307
|
align-items: center;
|
|
308
308
|
gap: 0.75rem;
|
|
309
|
-
min-block-size: max(2.5rem, var(--smrt-control-target-min
|
|
309
|
+
min-block-size: max(2.5rem, var(--smrt-control-target-min));
|
|
310
310
|
padding: 0.375rem 0.75rem;
|
|
311
311
|
border-radius: var(--smrt-radius-medium, 0.5rem);
|
|
312
312
|
color: var(--smrt-color-on-surface);
|
|
@@ -33,12 +33,17 @@ import type { OverviewOpResult } from './controller.svelte.js';
|
|
|
33
33
|
import type { OverviewGridProps } from './grid-types.js';
|
|
34
34
|
import { spanFromKey, spanFromPointer } from './resize.js';
|
|
35
35
|
import { defaultWidgetOptions } from './schema.js';
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
OverviewOptions,
|
|
38
|
+
OverviewWidget,
|
|
39
|
+
WidgetOptionIssue,
|
|
40
|
+
} from './types.js';
|
|
37
41
|
import WidgetOptionsForm from './WidgetOptionsForm.svelte';
|
|
38
42
|
|
|
39
43
|
let {
|
|
40
44
|
controller,
|
|
41
45
|
components,
|
|
46
|
+
optionEditors,
|
|
42
47
|
editing: editingProp,
|
|
43
48
|
label,
|
|
44
49
|
headingLevel = 2,
|
|
@@ -299,9 +304,7 @@ function chooseType(type: string): void {
|
|
|
299
304
|
}
|
|
300
305
|
}
|
|
301
306
|
|
|
302
|
-
function saveOptions(
|
|
303
|
-
options: OverviewOptions,
|
|
304
|
-
): { key: string; code: string }[] | null {
|
|
307
|
+
function saveOptions(options: OverviewOptions): WidgetOptionIssue[] | null {
|
|
305
308
|
const current = configure;
|
|
306
309
|
if (!current) return null;
|
|
307
310
|
const result: OverviewOpResult =
|
|
@@ -531,6 +534,7 @@ const headingTag = $derived(`h${headingLevel}`);
|
|
|
531
534
|
definition={configureDef}
|
|
532
535
|
initial={configure.initial}
|
|
533
536
|
{models}
|
|
537
|
+
optionsEditor={optionEditors?.get(configure.type) ?? configureDef.optionsEditor}
|
|
534
538
|
onsave={saveOptions}
|
|
535
539
|
oncancel={() => (configure = null)}
|
|
536
540
|
/>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OverviewGrid.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/OverviewGrid.svelte.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"OverviewGrid.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/OverviewGrid.svelte.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAgczD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
|
|
@@ -23,8 +23,14 @@ import { M } from '../../i18n/strings.overview.js';
|
|
|
23
23
|
import type { WidgetOptionsFormProps } from './grid-types.js';
|
|
24
24
|
import type { OverviewOptions, WidgetOptionField } from './types.js';
|
|
25
25
|
|
|
26
|
-
let {
|
|
27
|
-
|
|
26
|
+
let {
|
|
27
|
+
definition,
|
|
28
|
+
initial,
|
|
29
|
+
models,
|
|
30
|
+
onsave,
|
|
31
|
+
oncancel,
|
|
32
|
+
optionsEditor,
|
|
33
|
+
}: WidgetOptionsFormProps = $props();
|
|
28
34
|
|
|
29
35
|
const { t } = useI18n();
|
|
30
36
|
|
|
@@ -108,8 +114,12 @@ const optionalChoice = (field: WidgetOptionField): boolean =>
|
|
|
108
114
|
const modelValues = $derived(
|
|
109
115
|
new Set((models ?? []).map((model) => model.value)),
|
|
110
116
|
);
|
|
117
|
+
const CustomEditor = $derived(optionsEditor ?? definition.optionsEditor);
|
|
111
118
|
</script>
|
|
112
119
|
|
|
120
|
+
{#if CustomEditor}
|
|
121
|
+
<CustomEditor {definition} {initial} {models} {onsave} {oncancel} />
|
|
122
|
+
{:else}
|
|
113
123
|
<Form onsubmit={submit} novalidate>
|
|
114
124
|
{#if failed}
|
|
115
125
|
<Alert variant="error">{t(M['ui.overview.options_failed'])}</Alert>
|
|
@@ -184,6 +194,7 @@ const modelValues = $derived(
|
|
|
184
194
|
</Button>
|
|
185
195
|
</FormActionBar>
|
|
186
196
|
</Form>
|
|
197
|
+
{/if}
|
|
187
198
|
|
|
188
199
|
<style>
|
|
189
200
|
@layer smrt.reset, smrt.structure, smrt.theme;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WidgetOptionsForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/WidgetOptionsForm.svelte.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"WidgetOptionsForm.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/overview/WidgetOptionsForm.svelte.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAyK9D,QAAA,MAAM,iBAAiB,4DAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
3
|
-
|
|
4
|
-
/** ISO 4217 code for `money` (default USD). */
|
|
5
|
-
currency?: string;
|
|
6
|
-
locale: string;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Format a metric value. `money` takes integer minor units, `percent` takes a
|
|
10
|
-
* fraction (0.25 is 25%). Non-finite input renders as an en dash.
|
|
11
|
-
*/
|
|
12
|
-
export declare function formatWidgetValue(value: unknown, { format, currency, locale }: FormatWidgetValueOptions): string;
|
|
1
|
+
/** Shared number formatting; money values are integer minor units. */
|
|
2
|
+
export type { FormatWidgetValueOptions, WidgetValueFormat, } from '@happyvertical/smrt-ui/charts/format';
|
|
3
|
+
export { formatWidgetValue } from '@happyvertical/smrt-ui/charts/format';
|
|
13
4
|
//# sourceMappingURL=format.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/components/overview/format.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../../../src/components/overview/format.ts"],"names":[],"mappings":"AAAA,sEAAsE;AAEtE,YAAY,EACV,wBAAwB,EACxB,iBAAiB,GAClB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC"}
|
|
@@ -1,45 +1,2 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
* locale so a server render and the client hydrate to the same text. Money is
|
|
4
|
-
* integer minor units end to end (never a decimal in data).
|
|
5
|
-
*/
|
|
6
|
-
import { currencyMinorDigits } from '../screens/values.js';
|
|
7
|
-
const EN_DASH = '–';
|
|
8
|
-
function safeLocale(locale) {
|
|
9
|
-
try {
|
|
10
|
-
return Intl.getCanonicalLocales(locale)[0] ?? 'en';
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return 'en';
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Format a metric value. `money` takes integer minor units, `percent` takes a
|
|
18
|
-
* fraction (0.25 is 25%). Non-finite input renders as an en dash.
|
|
19
|
-
*/
|
|
20
|
-
export function formatWidgetValue(value, { format = 'integer', currency = 'USD', locale }) {
|
|
21
|
-
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
22
|
-
return EN_DASH;
|
|
23
|
-
const tag = safeLocale(locale);
|
|
24
|
-
switch (format) {
|
|
25
|
-
case 'money': {
|
|
26
|
-
const code = /^[A-Za-z]{3}$/.test(currency)
|
|
27
|
-
? currency.toUpperCase()
|
|
28
|
-
: 'USD';
|
|
29
|
-
const digits = currencyMinorDigits(code);
|
|
30
|
-
return new Intl.NumberFormat(tag, {
|
|
31
|
-
style: 'currency',
|
|
32
|
-
currency: code,
|
|
33
|
-
}).format(value / 10 ** digits);
|
|
34
|
-
}
|
|
35
|
-
case 'percent':
|
|
36
|
-
return new Intl.NumberFormat(tag, {
|
|
37
|
-
style: 'percent',
|
|
38
|
-
maximumFractionDigits: 1,
|
|
39
|
-
}).format(value);
|
|
40
|
-
case 'decimal':
|
|
41
|
-
return new Intl.NumberFormat(tag, { maximumFractionDigits: 2 }).format(value);
|
|
42
|
-
default:
|
|
43
|
-
return new Intl.NumberFormat(tag, { maximumFractionDigits: 0 }).format(value);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
1
|
+
/** Shared number formatting; money values are integer minor units. */
|
|
2
|
+
export { formatWidgetValue } from '@happyvertical/smrt-ui/charts/format';
|