@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.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 (222) hide show
  1. package/LICENSE +21 -57
  2. package/dist/app/action-form-body.d.ts +2 -2
  3. package/dist/app/action-form-body.js +12 -4
  4. package/dist/app/action-form-shim.d.ts +8 -8
  5. package/dist/app/action-form-shim.js +20 -22
  6. package/dist/app/action-form-support.d.ts +1 -0
  7. package/dist/app/action-form-support.js +1 -1
  8. package/dist/app/app-features-context.js +4 -4
  9. package/dist/app/column-renderers.d.ts +7 -9
  10. package/dist/app/column-renderers.js +15 -18
  11. package/dist/app/config-edit-body.js +7 -3
  12. package/dist/app/config-edit-shim.d.ts +7 -8
  13. package/dist/app/config-edit-shim.js +18 -20
  14. package/dist/app/custom-screens.d.ts +3 -3
  15. package/dist/app/custom-screens.js +8 -8
  16. package/dist/app/dashboard-body.d.ts +2 -2
  17. package/dist/app/dashboard-body.js +2 -2
  18. package/dist/app/entity-edit-prefill.d.ts +11 -0
  19. package/dist/app/entity-edit-prefill.js +18 -13
  20. package/dist/app/entity-edit-screen.js +4 -4
  21. package/dist/app/entity-list-screen.js +3 -3
  22. package/dist/app/extension-form-submit.d.ts +1 -1
  23. package/dist/app/extension-form-submit.js +14 -15
  24. package/dist/app/extension-sections.d.ts +29 -28
  25. package/dist/app/extension-sections.js +10 -10
  26. package/dist/app/feature-schema.js +11 -11
  27. package/dist/app/form-schema.js +2 -2
  28. package/dist/app/header-actions-bar.js +2 -2
  29. package/dist/app/kumiko-screen.js +8 -10
  30. package/dist/app/layout-fields.js +1 -1
  31. package/dist/app/list-facets.js +3 -3
  32. package/dist/app/nav.d.ts +10 -10
  33. package/dist/app/nav.js +13 -13
  34. package/dist/app/projection-detail-body.js +4 -4
  35. package/dist/app/projection-detail-shim.d.ts +6 -5
  36. package/dist/app/projection-detail-shim.js +23 -22
  37. package/dist/app/projection-list-body.js +10 -10
  38. package/dist/app/projection-list-shim.d.ts +5 -5
  39. package/dist/app/projection-list-shim.js +17 -18
  40. package/dist/app/qn.js +10 -12
  41. package/dist/app/qualify-screen-id.js +1 -1
  42. package/dist/app/screen-access.js +2 -2
  43. package/dist/app/screen-shared.js +1 -1
  44. package/dist/app/secret-mint-body.js +4 -4
  45. package/dist/app/write-failed-error.js +8 -7
  46. package/dist/components/edit-mounts.d.ts +2 -2
  47. package/dist/components/embedded-list-field.js +5 -5
  48. package/dist/components/grid-cell-for-field.d.ts +1 -1
  49. package/dist/components/reference-create-dialog.js +3 -3
  50. package/dist/components/related-list-section.d.ts +1 -1
  51. package/dist/components/related-list-section.js +15 -2
  52. package/dist/components/render-edit-action-button.d.ts +1 -3
  53. package/dist/components/render-edit-action-button.js +2 -2
  54. package/dist/components/render-edit-logic.js +1 -1
  55. package/dist/components/render-edit-types.d.ts +6 -6
  56. package/dist/components/render-edit.js +74 -73
  57. package/dist/components/render-field.d.ts +7 -7
  58. package/dist/components/render-field.js +62 -61
  59. package/dist/components/render-list.d.ts +42 -43
  60. package/dist/components/render-list.js +93 -84
  61. package/dist/components/use-wizard-form-draft.js +10 -10
  62. package/dist/context/dispatcher-context.js +5 -5
  63. package/dist/context/user-roles-context.js +2 -2
  64. package/dist/flow-diagram/render.js +9 -1
  65. package/dist/headless/apex/index.d.ts +1 -2
  66. package/dist/headless/apex/index.js +5 -6
  67. package/dist/headless/form/form-controller.js +14 -6
  68. package/dist/headless/form/types.d.ts +1 -0
  69. package/dist/headless/form/zod-bridge.js +2 -2
  70. package/dist/headless/format/index.js +89 -64
  71. package/dist/headless/format/money.js +1 -1
  72. package/dist/headless/index.js +2 -2
  73. package/dist/headless/view-model/edit.js +4 -2
  74. package/dist/headless/view-model/list.js +184 -150
  75. package/dist/headless/view-model/types.d.ts +6 -3
  76. package/dist/hooks/live-refetch-scheduler.js +4 -0
  77. package/dist/hooks/reference-limits.js +11 -12
  78. package/dist/hooks/use-list-url-state.d.ts +13 -13
  79. package/dist/hooks/use-list-url-state.js +21 -22
  80. package/dist/hooks/use-query.js +3 -3
  81. package/dist/hooks/use-reference-lookup.d.ts +9 -1
  82. package/dist/hooks/use-reference-lookup.js +32 -19
  83. package/dist/hooks/use-store.js +18 -22
  84. package/dist/hooks/use-stream-handler.js +4 -2
  85. package/dist/i18n-defaults.js +6 -8
  86. package/dist/i18n.d.ts +10 -10
  87. package/dist/i18n.js +25 -25
  88. package/dist/index.d.ts +1 -0
  89. package/dist/index.js +6 -4
  90. package/dist/live/dispatcher-live.js +2 -2
  91. package/dist/primitives.d.ts +244 -221
  92. package/dist/primitives.js +1 -1
  93. package/dist/sse/live-events.d.ts +5 -5
  94. package/dist/sse/live-events.js +17 -17
  95. package/dist/tokens.d.ts +8 -8
  96. package/dist/tokens.js +16 -17
  97. package/dist/web/app/browser-locale.d.ts +9 -9
  98. package/dist/web/app/browser-locale.js +19 -19
  99. package/dist/web/app/client-plugin.d.ts +44 -52
  100. package/dist/web/app/client-plugin.js +4 -5
  101. package/dist/web/app/create-app.d.ts +28 -31
  102. package/dist/web/app/create-app.js +29 -25
  103. package/dist/web/app/dashboard-body.js +17 -1
  104. package/dist/web/app/dashboard-panels.js +12 -2
  105. package/dist/web/app/document-lang-sync.js +1 -1
  106. package/dist/web/app/draft-storage.js +1 -1
  107. package/dist/web/app/nav-providers-context.d.ts +3 -3
  108. package/dist/web/app/nav-providers-context.js +3 -3
  109. package/dist/web/app/nav.d.ts +18 -17
  110. package/dist/web/app/nav.js +66 -64
  111. package/dist/web/app/rich-content-editor.js +1 -1
  112. package/dist/web/components/config-cascade.js +17 -18
  113. package/dist/web/components/config-source-badge.js +4 -4
  114. package/dist/web/index.d.ts +1 -1
  115. package/dist/web/index.js +1 -1
  116. package/dist/web/layout/app-layout.d.ts +9 -9
  117. package/dist/web/layout/app-layout.js +2 -2
  118. package/dist/web/layout/avatar.d.ts +4 -3
  119. package/dist/web/layout/avatar.js +15 -17
  120. package/dist/web/layout/default-app-shell.d.ts +22 -23
  121. package/dist/web/layout/editor-panel.js +3 -3
  122. package/dist/web/layout/nav-tree.js +86 -84
  123. package/dist/web/layout/profile-menu.d.ts +1 -1
  124. package/dist/web/layout/shell-header.js +2 -1
  125. package/dist/web/layout/sidebar-brand.d.ts +7 -7
  126. package/dist/web/layout/sidebar-user.d.ts +1 -1
  127. package/dist/web/layout/sidebar-user.js +4 -4
  128. package/dist/web/layout/sidebar.d.ts +8 -8
  129. package/dist/web/layout/sidebar.js +7 -7
  130. package/dist/web/layout/target-resolver-stub.d.ts +5 -5
  131. package/dist/web/layout/target-resolver-stub.js +20 -20
  132. package/dist/web/layout/target-url.d.ts +7 -7
  133. package/dist/web/layout/target-url.js +22 -22
  134. package/dist/web/layout/theme-toggle.js +4 -5
  135. package/dist/web/layout/uniform-bar-controls.d.ts +1 -0
  136. package/dist/web/layout/uniform-bar-controls.js +4 -0
  137. package/dist/web/layout/workspace-shell.d.ts +21 -21
  138. package/dist/web/layout/workspace-shell.js +29 -30
  139. package/dist/web/lib/cn.d.ts +4 -4
  140. package/dist/web/lib/cn.js +4 -4
  141. package/dist/web/lib/file-ref-download.js +4 -1
  142. package/dist/web/primitives/action-menu.d.ts +8 -8
  143. package/dist/web/primitives/action-menu.js +14 -13
  144. package/dist/web/primitives/calendar-popover.d.ts +1 -1
  145. package/dist/web/primitives/calendar-popover.js +10 -10
  146. package/dist/web/primitives/card-styles.js +1 -1
  147. package/dist/web/primitives/card.js +2 -2
  148. package/dist/web/primitives/code-input.d.ts +6 -0
  149. package/dist/web/primitives/code-input.js +31 -0
  150. package/dist/web/primitives/combobox.d.ts +12 -12
  151. package/dist/web/primitives/combobox.js +34 -35
  152. package/dist/web/primitives/data-table-cell.js +2 -2
  153. package/dist/web/primitives/data-table.js +6 -6
  154. package/dist/web/primitives/date-field.js +2 -2
  155. package/dist/web/primitives/date-input.js +4 -4
  156. package/dist/web/primitives/default-form.js +2 -2
  157. package/dist/web/primitives/dropdown-menu.js +14 -15
  158. package/dist/web/primitives/embedded-list-input.js +3 -3
  159. package/dist/web/primitives/facet-filter.js +2 -2
  160. package/dist/web/primitives/field.js +9 -8
  161. package/dist/web/primitives/file-thumbnail.d.ts +3 -0
  162. package/dist/web/primitives/file-thumbnail.js +5 -0
  163. package/dist/web/primitives/form.d.ts +1 -1
  164. package/dist/web/primitives/form.js +6 -6
  165. package/dist/web/primitives/input.js +8 -3
  166. package/dist/web/primitives/layout-elements.js +7 -5
  167. package/dist/web/primitives/layout.js +2 -2
  168. package/dist/web/primitives/located-timestamp-input.js +4 -4
  169. package/dist/web/primitives/money-input.js +13 -17
  170. package/dist/web/primitives/page-header.js +3 -1
  171. package/dist/web/primitives/pager.js +1 -1
  172. package/dist/web/primitives/row-actions.js +6 -7
  173. package/dist/web/primitives/secret-reveal.js +1 -1
  174. package/dist/web/primitives/timestamp-input.d.ts +9 -9
  175. package/dist/web/primitives/timestamp-input.js +21 -21
  176. package/dist/web/primitives/toast.js +12 -13
  177. package/dist/web/primitives/use-focus-on-mount.d.ts +2 -0
  178. package/dist/web/primitives/use-focus-on-mount.js +9 -0
  179. package/dist/web/sse/live-events.d.ts +6 -6
  180. package/dist/web/sse/live-events.js +3 -3
  181. package/dist/web/tokens.d.ts +11 -12
  182. package/dist/web/tokens.js +35 -37
  183. package/dist/web/version/update-checker.js +14 -13
  184. package/dist/web/widgets/ai-text-field.js +1 -1
  185. package/dist/web/widgets/bounded-load-cache.d.ts +1 -1
  186. package/dist/web/widgets/bounded-load-cache.js +10 -4
  187. package/dist/web/widgets/charts.d.ts +10 -10
  188. package/dist/web/widgets/charts.js +8 -8
  189. package/dist/web/widgets/choice-cards.d.ts +17 -0
  190. package/dist/web/widgets/choice-cards.js +12 -0
  191. package/dist/web/widgets/collapsible-section.d.ts +3 -3
  192. package/dist/web/widgets/collapsible-section.js +7 -7
  193. package/dist/web/widgets/detail-list.d.ts +5 -3
  194. package/dist/web/widgets/detail-list.js +5 -3
  195. package/dist/web/widgets/feed-list.d.ts +2 -2
  196. package/dist/web/widgets/feed-list.js +2 -2
  197. package/dist/web/widgets/form-fields.d.ts +14 -14
  198. package/dist/web/widgets/form-fields.js +14 -13
  199. package/dist/web/widgets/index.d.ts +1 -0
  200. package/dist/web/widgets/index.js +5 -4
  201. package/dist/web/widgets/infinity-list.js +3 -3
  202. package/dist/web/widgets/mode-switch.d.ts +2 -2
  203. package/dist/web/widgets/mode-switch.js +2 -2
  204. package/dist/web/widgets/pdf-preview.js +1 -1
  205. package/dist/web/widgets/plan-card.d.ts +2 -0
  206. package/dist/web/widgets/plan-card.js +1 -1
  207. package/dist/web/widgets/progress-list.d.ts +1 -2
  208. package/dist/web/widgets/progress-list.js +1 -2
  209. package/dist/web/widgets/query-table.d.ts +6 -6
  210. package/dist/web/widgets/result-panel.d.ts +16 -16
  211. package/dist/web/widgets/result-panel.js +10 -10
  212. package/dist/web/widgets/section-card.d.ts +3 -3
  213. package/dist/web/widgets/section-card.js +3 -3
  214. package/dist/web/widgets/stat.d.ts +4 -4
  215. package/dist/web/widgets/stat.js +6 -6
  216. package/dist/web/widgets/states.d.ts +4 -4
  217. package/dist/web/widgets/states.js +4 -4
  218. package/dist/web/widgets/upload-zone.js +13 -5
  219. package/dist/web/widgets/use-draft.d.ts +7 -7
  220. package/dist/web/widgets/use-draft.js +7 -7
  221. package/package.json +6 -6
  222. package/src/changes.json +49 -0
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
- /** Ergebnis-Sektion eines Rechners: SectionCard mit Empty-Zustand (Banner)
3
- * oder Kennzahl-Liste (DetailList) + optionalen Extras (Tabelle, Hinweise).
4
- * Ersetzt die handgebauten `<dl>`/Banner-Blöcke in Custom-Screens. */
2
+ /** Result section for a calculator: SectionCard with an empty state (Banner)
3
+ * or a metric list (DetailList) plus optional extras (table, notes).
4
+ * Replaces hand-built `<dl>`/banner blocks in custom screens. */
5
5
  export declare function ResultPanel({ title, subtitle, empty, emptyText, rows, footer, children, testId, }: {
6
6
  readonly title: string;
7
7
  readonly subtitle?: string;
@@ -12,7 +12,7 @@ export declare function ResultPanel({ title, subtitle, empty, emptyText, rows, f
12
12
  readonly value: ReactNode;
13
13
  readonly emphasize?: boolean;
14
14
  }[];
15
- /** Action-Slot am Karten-Fuß (z.B. „In Finanzierung übernehmen"). */
15
+ /** Action slot at the bottom of the card (e.g. "Move to financing"). */
16
16
  readonly footer?: ReactNode;
17
17
  readonly children?: ReactNode;
18
18
  readonly testId?: string;
@@ -27,18 +27,18 @@ export interface ResultTableFooterRow {
27
27
  readonly value: ReactNode;
28
28
  readonly emphasize?: boolean;
29
29
  }
30
- /** Statische, prop-getriebene Ergebnistabelle für berechnete Zeilen (Tranchen,
31
- * Szenarien). Nimmt dem Screen die `<table>`+tabular-nums-Ketten ab.
32
- * ponytail: bewusst die minimale Read-only-Tabelle — für Sort/Pager/Facets
33
- * stattdessen usePrimitives().DataTable bzw. QueryTable nutzen. */
30
+ /** Static, prop-driven result table for calculated rows (tranches, scenarios).
31
+ * Avoids repeating the `<table>`+tabular-nums pattern in each screen.
32
+ * ponytail: intentionally minimal and read-only—for sorting, paging, or facets,
33
+ * use usePrimitives().DataTable or QueryTable instead. */
34
34
  export declare function ResultTable<Row>({ columns, rows, rowKey, testId, className, card, footer, }: {
35
35
  readonly columns: readonly ResultColumn<Row>[];
36
36
  readonly rows: readonly Row[];
37
37
  readonly rowKey: (row: Row, index: number) => string;
38
38
  readonly testId?: string;
39
39
  readonly className?: string;
40
- /** Rahmen-Look wie die CRUD-Liste (DataTable): gerundeter Border-Container +
41
- * bg-muted-Header-Band. Default = bare (nur die Tabelle, ohne Container). */
40
+ /** Framed appearance like the CRUD list (DataTable): rounded border container +
41
+ * bg-muted header band. Default = bare (table only, without a container). */
42
42
  readonly card?: boolean;
43
43
  /** Summary rows (Subtotal/Tax/Total) rendered as real `<tr>`s in the same
44
44
  * `<table>` instead of a separate flex-div next to it — guarantees
@@ -49,12 +49,12 @@ export declare function ResultTable<Row>({ columns, rows, rowKey, testId, classN
49
49
  export interface ComparisonMetric<Col> {
50
50
  readonly label: string;
51
51
  readonly value: (col: Col, index: number) => ReactNode;
52
- /** Index der besten Spalte für diese Zeile (hervorgehoben); -1 = keine. */
52
+ /** Index of the best column for this row (highlighted); -1 = none. */
53
53
  readonly bestIndex?: (cols: readonly Col[]) => number;
54
54
  }
55
- /** Transponierte Vergleichstabelle: Zeile = Kennzahl, Spalte = Variante, je
56
- * Kennzahl optional die beste Spalte hervorgehoben. Für Szenario-/Angebots-
57
- * Vergleiche, wo ResultTable (Zeile=Datensatz) nicht passt. */
55
+ /** Transposed comparison table: row = metric, column = variant, with the best
56
+ * column optionally highlighted for each metric. For scenario/offer comparisons
57
+ * where ResultTable (row = record) is not suitable. */
58
58
  export declare function ComparisonTable<Col>({ columns, columnHeader, columnKey, metrics, metricLabel, testId, className, card, }: {
59
59
  readonly columns: readonly Col[];
60
60
  readonly columnHeader: (col: Col, index: number) => string;
@@ -63,7 +63,7 @@ export declare function ComparisonTable<Col>({ columns, columnHeader, columnKey,
63
63
  readonly metricLabel: string;
64
64
  readonly testId?: string;
65
65
  readonly className?: string;
66
- /** Rahmen-Look wie die CRUD-Liste (DataTable): gerundeter Border-Container +
67
- * bg-muted-Header-Band. Default = bare. */
66
+ /** Framed appearance like the CRUD list (DataTable): rounded border container +
67
+ * bg-muted header band. Default = bare. */
68
68
  readonly card?: boolean;
69
69
  }): ReactNode;
@@ -3,24 +3,24 @@ import { usePrimitives } from "../../index.js";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { DetailList } from "./detail-list.js";
5
5
  import { SectionCard } from "./section-card.js";
6
- /** Ergebnis-Sektion eines Rechners: SectionCard mit Empty-Zustand (Banner)
7
- * oder Kennzahl-Liste (DetailList) + optionalen Extras (Tabelle, Hinweise).
8
- * Ersetzt die handgebauten `<dl>`/Banner-Blöcke in Custom-Screens. */
6
+ /** Result section for a calculator: SectionCard with an empty state (Banner)
7
+ * or a metric list (DetailList) plus optional extras (table, notes).
8
+ * Replaces hand-built `<dl>`/banner blocks in custom screens. */
9
9
  export function ResultPanel({ title, subtitle, empty, emptyText, rows, footer, children, testId, }) {
10
10
  const { Banner } = usePrimitives();
11
11
  return (_jsx(SectionCard, { title: title, subtitle: subtitle, footer: footer, testId: testId, children: empty === true ? (_jsx(Banner, { variant: "info", padded: true, children: emptyText })) : (_jsxs("div", { className: "flex flex-col gap-4", children: [rows !== undefined && rows.length > 0 && _jsx(DetailList, { rows: rows }), children] })) }));
12
12
  }
13
- /** Statische, prop-getriebene Ergebnistabelle für berechnete Zeilen (Tranchen,
14
- * Szenarien). Nimmt dem Screen die `<table>`+tabular-nums-Ketten ab.
15
- * ponytail: bewusst die minimale Read-only-Tabelle — für Sort/Pager/Facets
16
- * stattdessen usePrimitives().DataTable bzw. QueryTable nutzen. */
13
+ /** Static, prop-driven result table for calculated rows (tranches, scenarios).
14
+ * Avoids repeating the `<table>`+tabular-nums pattern in each screen.
15
+ * ponytail: intentionally minimal and read-only—for sorting, paging, or facets,
16
+ * use usePrimitives().DataTable or QueryTable instead. */
17
17
  export function ResultTable({ columns, rows, rowKey, testId, className, card, footer, }) {
18
18
  const table = (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { "data-testid": testId, className: cn("w-full text-sm", className), children: [_jsx("thead", { className: cn(card === true && "bg-muted"), children: _jsx("tr", { className: "border-b text-left text-muted-foreground", children: columns.map((col) => (_jsx("th", { className: cn("py-1.5 font-medium", card === true && "px-3", col.align === "right" && "text-right"), children: col.header }, col.header))) }) }), _jsxs("tbody", { children: [rows.map((row, i) => (_jsx("tr", { className: "border-b last:border-0", children: columns.map((col) => (_jsx("td", { className: cn("py-1.5", card === true && "px-3", col.align === "right" && "text-right tabular-nums"), children: col.cell(row) }, col.header))) }, rowKey(row, i)))), footer?.map((row, i) => (_jsxs("tr", { className: cn(i === 0 && "border-t", row.emphasize === true && "font-semibold"), children: [_jsxs("td", { colSpan: columns.length > 1 ? Math.max(columns.length - 1, 1) : undefined, className: cn("py-1.5 text-muted-foreground", card === true && "px-3", row.emphasize === true && "text-foreground", columns.length === 1 && "flex items-center justify-between gap-4"), children: [row.label, columns.length === 1 && (_jsx("span", { className: cn("text-right tabular-nums", row.emphasize === true && "text-foreground"), children: row.value }))] }), columns.length > 1 && (_jsx("td", { className: cn("py-1.5 text-right tabular-nums", card === true && "px-3"), children: row.value }))] }, row.label)))] })] }) }));
19
19
  return card === true ? (_jsx("div", { className: "overflow-hidden rounded-lg border bg-card", children: table })) : (table);
20
20
  }
21
- /** Transponierte Vergleichstabelle: Zeile = Kennzahl, Spalte = Variante, je
22
- * Kennzahl optional die beste Spalte hervorgehoben. Für Szenario-/Angebots-
23
- * Vergleiche, wo ResultTable (Zeile=Datensatz) nicht passt. */
21
+ /** Transposed comparison table: row = metric, column = variant, with the best
22
+ * column optionally highlighted for each metric. For scenario/offer comparisons
23
+ * where ResultTable (row = record) is not suitable. */
24
24
  export function ComparisonTable({ columns, columnHeader, columnKey, metrics, metricLabel, testId, className, card, }) {
25
25
  const table = (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { "data-testid": testId, className: cn("w-full min-w-[24rem] text-sm", className), children: [_jsx("thead", { className: cn(card === true && "bg-muted"), children: _jsxs("tr", { className: "border-b text-left text-muted-foreground", children: [_jsx("th", { className: cn("py-1.5 font-medium", card === true && "px-3"), children: metricLabel }), columns.map((col, i) => (_jsx("th", { className: cn("py-1.5 text-right font-medium", card === true && "px-3"), children: columnHeader(col, i) }, columnKey(col, i))))] }) }), _jsx("tbody", { children: metrics.map((metric) => {
26
26
  const best = metric.bestIndex !== undefined ? metric.bestIndex(columns) : -1;
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
- /** Sektion mit Titel + optionalem Action-Slot (Range-Umschalter, Filter) —
3
- * ersetzt die wiederholten `<section className={CARD}>…<h2>`-Blöcke in
4
- * Custom-Screens. Dünner Wrapper über das Card-Primitive. */
2
+ /** Section with a title and optional action slot (range switcher, filter) —
3
+ * replaces repeated `<section className={CARD}>…<h2>` blocks in
4
+ * custom screens. Thin wrapper around the Card primitive. */
5
5
  export declare function SectionCard({ title, subtitle, action, footer, children, testId, }: {
6
6
  readonly title: string;
7
7
  readonly subtitle?: string;
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { usePrimitives } from "../../index.js";
3
- /** Sektion mit Titel + optionalem Action-Slot (Range-Umschalter, Filter) —
4
- * ersetzt die wiederholten `<section className={CARD}>…<h2>`-Blöcke in
5
- * Custom-Screens. Dünner Wrapper über das Card-Primitive. */
3
+ /** Section with a title and optional action slot (range switcher, filter) —
4
+ * replaces repeated `<section className={CARD}>…<h2>` blocks in
5
+ * custom screens. Thin wrapper around the Card primitive. */
6
6
  export function SectionCard({ title, subtitle, action, footer, children, testId, }) {
7
7
  const { Card } = usePrimitives();
8
8
  return (_jsx(Card, { slots: { title, subtitle, headerActions: action, footer }, testId: testId, children: _jsx("div", { className: "flex h-full flex-col gap-4", children: children }) }));
@@ -10,8 +10,8 @@ export declare function Sparkline({ points, className, }: {
10
10
  readonly points: readonly number[];
11
11
  readonly className?: string;
12
12
  }): ReactNode;
13
- /** Icon-tragende Kennzahl-Kachel mit optionalem Delta-Chip, Trend-Zeile
14
- * und Sparkline. `icon` ist ein fertiger Knoten (App liefert ihr SVG). */
13
+ /** Icon-bearing metric tile with an optional delta chip, trend line,
14
+ * and sparkline. `icon` is a ready-made node (the app supplies its SVG). */
15
15
  export declare function StatCard({ icon, label, value, sub, tone, accentColor, delta, trend, spark, testId, children, }: {
16
16
  readonly icon?: ReactNode;
17
17
  readonly label: string;
@@ -41,8 +41,8 @@ export declare function StatStripCell({ icon, label, value, sub, tone, accentCol
41
41
  readonly spark?: readonly number[];
42
42
  readonly testId?: string;
43
43
  }): ReactNode;
44
- /** Kompakte Kennzahl-Kachel (ohne Icon) — für dichte KPI-Raster.
45
- * StatCard ist die icon-tragende Variante. */
44
+ /** Compact metric tile (without an icon) — for dense KPI grids.
45
+ * StatCard is the icon-bearing variant. */
46
46
  export declare function MiniStat({ label, value, tone, emphasize, testId, labelTestId, valueTestId, }: {
47
47
  readonly label: string;
48
48
  readonly value: string;
@@ -15,8 +15,8 @@ const TONE_VALUE = {
15
15
  warn: "text-destructive",
16
16
  negative: "text-status-bad",
17
17
  };
18
- // Inline-SVG-Sparkline (kein Chart-Dep für eine 28px-Kurve): Linie +
19
- // schwacher Flächen-Verlauf, Farbe via currentColor (Caller setzt text-tone).
18
+ // Inline SVG sparkline (no chart dependency for a 28px curve): line +
19
+ // subtle area gradient, colored via currentColor (caller sets text-tone).
20
20
  export function Sparkline({ points, className, }) {
21
21
  if (points.length < 2)
22
22
  return null;
@@ -42,8 +42,8 @@ function StatIconChip({ icon, tone, accentColor, }) {
42
42
  backgroundColor: `color-mix(in srgb, ${accentColor} 12%, transparent)`,
43
43
  }, children: icon }));
44
44
  }
45
- /** Icon-tragende Kennzahl-Kachel mit optionalem Delta-Chip, Trend-Zeile
46
- * und Sparkline. `icon` ist ein fertiger Knoten (App liefert ihr SVG). */
45
+ /** Icon-bearing metric tile with an optional delta chip, trend line,
46
+ * and sparkline. `icon` is a ready-made node (the app supplies its SVG). */
47
47
  export function StatCard({ icon, label, value, sub, tone = "default", accentColor, delta, trend, spark, testId, children, }) {
48
48
  const { Card } = usePrimitives();
49
49
  // StatCard is a reusable small-tile widget (a metrics dashboard grid, not
@@ -57,8 +57,8 @@ export function StatCard({ icon, label, value, sub, tone = "default", accentColo
57
57
  export function StatStripCell({ icon, label, value, sub, tone = "default", accentColor, delta, spark, testId, }) {
58
58
  return (_jsxs("div", { "data-testid": testId, className: "flex min-w-0 flex-col gap-1 px-4 py-3", children: [_jsxs("div", { className: "flex flex-wrap items-center justify-between gap-x-2 gap-y-1", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2 text-xs font-medium text-muted-foreground", children: [icon !== undefined && _jsx(StatIconChip, { icon: icon, tone: tone, accentColor: accentColor }), _jsx("span", { children: label })] }), delta !== undefined && (_jsxs("span", { className: cn("inline-flex shrink-0 items-center gap-0.5 whitespace-nowrap rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums", TONE_CHIP[delta.tone ?? tone]), children: [delta.direction === "up" ? "↑" : "↓", delta.value] }))] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: cn("text-xl font-semibold tabular-nums", TONE_VALUE[tone]), children: value }), spark !== undefined && (_jsx(Sparkline, { points: spark, className: cn("h-7 w-20 shrink-0", TONE_VALUE[tone]) }))] }), sub !== undefined && _jsx("div", { className: "text-xs text-muted-foreground", children: sub })] }));
59
59
  }
60
- /** Kompakte Kennzahl-Kachel (ohne Icon) — für dichte KPI-Raster.
61
- * StatCard ist die icon-tragende Variante. */
60
+ /** Compact metric tile (without an icon) — for dense KPI grids.
61
+ * StatCard is the icon-bearing variant. */
62
62
  export function MiniStat({ label, value, tone = "default", emphasize = false, testId, labelTestId, valueTestId, }) {
63
63
  const { Card } = usePrimitives();
64
64
  return (_jsxs(Card, { options: { padded: false }, className: cn("p-3", emphasize && "ring-1 ring-primary/30"), testId: testId, children: [_jsx("div", { className: "text-xs text-muted-foreground", "data-testid": labelTestId, children: label }), _jsx("div", { className: cn("mt-0.5 font-semibold tabular-nums", TONE_VALUE[tone], emphasize ? "text-lg" : "text-sm"), "data-testid": valueTestId, children: value })] }));
@@ -1,7 +1,7 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { DispatcherError } from "../../headless/index.ts";
3
- /** Leerer-Zustand mit optionalem Icon + CTA — die dashed-Box-Optik der
4
- * entityList-Empty-States, als Standalone-Widget für Custom-Screens. */
3
+ /** Empty state with an optional icon and CTA, using the dashed-box style of
4
+ * entityList empty states as a standalone widget for custom screens. */
5
5
  export declare function EmptyState({ icon, title, description, action, testId, }: {
6
6
  readonly icon?: ReactNode;
7
7
  readonly title: string;
@@ -9,13 +9,13 @@ export declare function EmptyState({ icon, title, description, action, testId, }
9
9
  readonly action?: ReactNode;
10
10
  readonly testId?: string;
11
11
  }): ReactNode;
12
- /** Lade-Zustand: Skeleton-Zeilen in Card-Optik. `rows` steuert die Höhe. */
12
+ /** Loading state with skeleton rows styled like cards; `rows` controls the height. */
13
13
  export declare function LoadingState({ rows, className, testId, }: {
14
14
  readonly rows?: number;
15
15
  readonly className?: string;
16
16
  readonly testId?: string;
17
17
  }): ReactNode;
18
- /** Fehler-Zustand auf Basis des Banner-Primitives, mit Retry-Button. */
18
+ /** Error state based on the Banner primitive, with a retry button. */
19
19
  export declare function ErrorState({ error, onRetry, testId, }: {
20
20
  readonly error: DispatcherError;
21
21
  readonly onRetry?: () => void;
@@ -2,17 +2,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { dispatcherErrorText, usePrimitives, useTranslation } from "../../index.js";
3
3
  import { cn } from "../lib/cn.js";
4
4
  import { Skeleton } from "../ui/skeleton.js";
5
- /** Leerer-Zustand mit optionalem Icon + CTA — die dashed-Box-Optik der
6
- * entityList-Empty-States, als Standalone-Widget für Custom-Screens. */
5
+ /** Empty state with an optional icon and CTA, using the dashed-box style of
6
+ * entityList empty states as a standalone widget for custom screens. */
7
7
  export function EmptyState({ icon, title, description, action, testId, }) {
8
8
  return (_jsxs("div", { "data-testid": testId, className: "flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-6 py-10 text-center", children: [icon !== undefined && _jsx("div", { className: "text-muted-foreground", children: icon }), _jsx("div", { className: "text-sm font-medium", children: title }), description !== undefined && (_jsx("div", { className: "text-sm text-muted-foreground", children: description })), action !== undefined && _jsx("div", { className: "mt-2", children: action })] }));
9
9
  }
10
- /** Lade-Zustand: Skeleton-Zeilen in Card-Optik. `rows` steuert die Höhe. */
10
+ /** Loading state with skeleton rows styled like cards; `rows` controls the height. */
11
11
  export function LoadingState({ rows = 3, className, testId, }) {
12
12
  const t = useTranslation();
13
13
  return (_jsx("output", { "data-testid": testId, "aria-label": t("kumiko.widget.loading"), className: cn("flex w-full flex-col gap-2", className), children: Array.from({ length: rows }, (_, i) => (_jsx(Skeleton, { className: "h-9 w-full" }, i))) }));
14
14
  }
15
- /** Fehler-Zustand auf Basis des Banner-Primitives, mit Retry-Button. */
15
+ /** Error state based on the Banner primitive, with a retry button. */
16
16
  export function ErrorState({ error, onRetry, testId, }) {
17
17
  const { Banner, Button } = usePrimitives();
18
18
  const t = useTranslation();
@@ -114,6 +114,18 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
114
114
  if (row.value !== undefined)
115
115
  onRemove?.(row.value);
116
116
  }
117
+ // Values the owner already held before any upload of this zone (editing a record with files).
118
+ // Hidden while an upload runs: its value reaches `doneValues` one render before its row is done.
119
+ const uploading = rows.some((row) => row.status === "uploading");
120
+ const existingRows = doneValues === undefined || uploading
121
+ ? []
122
+ : doneValues
123
+ .filter((value) => !rows.some((row) => row.value === value))
124
+ .map((value) => ({ id: `existing:${value}`, fileName: value, status: "done", value }));
125
+ const visibleRows = [
126
+ ...existingRows,
127
+ ...rows.filter((row) => row.value === undefined || doneValues === undefined || doneValues.includes(row.value)),
128
+ ];
117
129
  const acceptAttr = toAcceptAttr(accept);
118
130
  return (_jsxs("div", { "data-testid": testId, className: "flex flex-col gap-4", children: [_jsxs("label", { htmlFor: inputId, onDragOver: (e) => {
119
131
  e.preventDefault();
@@ -123,9 +135,5 @@ export function UploadZone({ onUpload, title, hint, accept, multiple = true, cap
123
135
  if (!(e.relatedTarget instanceof Node) || !e.currentTarget.contains(e.relatedTarget)) {
124
136
  setDragOver(false);
125
137
  }
126
- }, onDrop: handleDrop, "data-testid": testId !== undefined ? `${testId}-dropzone` : undefined, className: cn("flex w-full cursor-pointer flex-col items-center gap-3 rounded-lg border-2 border-dashed p-10 text-center transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2", dragOver ? "border-primary bg-muted/40" : "border-border", disabled === true && "cursor-not-allowed opacity-50"), children: [_jsx(Upload, { className: "size-8 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "text-sm", children: title }), hint !== undefined && _jsx("span", { className: "text-sm text-muted-foreground", children: hint }), _jsx("input", { ref: inputRef, id: inputId, type: "file", multiple: multiple, className: "sr-only", "data-testid": testId !== undefined ? `${testId}-input` : undefined, disabled: disabled, ...(acceptAttr !== undefined && { accept: acceptAttr }), ...(capture !== undefined && { capture }), onChange: (e) => void uploadFiles(e.target.files) })] }), rows.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: rows
127
- .filter((row) => row.value === undefined ||
128
- doneValues === undefined ||
129
- doneValues.includes(row.value))
130
- .map((row) => (_jsxs("li", { "data-testid": testId !== undefined ? `${testId}-row` : undefined, className: "flex items-center justify-between gap-3 rounded-md border border-border px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(FileUp, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "truncate text-sm", children: row.fileName })] }), _jsxs("output", { className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground", children: [STATUS_ICON[row.status], _jsx("span", { className: "sr-only", children: t(STATUS_LABEL_KEY[row.status]) }), row.status === "error" && row.error !== undefined && _jsx("span", { children: row.error }), row.value !== undefined && onRemove !== undefined && disabled !== true && (_jsx("button", { type: "button", className: "rounded-sm p-0.5 text-muted-foreground hover:text-foreground", "aria-label": t("kumiko.widget.upload.remove"), "data-testid": testId !== undefined ? `${testId}-remove` : undefined, onClick: () => removeRow(row), children: _jsx(X, { className: "size-3.5", "aria-hidden": "true" }) }))] })] }, row.id))) }))] }));
138
+ }, onDrop: handleDrop, "data-testid": testId !== undefined ? `${testId}-dropzone` : undefined, className: cn("flex w-full cursor-pointer flex-col items-center gap-3 rounded-lg border-2 border-dashed p-10 text-center transition-colors has-[:focus-visible]:ring-2 has-[:focus-visible]:ring-ring has-[:focus-visible]:ring-offset-2", dragOver ? "border-primary bg-muted/40" : "border-border", disabled === true && "cursor-not-allowed opacity-50"), children: [_jsx(Upload, { className: "size-8 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "text-sm", children: title }), hint !== undefined && _jsx("span", { className: "text-sm text-muted-foreground", children: hint }), _jsx("input", { ref: inputRef, id: inputId, type: "file", multiple: multiple, className: "sr-only", "data-testid": testId !== undefined ? `${testId}-input` : undefined, disabled: disabled, ...(acceptAttr !== undefined && { accept: acceptAttr }), ...(capture !== undefined && { capture }), onChange: (e) => void uploadFiles(e.target.files) })] }), visibleRows.length > 0 && (_jsx("ul", { className: "flex flex-col gap-2", children: visibleRows.map((row) => (_jsxs("li", { "data-testid": testId !== undefined ? `${testId}-row` : undefined, className: "flex items-center justify-between gap-3 rounded-md border border-border px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(FileUp, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }), _jsx("span", { className: "truncate text-sm", children: row.fileName })] }), _jsxs("output", { className: "flex shrink-0 items-center gap-1.5 text-xs text-muted-foreground", children: [STATUS_ICON[row.status], _jsx("span", { className: "sr-only", children: t(STATUS_LABEL_KEY[row.status]) }), row.status === "error" && row.error !== undefined && _jsx("span", { children: row.error }), row.value !== undefined && onRemove !== undefined && disabled !== true && (_jsx("button", { type: "button", className: "rounded-sm p-0.5 text-muted-foreground hover:text-foreground", "aria-label": t("kumiko.widget.upload.remove"), "data-testid": testId !== undefined ? `${testId}-remove` : undefined, onClick: () => removeRow(row), children: _jsx(X, { className: "size-3.5", "aria-hidden": "true" }) }))] })] }, row.id))) }))] }));
131
139
  }
@@ -1,10 +1,10 @@
1
- /** Formular-State für Rechner-Screens: ein Draft-Objekt + `patch` für
2
- * Teil-Updates + `field(name)` das die `{ id, name, value, onChange }`-Props
3
- * fertig für die Feld-Widgets (NumberField/MoneyField/PercentField) liefert.
4
- * Ersetzt das pro Screen wiederholte Draft-Interface + patch-Helper.
5
- * `idPrefix` verhindert doppelte DOM-`id`s, wenn mehrere `useDraft`-Formulare
6
- * mit gleichnamigen Feldern auf derselben Seite gemountet werden — `name`
7
- * bleibt der reine Key fürs Form-Payload. */
1
+ /** Form state for calculator screens: a draft object plus `patch` for
2
+ * partial updates, and `field(name)` that provides `{ id, name, value, onChange }`
3
+ * props ready for field widgets (NumberField/MoneyField/PercentField).
4
+ * Replaces the repeated draft interface and patch helper in each screen.
5
+ * `idPrefix` prevents duplicate DOM `id`s when multiple `useDraft` forms with
6
+ * same-named fields are mounted on the same page; `name` remains the plain key
7
+ * for the form payload. */
8
8
  export declare function useDraft<T extends object>(defaults: T, options?: {
9
9
  readonly idPrefix?: string;
10
10
  }): {
@@ -1,11 +1,11 @@
1
1
  import { useCallback, useState } from "react";
2
- /** Formular-State für Rechner-Screens: ein Draft-Objekt + `patch` für
3
- * Teil-Updates + `field(name)` das die `{ id, name, value, onChange }`-Props
4
- * fertig für die Feld-Widgets (NumberField/MoneyField/PercentField) liefert.
5
- * Ersetzt das pro Screen wiederholte Draft-Interface + patch-Helper.
6
- * `idPrefix` verhindert doppelte DOM-`id`s, wenn mehrere `useDraft`-Formulare
7
- * mit gleichnamigen Feldern auf derselben Seite gemountet werden — `name`
8
- * bleibt der reine Key fürs Form-Payload. */
2
+ /** Form state for calculator screens: a draft object plus `patch` for
3
+ * partial updates, and `field(name)` that provides `{ id, name, value, onChange }`
4
+ * props ready for field widgets (NumberField/MoneyField/PercentField).
5
+ * Replaces the repeated draft interface and patch helper in each screen.
6
+ * `idPrefix` prevents duplicate DOM `id`s when multiple `useDraft` forms with
7
+ * same-named fields are mounted on the same page; `name` remains the plain key
8
+ * for the form payload. */
9
9
  export function useDraft(defaults, options) {
10
10
  const idPrefix = options?.idPrefix;
11
11
  const [draft, setDraft] = useState(defaults);
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-renderer",
3
- "version": "0.386.0",
3
+ "version": "0.388.0",
4
4
  "description": "React renderer for Kumiko screens. The root export holds the platform-agnostic logic (primitives contract, hooks, KumikoScreen, navigation and SSE abstractions); ./headless the DOM-free UI logic, ./live the HTTP dispatcher, ./web the browser bindings (HTML primitives, history navigation, EventSource live events, createKumikoApp).",
5
- "license": "BUSL-1.1",
5
+ "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
7
7
  "type": "module",
8
8
  "kumiko": {
@@ -48,8 +48,8 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@cosmicdrift/kumiko-framework": "0.386.0",
52
- "@cosmicdrift/kumiko-types": "0.386.0",
51
+ "@cosmicdrift/kumiko-framework": "0.388.0",
52
+ "@cosmicdrift/kumiko-types": "0.388.0",
53
53
  "@radix-ui/react-dialog": "^1.1.15",
54
54
  "@radix-ui/react-dropdown-menu": "^2.1.16",
55
55
  "@radix-ui/react-label": "^2.1.8",
@@ -92,8 +92,8 @@
92
92
  }
93
93
  },
94
94
  "devDependencies": {
95
- "@cosmicdrift/kumiko-locale-de": "0.386.0",
96
- "@cosmicdrift/kumiko-testing": "0.386.0",
95
+ "@cosmicdrift/kumiko-locale-de": "0.388.0",
96
+ "@cosmicdrift/kumiko-testing": "0.388.0",
97
97
  "@tailwindcss/cli": "^4.3.0",
98
98
  "@testing-library/react": "^16.3.2",
99
99
  "@testing-library/user-event": "^14.6.1",
package/src/changes.json CHANGED
@@ -1,4 +1,53 @@
1
1
  [
2
+ {
3
+ "version": "0.388.0",
4
+ "type": "improvement",
5
+ "title": "List view model and cell formats as tables",
6
+ "detail": "computeListViewModel resolves each column through an ordered list of resolvers and applyFormatSpec dispatches through a table of cell formatters. Output is unchanged; a format named like an Object.prototype member such as \"toString\" still takes the unknown-format path."
7
+ },
8
+ {
9
+ "version": "0.388.0",
10
+ "type": "fix",
11
+ "title": "Single-line row actions, stable form columns and select width, system-scope configEdit",
12
+ "detail": "Row actions never wrap onto a second line; the template overview lets long titles break and cuts the slug earlier so the table keeps room at 1280 px. In screen forms a section with explicitly declared columns (two or more) lays out as that many columns, and a select cell is as wide as a text cell instead of growing with its help text. A configEdit screen with scope system now reads the system level, so a SystemAdmin no longer sees the value of their own tenant row."
13
+ },
14
+ {
15
+ "version": "0.388.0",
16
+ "type": "fix",
17
+ "title": "Uniform control height in page headers and list toolbars, spacing for projectionList descriptions",
18
+ "detail": "Buttons in the page header (create, toolbarActions, detail actions, form actions and app-added buttons such as the AI button) and in list toolbars (search, filters, secondary actions) now share one height, independent of the size a screen or app requested. A toolbarAction promoted into the page header no longer renders smaller than the standard create button. A projectionList without a toolbar no longer glues its description to the page header. The header of a CollapsibleSection (\"Erweitert\") has the same height as the form controls next to it."
19
+ },
20
+ {
21
+ "version": "0.388.0",
22
+ "type": "breaking",
23
+ "title": "Input gets kind \"code\" and autoFocus on kind \"text\"",
24
+ "detail": "Input has a new kind \"code\" (length, onComplete, autoFocus, autoComplete defaulting to one-time-code), a single real input shown as separate cells; onComplete fires once when input, paste or autofill reaches full length and the value holds digits only. kind \"text\" gains autoFocus.",
25
+ "migration": "custom Input implementations (PrimitivesProvider override) with an exhaustive switch over props.kind need a case \"code\"; delegating to DefaultInput from @cosmicdrift/kumiko-renderer/web is enough"
26
+ },
27
+ {
28
+ "version": "0.388.0",
29
+ "type": "fix",
30
+ "title": "Renderer low-severity robustness fixes",
31
+ "detail": "The list search echo handling no longer mutates state during render and no longer re-sends an already sent value. A failing live refetch no longer drops a queued follow-up run. Downloaded blob URLs are revoked after a delay, failed PDF loads are retried on the next mount, the file meta cache is cleared when the app gate unmounts, and renderFlow rejects duplicate node ids and unknown icons. The es/fr/it titles of the reset-to-system action now say the tenant template is reset to the system template."
32
+ },
33
+ {
34
+ "version": "0.388.0",
35
+ "type": "fix",
36
+ "title": "Renderer review fixes for lookups, dropzone, markSaved, actionForm reset and dashboard gates",
37
+ "detail": "Reference labels show \"Unknown\" only when the lookup page was complete; a related-list column keeps sortable on fields without a sortable key; a files dropzone lists and can remove files already in the value; extension markSaved accepts the written field names, no longer clears other sections' dirty state; an actionForm with load and resetOnSuccess reloads the record before resetting; fallback panels no longer flash before the visibility gate answers; identical text blocks get unique keys; the missing-screen warning names the route id."
38
+ },
39
+ {
40
+ "version": "0.388.0",
41
+ "type": "improvement",
42
+ "title": "useTenantCurrency hook",
43
+ "detail": "The tenant currency resolution of the entity edit prefill is exported as a hook with an explicit fallback for errors and missing values and no query when no currency is needed; the mail preview uses it only for templates with a money variable."
44
+ },
45
+ {
46
+ "version": "0.388.0",
47
+ "type": "improvement",
48
+ "title": "ChoiceCards widget and PlanCard price prefix",
49
+ "detail": "ChoiceCards is a card-styled radio group (title, description, aside, native radios, keyboard operable) for exclusive choices that need more than a segment label. PlanCardPrice takes an optional prefix shown before the amount, for example \"from\"."
50
+ },
2
51
  {
3
52
  "version": "0.385.0",
4
53
  "type": "improvement",