@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.
Files changed (108) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +2 -2
  3. package/agents/overview-surfaces.md +117 -6
  4. package/agents/screens.md +171 -5
  5. package/dist/components/command-palette/CommandPaletteDialog.svelte +1 -1
  6. package/dist/components/overview/OverviewGrid.svelte +8 -4
  7. package/dist/components/overview/OverviewGrid.svelte.d.ts.map +1 -1
  8. package/dist/components/overview/WidgetOptionsForm.svelte +13 -2
  9. package/dist/components/overview/WidgetOptionsForm.svelte.d.ts.map +1 -1
  10. package/dist/components/overview/format.d.ts +3 -12
  11. package/dist/components/overview/format.d.ts.map +1 -1
  12. package/dist/components/overview/format.js +2 -45
  13. package/dist/components/overview/grid-types.d.ts +6 -11
  14. package/dist/components/overview/grid-types.d.ts.map +1 -1
  15. package/dist/components/overview/index.d.ts +5 -1
  16. package/dist/components/overview/index.d.ts.map +1 -1
  17. package/dist/components/overview/index.js +3 -0
  18. package/dist/components/overview/recipe-widgets.d.ts +6 -3
  19. package/dist/components/overview/recipe-widgets.d.ts.map +1 -1
  20. package/dist/components/overview/recipe-widgets.js +9 -0
  21. package/dist/components/overview/record.d.ts +51 -0
  22. package/dist/components/overview/record.d.ts.map +1 -0
  23. package/dist/components/overview/record.js +146 -0
  24. package/dist/components/overview/registry.d.ts +3 -0
  25. package/dist/components/overview/registry.d.ts.map +1 -1
  26. package/dist/components/overview/registry.js +25 -0
  27. package/dist/components/overview/server.d.ts +5 -1
  28. package/dist/components/overview/server.d.ts.map +1 -1
  29. package/dist/components/overview/server.js +5 -1
  30. package/dist/components/overview/types.d.ts +29 -0
  31. package/dist/components/overview/types.d.ts.map +1 -1
  32. package/dist/components/overview/widgets/ChartWidget.svelte +5 -265
  33. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts +1 -1
  34. package/dist/components/overview/widgets/ChartWidget.svelte.d.ts.map +1 -1
  35. package/dist/components/overview/widgets/LineItemsWidget.svelte +144 -0
  36. package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts +6 -0
  37. package/dist/components/overview/widgets/LineItemsWidget.svelte.d.ts.map +1 -0
  38. package/dist/components/overview/widgets/RecordListWidget.svelte +67 -3
  39. package/dist/components/overview/widgets/RecordListWidget.svelte.d.ts.map +1 -1
  40. package/dist/components/overview/widgets/RecordTable.svelte +167 -0
  41. package/dist/components/overview/widgets/RecordTable.svelte.d.ts +21 -0
  42. package/dist/components/overview/widgets/RecordTable.svelte.d.ts.map +1 -0
  43. package/dist/components/overview/widgets/RelatedWidget.svelte +117 -0
  44. package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts +6 -0
  45. package/dist/components/overview/widgets/RelatedWidget.svelte.d.ts.map +1 -0
  46. package/dist/components/overview/widgets/core.d.ts +13 -2
  47. package/dist/components/overview/widgets/core.d.ts.map +1 -1
  48. package/dist/components/overview/widgets/core.js +43 -2
  49. package/dist/components/overview/widgets/data.d.ts +97 -8
  50. package/dist/components/overview/widgets/data.d.ts.map +1 -1
  51. package/dist/components/overview/widgets/line-totals.d.ts +48 -0
  52. package/dist/components/overview/widgets/line-totals.d.ts.map +1 -0
  53. package/dist/components/overview/widgets/line-totals.js +91 -0
  54. package/dist/components/overview/widgets/record-loaders.d.ts +63 -0
  55. package/dist/components/overview/widgets/record-loaders.d.ts.map +1 -0
  56. package/dist/components/overview/widgets/record-loaders.js +150 -0
  57. package/dist/components/overview/widgets/record-table.d.ts +45 -0
  58. package/dist/components/overview/widgets/record-table.d.ts.map +1 -0
  59. package/dist/components/overview/widgets/record-table.js +201 -0
  60. package/dist/components/overview/widgets/record-widgets.d.ts +27 -0
  61. package/dist/components/overview/widgets/record-widgets.d.ts.map +1 -0
  62. package/dist/components/overview/widgets/record-widgets.js +100 -0
  63. package/dist/components/screens/DetailScreen.svelte +105 -6
  64. package/dist/components/screens/DetailScreen.svelte.d.ts.map +1 -1
  65. package/dist/components/screens/EditForm.svelte +39 -3
  66. package/dist/components/screens/EditForm.svelte.d.ts.map +1 -1
  67. package/dist/components/screens/RecipeScreens.svelte +18 -18
  68. package/dist/components/screens/RecipeScreens.svelte.d.ts.map +1 -1
  69. package/dist/components/screens/RecordHeader.svelte +238 -0
  70. package/dist/components/screens/RecordHeader.svelte.d.ts +26 -0
  71. package/dist/components/screens/RecordHeader.svelte.d.ts.map +1 -0
  72. package/dist/components/screens/RecordScreen.svelte +511 -0
  73. package/dist/components/screens/RecordScreen.svelte.d.ts +5 -0
  74. package/dist/components/screens/RecordScreen.svelte.d.ts.map +1 -0
  75. package/dist/components/screens/columns.d.ts +29 -0
  76. package/dist/components/screens/columns.d.ts.map +1 -0
  77. package/dist/components/screens/columns.js +115 -0
  78. package/dist/components/screens/fields.d.ts +12 -2
  79. package/dist/components/screens/fields.d.ts.map +1 -1
  80. package/dist/components/screens/fields.js +37 -5
  81. package/dist/components/screens/index.d.ts +7 -2
  82. package/dist/components/screens/index.d.ts.map +1 -1
  83. package/dist/components/screens/index.js +6 -1
  84. package/dist/components/screens/record-header.d.ts +51 -0
  85. package/dist/components/screens/record-header.d.ts.map +1 -0
  86. package/dist/components/screens/record-header.js +118 -0
  87. package/dist/components/screens/relations.d.ts +49 -0
  88. package/dist/components/screens/relations.d.ts.map +1 -0
  89. package/dist/components/screens/relations.js +98 -0
  90. package/dist/components/screens/roles.d.ts +56 -0
  91. package/dist/components/screens/roles.d.ts.map +1 -0
  92. package/dist/components/screens/roles.js +174 -0
  93. package/dist/components/screens/types.d.ts +182 -1
  94. package/dist/components/screens/types.d.ts.map +1 -1
  95. package/dist/components/screens/values.d.ts +23 -1
  96. package/dist/components/screens/values.d.ts.map +1 -1
  97. package/dist/components/screens/values.js +80 -4
  98. package/dist/components/workspace/admin-shell/AdminShell.svelte +4 -4
  99. package/dist/components/workspace/admin-shell/ShellIconButton.svelte +2 -2
  100. package/dist/components/workspace/admin-shell/TenantNav.svelte +8 -8
  101. package/dist/components/workspace/admin-shell/WorkspaceAccountMenu.svelte +3 -3
  102. package/dist/i18n/strings.overview.d.ts +25 -0
  103. package/dist/i18n/strings.overview.d.ts.map +1 -1
  104. package/dist/i18n/strings.overview.js +25 -0
  105. package/dist/i18n/strings.screens.d.ts +8 -0
  106. package/dist/i18n/strings.screens.d.ts.map +1 -1
  107. package/dist/i18n/strings.screens.js +8 -0
  108. 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`, and any host capabilities (index keys). It must
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. Relative or absolute specifiers
244
- are refused.
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`). Pages still confine what
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`, `DetailScreen` and `EditForm`, each of which can also be used on
12
- its own. Every screen takes a generated web `definition`
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 (`ui.widget: 'currency'` on an INTEGER field
38
- only) is integer minor units end to end. Editing shows major units but converts
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. Empty optional text becomes `null` when the field is nullable and `''`
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, 0px));
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 { OverviewOptions, OverviewWidget } from './types.js';
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;AA6bzD,QAAA,MAAM,YAAY,uDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,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 { definition, initial, models, onsave, oncancel }: WidgetOptionsFormProps =
27
- $props();
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;AA8J9D,QAAA,MAAM,iBAAiB,4DAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,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
- export type WidgetValueFormat = 'integer' | 'decimal' | 'percent' | 'money';
2
- export interface FormatWidgetValueOptions {
3
- format?: WidgetValueFormat;
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":"AAOA,MAAM,MAAM,iBAAiB,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5E,MAAM,WAAW,wBAAwB;IACvC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAYD;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,EAAE,MAAkB,EAAE,QAAgB,EAAE,MAAM,EAAE,EAAE,wBAAwB,GACzE,MAAM,CA4BR"}
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
- * Number formatting for overview widgets (#3727). Deterministic for a given
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';