@cosmicdrift/kumiko-renderer 0.389.0 → 0.390.1

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 (230) hide show
  1. package/dist/app/projection-detail-body.js +59 -394
  2. package/dist/app/projection-detail-header-model.d.ts +11 -0
  3. package/dist/app/projection-detail-header-model.js +33 -0
  4. package/dist/app/projection-detail-header-region.d.ts +28 -0
  5. package/dist/app/projection-detail-header-region.js +81 -0
  6. package/dist/app/projection-detail-header.d.ts +29 -0
  7. package/dist/app/projection-detail-header.js +84 -0
  8. package/dist/app/projection-detail-navigation.d.ts +13 -0
  9. package/dist/app/projection-detail-navigation.js +91 -0
  10. package/dist/app/projection-detail-states.d.ts +22 -0
  11. package/dist/app/projection-detail-states.js +26 -0
  12. package/dist/app/projection-drawer-host.d.ts +12 -0
  13. package/dist/app/projection-drawer-host.js +10 -0
  14. package/dist/app/projection-list-body.d.ts +1 -1
  15. package/dist/app/projection-list-body.js +69 -189
  16. package/dist/app/projection-list-render-props.d.ts +56 -0
  17. package/dist/app/projection-list-render-props.js +47 -0
  18. package/dist/app/projection-list-render.d.ts +26 -0
  19. package/dist/app/projection-list-render.js +20 -0
  20. package/dist/app/projection-list-row-click.d.ts +12 -0
  21. package/dist/app/projection-list-row-click.js +14 -0
  22. package/dist/app/use-projection-detail-actions.d.ts +35 -0
  23. package/dist/app/use-projection-detail-actions.js +156 -0
  24. package/dist/app/use-projection-detail-data.d.ts +19 -0
  25. package/dist/app/use-projection-detail-data.js +47 -0
  26. package/dist/app/use-projection-detail-layout.d.ts +17 -0
  27. package/dist/app/use-projection-detail-layout.js +23 -0
  28. package/dist/app/use-projection-list-actions.d.ts +31 -0
  29. package/dist/app/use-projection-list-actions.js +73 -0
  30. package/dist/app/use-projection-list-data.d.ts +20 -0
  31. package/dist/app/use-projection-list-data.js +26 -0
  32. package/dist/app/use-projection-list-definitions.d.ts +11 -0
  33. package/dist/app/use-projection-list-definitions.js +20 -0
  34. package/dist/app/use-projection-list-query.d.ts +28 -0
  35. package/dist/app/use-projection-list-query.js +83 -0
  36. package/dist/components/render-edit-banners.d.ts +12 -0
  37. package/dist/components/render-edit-banners.js +90 -0
  38. package/dist/components/render-edit-draft-save-helpers.d.ts +19 -0
  39. package/dist/components/render-edit-draft-save-helpers.js +42 -0
  40. package/dist/components/render-edit-field-grid.d.ts +4 -0
  41. package/dist/components/render-edit-field-grid.js +40 -0
  42. package/dist/components/render-edit-fields-section.d.ts +4 -0
  43. package/dist/components/render-edit-fields-section.js +76 -0
  44. package/dist/components/render-edit-form-actions.d.ts +4 -0
  45. package/dist/components/render-edit-form-actions.js +66 -0
  46. package/dist/components/render-edit-header-actions.d.ts +11 -0
  47. package/dist/components/render-edit-header-actions.js +120 -0
  48. package/dist/components/render-edit-layout-flags.d.ts +29 -0
  49. package/dist/components/render-edit-layout-flags.js +106 -0
  50. package/dist/components/render-edit-layout.d.ts +4 -0
  51. package/dist/components/render-edit-layout.js +79 -0
  52. package/dist/components/render-edit-render-model.d.ts +63 -0
  53. package/dist/components/render-edit-render-model.js +14 -0
  54. package/dist/components/render-edit-section-kinds.d.ts +4 -0
  55. package/dist/components/render-edit-section-kinds.js +115 -0
  56. package/dist/components/render-edit-submit-result.d.ts +8 -0
  57. package/dist/components/render-edit-submit-result.js +77 -0
  58. package/dist/components/render-edit-submit.d.ts +7 -0
  59. package/dist/components/render-edit-submit.js +113 -0
  60. package/dist/components/render-edit-titles.d.ts +17 -0
  61. package/dist/components/render-edit-titles.js +99 -0
  62. package/dist/components/render-edit-wizard-navigation.d.ts +11 -0
  63. package/dist/components/render-edit-wizard-navigation.js +115 -0
  64. package/dist/components/render-edit.d.ts +2 -2
  65. package/dist/components/render-edit.js +63 -1128
  66. package/dist/components/render-field-choice-inputs.d.ts +7 -0
  67. package/dist/components/render-field-choice-inputs.js +52 -0
  68. package/dist/components/render-field-control.d.ts +12 -0
  69. package/dist/components/render-field-control.js +93 -0
  70. package/dist/components/render-field-display-text.d.ts +4 -0
  71. package/dist/components/render-field-display-text.js +48 -0
  72. package/dist/components/render-field-fill-action.d.ts +13 -0
  73. package/dist/components/render-field-fill-action.js +63 -0
  74. package/dist/components/render-field-input-shared.d.ts +39 -0
  75. package/dist/components/render-field-input-shared.js +20 -0
  76. package/dist/components/render-field-input.d.ts +3 -0
  77. package/dist/components/render-field-input.js +34 -0
  78. package/dist/components/render-field-meta.d.ts +6 -0
  79. package/dist/components/render-field-meta.js +51 -0
  80. package/dist/components/render-field-read-only.d.ts +11 -0
  81. package/dist/components/render-field-read-only.js +75 -0
  82. package/dist/components/render-field-reference.d.ts +15 -0
  83. package/dist/components/render-field-reference.js +103 -0
  84. package/dist/components/render-field-scalar-inputs.d.ts +10 -0
  85. package/dist/components/render-field-scalar-inputs.js +35 -0
  86. package/dist/components/render-field-text-inputs.d.ts +3 -0
  87. package/dist/components/render-field-text-inputs.js +34 -0
  88. package/dist/components/render-field-values.d.ts +18 -0
  89. package/dist/components/render-field-values.js +111 -0
  90. package/dist/components/render-field-write-only.d.ts +9 -0
  91. package/dist/components/render-field-write-only.js +29 -0
  92. package/dist/components/render-field.d.ts +2 -2
  93. package/dist/components/render-field.js +39 -670
  94. package/dist/components/render-list-header-slot.d.ts +5 -0
  95. package/dist/components/render-list-header-slot.js +30 -0
  96. package/dist/components/render-list-reference-bridge.d.ts +3 -0
  97. package/dist/components/render-list-reference-bridge.js +25 -0
  98. package/dist/components/render-list-reference-columns.d.ts +25 -0
  99. package/dist/components/render-list-reference-columns.js +137 -0
  100. package/dist/components/render-list-table-props.d.ts +52 -0
  101. package/dist/components/render-list-table-props.js +53 -0
  102. package/dist/components/render-list-toolbar-model.d.ts +42 -0
  103. package/dist/components/render-list-toolbar-model.js +82 -0
  104. package/dist/components/render-list-toolbar.d.ts +38 -0
  105. package/dist/components/render-list-toolbar.js +40 -0
  106. package/dist/components/render-list-types.d.ts +113 -0
  107. package/dist/components/render-list-types.js +1 -0
  108. package/dist/components/render-list.d.ts +4 -113
  109. package/dist/components/render-list.js +91 -307
  110. package/dist/components/use-reference-input-state.d.ts +20 -0
  111. package/dist/components/use-reference-input-state.js +85 -0
  112. package/dist/components/use-render-edit-controlled-sync.d.ts +3 -0
  113. package/dist/components/use-render-edit-controlled-sync.js +26 -0
  114. package/dist/components/use-render-edit-draft-save.d.ts +29 -0
  115. package/dist/components/use-render-edit-draft-save.js +141 -0
  116. package/dist/components/use-render-edit-form.d.ts +10 -0
  117. package/dist/components/use-render-edit-form.js +101 -0
  118. package/dist/components/use-render-edit-sections.d.ts +15 -0
  119. package/dist/components/use-render-edit-sections.js +100 -0
  120. package/dist/components/use-render-edit-state.d.ts +65 -0
  121. package/dist/components/use-render-edit-state.js +158 -0
  122. package/dist/components/use-render-edit-steps.d.ts +13 -0
  123. package/dist/components/use-render-edit-steps.js +52 -0
  124. package/dist/components/use-render-list-columns.d.ts +18 -0
  125. package/dist/components/use-render-list-columns.js +41 -0
  126. package/dist/components/use-render-list-search.d.ts +4 -0
  127. package/dist/components/use-render-list-search.js +50 -0
  128. package/dist/primitives.d.ts +1 -1
  129. package/dist/web/app/dashboard-body.d.ts +1 -1
  130. package/dist/web/app/dashboard-body.js +42 -246
  131. package/dist/web/app/dashboard-header.d.ts +34 -0
  132. package/dist/web/app/dashboard-header.js +58 -0
  133. package/dist/web/app/dashboard-screen-controls.d.ts +11 -0
  134. package/dist/web/app/dashboard-screen-controls.js +42 -0
  135. package/dist/web/app/dashboard-screen-tiles.d.ts +8 -0
  136. package/dist/web/app/dashboard-screen-tiles.js +42 -0
  137. package/dist/web/app/dashboard-visibility.d.ts +42 -0
  138. package/dist/web/app/dashboard-visibility.js +102 -0
  139. package/dist/web/app/use-dashboard-screen-state.d.ts +35 -0
  140. package/dist/web/app/use-dashboard-screen-state.js +69 -0
  141. package/dist/web/primitives/data-table/actions-column.js +6 -5
  142. package/dist/web/primitives/data-table/card-body.d.ts +6 -0
  143. package/dist/web/primitives/data-table/card-body.js +32 -0
  144. package/dist/web/primitives/data-table/card-row.d.ts +5 -0
  145. package/dist/web/primitives/data-table/card-row.js +31 -0
  146. package/dist/web/primitives/data-table/card-view.d.ts +3 -0
  147. package/dist/web/primitives/data-table/card-view.js +13 -0
  148. package/dist/web/primitives/data-table/column-resize-handle.js +3 -1
  149. package/dist/web/primitives/data-table/data-cell-props.d.ts +7 -0
  150. package/dist/web/primitives/data-table/data-cell-props.js +8 -0
  151. package/dist/web/primitives/data-table/row-render-parts.d.ts +8 -0
  152. package/dist/web/primitives/data-table/row-render-parts.js +35 -0
  153. package/dist/web/primitives/data-table/table-body.d.ts +3 -0
  154. package/dist/web/primitives/data-table/table-body.js +32 -0
  155. package/dist/web/primitives/data-table/table-content.d.ts +3 -0
  156. package/dist/web/primitives/data-table/table-content.js +41 -0
  157. package/dist/web/primitives/data-table/table-data-cell.d.ts +4 -0
  158. package/dist/web/primitives/data-table/table-data-cell.js +25 -0
  159. package/dist/web/primitives/data-table/table-header-row.d.ts +3 -0
  160. package/dist/web/primitives/data-table/table-header-row.js +21 -0
  161. package/dist/web/primitives/data-table/table-view.d.ts +3 -0
  162. package/dist/web/primitives/data-table/table-view.js +25 -0
  163. package/dist/web/primitives/data-table/toolbar.d.ts +6 -0
  164. package/dist/web/primitives/data-table/toolbar.js +53 -0
  165. package/dist/web/primitives/data-table/use-data-table-state.d.ts +44 -0
  166. package/dist/web/primitives/data-table/use-data-table-state.js +123 -0
  167. package/dist/web/primitives/data-table.d.ts +3 -4
  168. package/dist/web/primitives/data-table.js +32 -252
  169. package/dist/web/primitives/file-value-list.js +1 -1
  170. package/dist/web/primitives/form-footer-parts.d.ts +25 -0
  171. package/dist/web/primitives/form-footer-parts.js +55 -0
  172. package/dist/web/primitives/form.js +33 -43
  173. package/dist/web/primitives/input-choice-kinds.d.ts +4 -0
  174. package/dist/web/primitives/input-choice-kinds.js +67 -0
  175. package/dist/web/primitives/input-file-kinds.d.ts +3 -0
  176. package/dist/web/primitives/input-file-kinds.js +6 -0
  177. package/dist/web/primitives/input-number-kinds.d.ts +4 -0
  178. package/dist/web/primitives/input-number-kinds.js +16 -0
  179. package/dist/web/primitives/input-optional-props.d.ts +20 -0
  180. package/dist/web/primitives/input-optional-props.js +24 -0
  181. package/dist/web/primitives/input-select-presentation.d.ts +13 -0
  182. package/dist/web/primitives/input-select-presentation.js +19 -0
  183. package/dist/web/primitives/input-shared.d.ts +22 -0
  184. package/dist/web/primitives/input-shared.js +10 -0
  185. package/dist/web/primitives/input-temporal-kinds.d.ts +5 -0
  186. package/dist/web/primitives/input-temporal-kinds.js +10 -0
  187. package/dist/web/primitives/input-text-kinds.d.ts +7 -0
  188. package/dist/web/primitives/input-text-kinds.js +33 -0
  189. package/dist/web/primitives/input.js +35 -161
  190. package/dist/web/widgets/chart-data.d.ts +35 -0
  191. package/dist/web/widgets/chart-data.js +37 -0
  192. package/dist/web/widgets/chart-markers.d.ts +26 -0
  193. package/dist/web/widgets/chart-markers.js +64 -0
  194. package/dist/web/widgets/chart-parts.d.ts +20 -0
  195. package/dist/web/widgets/chart-parts.js +15 -0
  196. package/dist/web/widgets/charts.d.ts +5 -78
  197. package/dist/web/widgets/charts.js +7 -485
  198. package/dist/web/widgets/drawer-panel-class.d.ts +3 -0
  199. package/dist/web/widgets/drawer-panel-class.js +40 -0
  200. package/dist/web/widgets/drawer-resize-controls.d.ts +11 -0
  201. package/dist/web/widgets/drawer-resize-controls.js +12 -0
  202. package/dist/web/widgets/drawer-sizing.d.ts +19 -0
  203. package/dist/web/widgets/drawer-sizing.js +60 -0
  204. package/dist/web/widgets/drawer.js +17 -116
  205. package/dist/web/widgets/stacked-area-brush.d.ts +13 -0
  206. package/dist/web/widgets/stacked-area-brush.js +91 -0
  207. package/dist/web/widgets/stacked-area-chart.d.ts +6 -0
  208. package/dist/web/widgets/stacked-area-chart.js +45 -0
  209. package/dist/web/widgets/stacked-area-chrome.d.ts +7 -0
  210. package/dist/web/widgets/stacked-area-chrome.js +77 -0
  211. package/dist/web/widgets/stacked-area-geometry.d.ts +54 -0
  212. package/dist/web/widgets/stacked-area-geometry.js +83 -0
  213. package/dist/web/widgets/stacked-area-model.d.ts +11 -0
  214. package/dist/web/widgets/stacked-area-model.js +1 -0
  215. package/dist/web/widgets/stacked-area-plot.d.ts +3 -0
  216. package/dist/web/widgets/stacked-area-plot.js +51 -0
  217. package/dist/web/widgets/stacked-area-props.d.ts +83 -0
  218. package/dist/web/widgets/stacked-area-props.js +16 -0
  219. package/dist/web/widgets/stacked-area-svg.d.ts +3 -0
  220. package/dist/web/widgets/stacked-area-svg.js +38 -0
  221. package/dist/web/widgets/stacked-area-tooltip.d.ts +3 -0
  222. package/dist/web/widgets/stacked-area-tooltip.js +86 -0
  223. package/dist/web/widgets/use-drawer-resize.d.ts +22 -0
  224. package/dist/web/widgets/use-drawer-resize.js +52 -0
  225. package/dist/web/widgets/use-stacked-area-tooltip.d.ts +9 -0
  226. package/dist/web/widgets/use-stacked-area-tooltip.js +34 -0
  227. package/dist/web/widgets/use-stacked-area-window.d.ts +20 -0
  228. package/dist/web/widgets/use-stacked-area-window.js +76 -0
  229. package/package.json +10 -10
  230. package/src/changes.json +54 -0
@@ -0,0 +1,18 @@
1
+ import type { EntityDefinition, EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { ListColumnViewModel, ListViewModel, Translate } from "../headless/index.ts";
3
+ import { type PublishedReferenceLookup } from "./render-list-reference-columns.ts";
4
+ type RenderListColumnsInput = {
5
+ readonly screen: EntityListScreenDefinition;
6
+ readonly entity: Pick<EntityDefinition, "fields" | "derivedFields">;
7
+ readonly rows: readonly Readonly<Record<string, unknown>>[];
8
+ readonly featureName: string;
9
+ readonly translate: Translate;
10
+ };
11
+ export declare function useRenderListColumns(input: RenderListColumnsInput): {
12
+ referenceColumns: import("./render-list-reference-columns.ts").ReferenceColumnSpec[];
13
+ handleLookupMap: (field: string, lookup: PublishedReferenceLookup) => void;
14
+ enrichedVm: Omit<ListViewModel, "columns"> & {
15
+ columns: readonly ListColumnViewModel[];
16
+ };
17
+ };
18
+ export {};
@@ -0,0 +1,41 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { useReferenceLinkResolver } from "../app/reference-link.js";
3
+ import { computeListViewModel } from "../headless/index.js";
4
+ import { enrichReferenceColumns, selectReferenceColumns, } from "./render-list-reference-columns.js";
5
+ export function useRenderListColumns(input) {
6
+ const { screen, entity, rows, featureName, translate } = input;
7
+ const vm = useMemo(() => computeListViewModel({ screen, entity, rows, translate, featureName }), [screen, entity, rows, translate, featureName]);
8
+ // Reference-field eager loading through bridge components.
9
+ // The Hooks rule requires useReferenceLookup for each reference column,
10
+ // but the number of columns varies with the schema. Each column therefore
11
+ // mounts a ReferenceLookupBridge that publishes its map to local state
12
+ // through onMap. Each bridge makes a constant number of hook calls (one),
13
+ // and React manages its lifecycle when the schema changes via key.
14
+ const referenceColumns = useMemo(() => selectReferenceColumns(vm.columns, featureName), [vm.columns, featureName]);
15
+ const [referenceLookups, setReferenceLookups] = useState({});
16
+ const handleLookupMap = useCallback((field, lookup) => {
17
+ setReferenceLookups((prev) => {
18
+ // Skip the update when the lookup is identical. Otherwise the bridge
19
+ // would re-render and send the same map back until React entered
20
+ // an infinite loop.
21
+ const current = prev[field];
22
+ if (current?.map === lookup.map && current.complete === lookup.complete)
23
+ return prev;
24
+ return { ...prev, [field]: lookup };
25
+ });
26
+ }, []);
27
+ const resolveReferenceLink = useReferenceLinkResolver();
28
+ const enrichedColumns = useMemo(() => enrichReferenceColumns({
29
+ columns: vm.columns,
30
+ referenceColumns,
31
+ referenceLookups,
32
+ resolveReferenceLink,
33
+ featureName,
34
+ translate,
35
+ }), [vm.columns, referenceColumns, referenceLookups, resolveReferenceLink, featureName, translate]);
36
+ const enrichedVm = useMemo(() => ({
37
+ ...vm,
38
+ columns: enrichedColumns,
39
+ }), [vm, enrichedColumns]);
40
+ return { referenceColumns, handleLookupMap, enrichedVm };
41
+ }
@@ -0,0 +1,4 @@
1
+ export declare function useRenderListSearch(searchValue: string | undefined, onSearchChange: ((next: string) => void) | undefined): {
2
+ readonly localQ: string;
3
+ readonly setLocalQ: (next: string) => void;
4
+ };
@@ -0,0 +1,50 @@
1
+ import { useEffect, useRef, useState } from "react";
2
+ const SEARCH_DEBOUNCE_MS = 300;
3
+ // Local search buffer + debounce. External changes (browser back,
4
+ // cross-component reset) are mirrored back; typing only fires
5
+ // onSearchChange after 300ms without further keystrokes.
6
+ export function useRenderListSearch(searchValue, onSearchChange) {
7
+ const [localQ, setLocalQ] = useState(searchValue ?? "");
8
+ // Sync during render, not in a useEffect: the passive effect also fires on
9
+ // mount and can flush after a keystroke landing in the same commit,
10
+ // resetting localQ and swallowing the input.
11
+ const [syncedSearchValue, setSyncedSearchValue] = useState(searchValue);
12
+ // The parent may echo our own debounced emits back late (router/URL round
13
+ // trip), possibly after a newer emit already went out; adopting such a stale
14
+ // echo would erase keys typed in between. All unechoed emits are kept, and a
15
+ // matching echo drops itself and every older one. The render phase only reads
16
+ // the list (a discarded or repeated render must find the echo again); the
17
+ // matched entries are dropped in an effect.
18
+ const pendingEmitsRef = useRef([]);
19
+ if (searchValue !== syncedSearchValue) {
20
+ setSyncedSearchValue(searchValue);
21
+ if (!pendingEmitsRef.current.includes(searchValue ?? "")) {
22
+ setLocalQ(searchValue ?? "");
23
+ pendingEmitsRef.current = [];
24
+ }
25
+ }
26
+ useEffect(() => {
27
+ const echoIndex = pendingEmitsRef.current.indexOf(syncedSearchValue ?? "");
28
+ if (echoIndex >= 0)
29
+ pendingEmitsRef.current.splice(0, echoIndex + 1);
30
+ }, [syncedSearchValue]);
31
+ useEffect(() => {
32
+ // skip: no search handler wired, so typing never emits
33
+ if (onSearchChange === undefined)
34
+ return;
35
+ // skip: buffer already matches the parent value
36
+ if (localQ === (searchValue ?? ""))
37
+ return;
38
+ // A late echo of an older emit changes searchValue but must not re-send
39
+ // the value that is already in flight.
40
+ // skip: that value is already in flight
41
+ if (pendingEmitsRef.current.at(-1) === localQ)
42
+ return;
43
+ const timer = setTimeout(() => {
44
+ pendingEmitsRef.current.push(localQ);
45
+ onSearchChange(localQ);
46
+ }, SEARCH_DEBOUNCE_MS);
47
+ return () => clearTimeout(timer);
48
+ }, [localQ, searchValue, onSearchChange]);
49
+ return { localQ, setLocalQ };
50
+ }
@@ -1365,7 +1365,7 @@ export type JsonViewProps = {
1365
1365
  * PDFs, an inline preview. Used for `renderer: { format: "file" }` on a detail field. */
1366
1366
  export type FileValueListProps = {
1367
1367
  readonly fileIds: readonly string[];
1368
- /** Starts the inline PDF preview of the first file open. */
1368
+ /** Starts the inline PDF preview of the newest file open; uploads append, so that is the last id. */
1369
1369
  readonly previewOpenByDefault?: boolean;
1370
1370
  readonly testId?: string;
1371
1371
  };
@@ -1,3 +1,3 @@
1
- import { type ReactNode } from "react";
1
+ import type { ReactNode } from "react";
2
2
  import { type DashboardBodyProps } from "../../index.ts";
3
3
  export declare function WebDashboardBody({ featureName, screen, translate, }: DashboardBodyProps): ReactNode;
@@ -1,55 +1,12 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from "react";
3
- import { ErrorReference } from "../../app/error-reference.js";
4
- import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, HeaderActionsBar, KumikoScreen, useEmbeddedScreen, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, } from "../../index.js";
5
- import { cn } from "../lib/cn.js";
6
- import { EmbeddedFormProvider } from "../primitives/index.js";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useTranslation, } from "../../index.js";
7
3
  import { PageSection } from "../primitives/layout.js";
8
- import { ModeSwitch } from "../widgets/mode-switch.js";
9
- import { SectionCard } from "../widgets/section-card.js";
10
- import { StatusBadge } from "../widgets/status-badge.js";
4
+ import { buildScopeBadge, dashboardDescription, recordTitleText, renderActionErrorBanner, renderDescriptionAndNotice, renderHeaderActionsBar, renderPageHeaderSlot, renderRecordError, renderRecordMissing, renderTitleRow, } from "./dashboard-header.js";
11
5
  import { ChartPanelBody, CustomPanelBody, FeedPanelBody, ListPanelBody, ProgressListPanelBody, StatGroupPanelBody, StatPanelBody, TextPanelBody, } from "./dashboard-panels.js";
12
- import { resolveRefreshIntervalMs } from "./dashboard-refresh.js";
13
- import { DashboardQueryContext, HALF_PANEL, PanelError, PanelLoadedContext, useDashboardFormats, useDashboardQuery, useLoadedAt, useRefetchRegistry, WIDE_PANEL, } from "./dashboard-shared.js";
14
- // Value lives in the URL under `filter.id` (like useListUrlState's
15
- // `<screenId>.page`, same replaceState) so a `navigate` rowAction with
16
- // `params` can deep-link onto this filter.
17
- function useFilterParams(screen, enabled) {
18
- const { Field, Input } = usePrimitives();
19
- const t = useTranslation();
20
- const nav = useNav();
21
- const filter = screen.filter;
22
- const value = filter !== undefined ? (nav.searchParams[filter.id] ?? "") : "";
23
- const optionsQueryResult = useQuery(filter?.optionsQuery ?? "", {}, { enabled: enabled && filter?.optionsQuery !== undefined });
24
- if (filter === undefined) {
25
- return { params: {}, picker: null };
26
- }
27
- const dynamicOptions = filter.optionsQuery !== undefined
28
- ? (optionsQueryResult.data?.rows ?? [])
29
- : (filter.options ?? []).map((o) => ({ value: o.value, label: t(o.label) }));
30
- const allLabel = t(filter.allLabel ?? "kumiko.dashboard.filter.all");
31
- const options = [{ value: "", label: allLabel }, ...dynamicOptions];
32
- const picker = (_jsx("div", { className: "max-w-xs", children: _jsx(Field, { id: `dashboard-filter-${filter.id}`, label: t(filter.label), children: _jsx(Input, { kind: "combobox", id: `dashboard-filter-${filter.id}`, name: filter.id, options: options, value: value, onChange: (next) => nav.setSearchParams({ [filter.id]: next === "" ? null : next }), placeholder: filter.placeholder !== undefined ? t(filter.placeholder) : allLabel }) }) }));
33
- const params = value === "" ? {} : { [filter.id]: value };
34
- return { params, picker };
35
- }
36
- // An unknown/stale URL value falls back to the declared default.
37
- function useTimeRange(screen, translate) {
38
- const t = useTranslation();
39
- const nav = useNav();
40
- const resolver = useLocale();
41
- const timeRange = screen.timeRange;
42
- if (timeRange === undefined)
43
- return { params: {}, control: null };
44
- const fromUrl = nav.searchParams[timeRange.id];
45
- const value = timeRange.options.find((o) => o.value === fromUrl)?.value ?? timeRange.default;
46
- return {
47
- // Metric buckets are cut server-side; without the user's zone they default
48
- // to UTC while the axis labels are formatted in the user's zone.
49
- params: { [timeRange.id]: value, timeZone: resolver.timeZone() },
50
- control: (_jsx(ModeSwitch, { value: value, options: timeRange.options.map((o) => ({ value: o.value, label: translate(o.label) })), onChange: (next) => nav.setSearchParams({ [timeRange.id]: next }), ariaLabel: t("kumiko.dashboard.time-range"), testId: `dashboard-time-range-${timeRange.id}` })),
51
- };
52
- }
6
+ import { ScreenPanelTile } from "./dashboard-screen-tiles.js";
7
+ import { DashboardQueryContext, HALF_PANEL, PanelLoadedContext, useDashboardFormats, WIDE_PANEL, } from "./dashboard-shared.js";
8
+ import { GatedGridCell, useDashboardVisibility, VisibilityGateQueries, VisibilityQueryContext, } from "./dashboard-visibility.js";
9
+ import { useDashboardHeaderActions, useDashboardRecord, useDashboardScreenParams, } from "./use-dashboard-screen-state.js";
53
10
  function panelSpanClassName(panel) {
54
11
  if (panel.kind === "stat")
55
12
  return undefined;
@@ -88,212 +45,51 @@ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
88
45
  }
89
46
  return (_jsx(ProgressListPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
90
47
  }
91
- const VisibilityQueryContext = createContext(new Map());
92
- const NO_GATE_PAYLOAD = {};
93
- function visibilityGate(query, payload) {
94
- return { key: `${query}|${JSON.stringify(payload)}`, query, payload };
95
- }
96
- // Embedded screens carry no screen filter; in-grid panels pass the filter
97
- // (unless ignoreScreenFilter, never for a stat-group) and the time range.
98
- function panelVisibilityGate(panel, screenParams) {
99
- if (panel.kind === "custom" || panel.visibleWhen === undefined)
100
- return undefined;
101
- if (panel.kind === "screen")
102
- return visibilityGate(panel.visibleWhen.query, NO_GATE_PAYLOAD);
103
- const dropsFilter = panel.kind !== "stat-group" && panel.kind !== "text" && panel.ignoreScreenFilter === true;
104
- return visibilityGate(panel.visibleWhen.query, {
105
- ...(dropsFilter ? {} : screenParams.filterParams),
106
- ...screenParams.rangeParams,
107
- ...screenParams.recordParams,
108
- });
109
- }
110
- // One live query per distinct gate (query + payload), shared by every panel
111
- // that gates on it — N panels on the same status query would otherwise each
112
- // open their own request, SSE subscription and refetch per event. The queries
113
- // run as keyed siblings of the grid so a changing gate count never changes the
114
- // grid's position in the tree (which would remount every panel).
115
- function VisibilityGateQuery({ gate, onResult, onForget, }) {
116
- const { data, error, loading, refetch } = useDashboardQuery(gate.query, gate.payload);
117
- const { key } = gate;
118
- useEffect(() => {
119
- onResult(key, { data, error, loading, refetch });
120
- }, [key, data, error, loading, refetch, onResult]);
121
- useEffect(() => () => onForget(key), [key, onForget]);
122
- return null;
123
- }
124
- function VisibilityGateQueries({ gates, onResult, onForget, }) {
125
- return gates.map((gate) => (_jsx(VisibilityGateQuery, { gate: gate, onResult: onResult, onForget: onForget }, gate.key)));
126
- }
127
- function useVisibilityGateResults() {
128
- const [results, setResults] = useState(new Map());
129
- const report = useCallback((key, result) => {
130
- setResults((prev) => new Map(prev).set(key, result));
131
- }, []);
132
- const forget = useCallback((key) => {
133
- setResults((prev) => {
134
- if (!prev.has(key))
135
- return prev;
136
- const next = new Map(prev);
137
- next.delete(key);
138
- return next;
139
- });
140
- }, []);
141
- return { results, report, forget };
142
- }
143
- const NOOP_REFETCH = async () => { };
144
- // A gate reports through an effect, so for its first commit (and after a payload change) no
145
- // result exists yet; without a placeholder that reads as "hidden" and flashes the fallback.
146
- function withPendingGates(results, gateKeys) {
147
- if (gateKeys.every((key) => results.has(key)))
148
- return results;
149
- const merged = new Map(results);
150
- for (const key of gateKeys) {
151
- if (!merged.has(key)) {
152
- merged.set(key, { data: null, error: null, loading: true, refetch: NOOP_REFETCH });
153
- }
48
+ function renderDashboardPanel(panel, featureName, screenId, screenParams, translate) {
49
+ if (panel.kind === "screen") {
50
+ return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: translate }, panel.id));
154
51
  }
155
- return merged;
156
- }
157
- function distinctVisibilityGates(panels, screenParams) {
158
- const gates = new Map();
159
- for (const panel of panels) {
160
- const gate = panelVisibilityGate(panel, screenParams);
161
- if (gate !== undefined)
162
- gates.set(gate.key, gate);
163
- }
164
- return [...gates.values()];
165
- }
166
- function GatedGridCell({ panel, screenParams, label, className, children, }) {
167
- const gate = panelVisibilityGate(panel, screenParams);
168
- const visibility = useContext(VisibilityQueryContext).get(gate?.key ?? "");
169
- const visibleWhen = panel.kind === "custom" ? undefined : panel.visibleWhen;
170
- if (visibleWhen !== undefined) {
171
- const verdict = evalVisibleWhen(visibleWhen, visibility);
172
- if (verdict === "error" && visibility?.error) {
173
- return (_jsx("div", { className: cn("min-w-0", className), "data-testid": `dashboard-panel-${panel.id}`, children: _jsx(PanelError, { label: label, error: visibility.error, onRetry: () => void visibility.refetch() }) }));
174
- }
175
- // Loading renders nothing too: a skeleton that then vanishes flickers.
176
- if (verdict !== "visible")
177
- return null;
178
- }
179
- return _jsx("div", { className: cn("min-w-0", className), children: children });
180
- }
181
- function EmbeddedScreenPanelTile({ panel, target, translate, }) {
182
- const screenBody = _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate });
183
- const embedded = panel.chromeless === true ? (_jsx(EmbeddedFormProvider, { children: _jsx(EmbeddedScreenProvider, { children: screenBody }) })) : (screenBody);
184
- return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: panel.label !== undefined && panel.chromeless !== true ? (_jsx(SectionCard, { title: translate(panel.label), children: embedded })) : (_jsxs(_Fragment, { children: [panel.label !== undefined && (_jsx("h3", { className: "mb-3 text-sm font-semibold text-foreground", children: translate(panel.label) })), embedded] })) }));
185
- }
186
- // Own component so the target lookup (a hook) only runs for panels that actually fall back.
187
- function FallbackScreenPanelTile({ panel, fallback, featureName, translate, }) {
188
- const target = useEmbeddedScreen(featureName, fallback);
189
- if (target === undefined)
190
- return null;
191
- return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
192
- }
193
- function ScreenPanelTile({ panel, featureName, translate, }) {
194
- const visibleWhen = panel.visibleWhen;
195
- const visibility = useContext(VisibilityQueryContext).get(visibleWhen !== undefined ? visibilityGate(visibleWhen.query, NO_GATE_PAYLOAD).key : "");
196
- const target = useEmbeddedScreen(featureName, panel.screen);
197
- const verdict = visibleWhen === undefined ? "visible" : evalVisibleWhen(visibleWhen, visibility);
198
- // Nothing while the gate loads: a fallback that flashes before the real panel flickers.
199
- if (verdict === "loading")
200
- return null;
201
- if (verdict !== "visible" && panel.fallback !== undefined) {
202
- return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, translate: translate }));
203
- }
204
- if (target === undefined)
205
- return null;
206
- // A failed gate query must not silently drop the panel: on the
207
- // account-security page that would hide every MFA enable/disable path.
208
- if (verdict === "error" && visibility?.error) {
209
- return (_jsx("div", { className: cn("min-w-0", WIDE_PANEL), "data-testid": `dashboard-panel-${panel.id}`, children: _jsx(PanelError, { label: panel.label !== undefined ? translate(panel.label) : panel.id, error: visibility.error, onRetry: () => void visibility.refetch() }) }));
210
- }
211
- if (verdict !== "visible")
212
- return null;
213
- return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
214
- }
215
- const NO_RECORD_ROW = {};
216
- function recordTitleText(row, field) {
217
- if (row === null || field === undefined)
218
- return undefined;
219
- const value = row[field];
220
- if (typeof value !== "string" && typeof value !== "number")
221
- return undefined;
222
- return String(value) === "" ? undefined : String(value);
52
+ const label = panel.kind === "custom" || panel.label === undefined ? undefined : translate(panel.label);
53
+ return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }) }, panel.id));
223
54
  }
224
55
  export function WebDashboardBody({ featureName, screen, translate, }) {
225
56
  const t = useTranslation();
226
57
  const effectiveTranslate = translate ?? t;
227
- const { Text, Banner, PageHeader, Heading, Button, Dialog, ActionOverflowMenu, Grid } = usePrimitives();
58
+ const primitives = usePrimitives();
228
59
  const nav = useNav();
229
60
  const headerSlotAvailable = usePageHeaderSlotAvailable();
230
61
  const headerCompactContext = usePageHeaderCompact();
231
62
  const { formatDateTime } = useDashboardFormats();
232
- const recordBinding = screen.record;
233
- const recordId = recordBinding !== undefined ? (nav.searchParams[recordBinding.param] ?? "") : "";
234
- const recordMissing = recordBinding !== undefined && recordId === "";
235
- const recordPayloadKey = recordBinding?.payloadKey ?? recordBinding?.param;
236
- const recordParams = recordPayloadKey !== undefined && recordId !== "" ? { [recordPayloadKey]: recordId } : {};
237
- const recordQueryName = recordBinding?.query;
238
- const { params: filterParams, picker } = useFilterParams(screen, !recordMissing);
239
- const { params: rangeParams, control: rangeControl } = useTimeRange(screen, effectiveTranslate);
240
- const screenParams = { filterParams, rangeParams, recordParams };
241
- const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
242
- const visibilityGates = distinctVisibilityGates(screen.panels, screenParams);
243
- const gateResults = useVisibilityGateResults();
244
- const gateKeysSignature = visibilityGates.map((gate) => gate.key).join("\n");
245
- const gateResultsWithPending = useMemo(() => withPendingGates(gateResults.results, gateKeysSignature === "" ? [] : gateKeysSignature.split("\n")), [gateResults.results, gateKeysSignature]);
246
- // The record query is not a panel query: it needs the interval before the
247
- // registry context below exists, and `refresh.field` reads from its own row.
248
- const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
249
- const recordQuery = useQuery(recordQueryName ?? "", recordParams, {
250
- enabled: recordQueryName !== undefined && !recordMissing,
251
- live: true,
252
- ...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
253
- });
254
- const { data: recordRow, refetch: refetchRecord } = recordQuery;
255
- useEffect(() => {
256
- setRefreshIntervalMs(resolveRefreshIntervalMs(screen.refresh, recordRow));
257
- }, [screen.refresh, recordRow]);
258
- const { register, refetchRegistered } = useRefetchRegistry();
259
- const queryRuntime = useMemo(() => ({ refreshIntervalMs, register }), [refreshIntervalMs, register]);
260
- const refetchAll = useCallback(async () => {
261
- await Promise.all([
262
- recordQueryName !== undefined ? refetchRecord() : Promise.resolve(),
263
- refetchRegistered(),
264
- ]);
265
- }, [recordQueryName, refetchRecord, refetchRegistered]);
266
- const [actionError, setActionError] = useState(null);
267
- const idOnlyRow = useMemo(() => (recordPayloadKey !== undefined ? { id: recordId, [recordPayloadKey]: recordId } : null), [recordId, recordPayloadKey]);
268
- const actionRow = recordQueryName !== undefined ? recordRow : idOnlyRow;
269
- const headerActions = useRecordActions({
270
- actions: actionRow !== null && !recordMissing ? screen.actions : undefined,
271
- record: actionRow ?? NO_RECORD_ROW,
272
- recordId,
273
- translate: effectiveTranslate,
274
- onWriteSuccess: refetchAll,
275
- });
276
- const scope = screen.scope;
277
- // A raw description is agent-facing prose; only an i18n key is user-facing copy.
278
- const translatedDescription = screen.description !== undefined ? effectiveTranslate(screen.description) : undefined;
279
- const description = translatedDescription !== screen.description ? translatedDescription : undefined;
280
- const scopeBadge = scope !== undefined ? (_jsx(StatusBadge, { tone: "accent", testId: `dashboard-${screen.id}-scope`, children: effectiveTranslate(scope.badge) })) : null;
281
- if (recordMissing) {
282
- return (_jsx(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: _jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-record-missing`, children: t("kumiko.dashboard.record.missing") }) }));
63
+ const params = useDashboardScreenParams(screen, effectiveTranslate, nav);
64
+ const { screenParams } = params;
65
+ const visibility = useDashboardVisibility(screen.panels, screenParams);
66
+ const record = useDashboardRecord(screen, params);
67
+ const { recordQuery, recordRow, refetchRecord } = record;
68
+ const { actionError, setActionError, headerActions } = useDashboardHeaderActions(screen, params, record, effectiveTranslate);
69
+ const description = dashboardDescription(screen, effectiveTranslate);
70
+ const scopeBadge = buildScopeBadge(screen, effectiveTranslate);
71
+ if (params.recordMissing) {
72
+ return (_jsx(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: renderRecordMissing(primitives, t, screen.id) }));
283
73
  }
284
- const usesPageHeaderSlot = headerSlotAvailable && PageHeader !== undefined;
285
- const recordTitle = recordTitleText(recordRow, recordBinding?.titleField);
286
- const headerActionsBar = headerActions !== undefined && headerActions.length > 0 ? (_jsx(Grid, { columns: "auto", testId: `dashboard-${screen.id}-actions`, children: _jsx(HeaderActionsBar, { actions: headerActions, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: usesPageHeaderSlot, compact: headerCompactContext && usesPageHeaderSlot, PageHeader: PageHeader, overflowTestId: `dashboard-${screen.id}-actions-overflow` }) })) : null;
74
+ const usesPageHeaderSlot = headerSlotAvailable && primitives.PageHeader !== undefined;
75
+ const model = {
76
+ screen,
77
+ t,
78
+ translate: effectiveTranslate,
79
+ primitives,
80
+ headerActions,
81
+ setActionError,
82
+ headerCompactContext,
83
+ usesPageHeaderSlot,
84
+ recordTitle: recordTitleText(recordRow, params.recordBinding?.titleField),
85
+ scopeBadge,
86
+ picker: params.picker,
87
+ rangeControl: params.rangeControl,
88
+ loadedAtMs: params.loadedAtMs,
89
+ formatDateTime,
90
+ };
91
+ const headerActionsBar = renderHeaderActionsBar(model);
287
92
  // The shell header owns the title; the badge and the actions sit in it there.
288
93
  const badgeInHeader = scopeBadge !== null && usesPageHeaderSlot;
289
- const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
290
- return (_jsx(PanelLoadedContext.Provider, { value: reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [showPageHeader && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })), _jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: t("kumiko.dashboard.updated-at", { time: formatDateTime(loadedAtMs) }) })), rangeControl] })] }), actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })), recordQuery.error !== null && recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(recordQuery.error, t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: t("kumiko.dashboard.panel.error.retry") })] })), description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: effectiveTranslate(scope.notice) })), _jsxs(VisibilityQueryContext.Provider, { value: gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibilityGates, onResult: gateResults.report, onForget: gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => {
291
- if (panel.kind === "screen") {
292
- return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: effectiveTranslate }, panel.id));
293
- }
294
- const label = panel.kind === "custom" || panel.label === undefined
295
- ? undefined
296
- : effectiveTranslate(panel.label);
297
- return (_jsx(GatedGridCell, { panel: panel, screenParams: screenParams, label: label ?? panel.id, className: panelSpanClassName(panel), children: _jsx(PanelBody, { panel: panel, label: label, screenId: screen.id, screenParams: screenParams, translate: effectiveTranslate }) }, panel.id));
298
- }) })] })] }) }) }));
94
+ return (_jsx(PanelLoadedContext.Provider, { value: params.reportLoaded, children: _jsx(DashboardQueryContext.Provider, { value: record.queryRuntime, children: _jsxs(PageSection, { className: "flex flex-col gap-4", testId: `dashboard-${screen.id}`, children: [renderPageHeaderSlot(model, headerActionsBar, badgeInHeader), renderTitleRow(model, headerActionsBar, badgeInHeader), renderActionErrorBanner(primitives, actionError), renderRecordError(model, recordQuery.error, recordRow, refetchRecord), renderDescriptionAndNotice(model, description), _jsxs(VisibilityQueryContext.Provider, { value: visibility.gateResultsWithPending, children: [_jsx(VisibilityGateQueries, { gates: visibility.visibilityGates, onResult: visibility.gateResults.report, onForget: visibility.gateResults.forget }), _jsx("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: screen.panels.map((panel) => renderDashboardPanel(panel, featureName, screen.id, screenParams, effectiveTranslate)) })] })] }) }) }));
299
95
  }
@@ -0,0 +1,34 @@
1
+ import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { Dispatch, ReactNode, SetStateAction } from "react";
3
+ import { type ErrorNotice } from "../../app/error-reference.tsx";
4
+ import type { DispatcherError, Translate } from "../../headless/index.ts";
5
+ import { type usePrimitives, type useRecordActions } from "../../index.ts";
6
+ import type { useDashboardFormats, useLoadedAt } from "./dashboard-shared.tsx";
7
+ type Primitives = ReturnType<typeof usePrimitives>;
8
+ export type DashboardHeaderModel = {
9
+ readonly screen: DashboardScreenDefinition;
10
+ readonly t: Translate;
11
+ readonly translate: Translate;
12
+ readonly primitives: Primitives;
13
+ readonly headerActions: ReturnType<typeof useRecordActions>;
14
+ readonly setActionError: Dispatch<SetStateAction<ErrorNotice | null>>;
15
+ readonly headerCompactContext: boolean;
16
+ readonly usesPageHeaderSlot: boolean;
17
+ readonly recordTitle: string | undefined;
18
+ readonly scopeBadge: ReactNode;
19
+ readonly picker: ReactNode;
20
+ readonly rangeControl: ReactNode;
21
+ readonly loadedAtMs: ReturnType<typeof useLoadedAt>["loadedAtMs"];
22
+ readonly formatDateTime: ReturnType<typeof useDashboardFormats>["formatDateTime"];
23
+ };
24
+ export declare function recordTitleText(row: Readonly<Record<string, unknown>> | null, field: string | undefined): string | undefined;
25
+ export declare function buildScopeBadge(screen: DashboardScreenDefinition, translate: Translate): ReactNode;
26
+ export declare function dashboardDescription(screen: DashboardScreenDefinition, translate: Translate): string | undefined;
27
+ export declare function renderRecordMissing(primitives: Primitives, t: Translate, screenId: string): ReactNode;
28
+ export declare function renderHeaderActionsBar(model: DashboardHeaderModel): ReactNode;
29
+ export declare function renderPageHeaderSlot(model: DashboardHeaderModel, headerActionsBar: ReactNode, badgeInHeader: boolean): ReactNode;
30
+ export declare function renderTitleRow(model: DashboardHeaderModel, headerActionsBar: ReactNode, badgeInHeader: boolean): ReactNode;
31
+ export declare function renderActionErrorBanner(primitives: Primitives, actionError: ErrorNotice | null): ReactNode;
32
+ export declare function renderRecordError(model: DashboardHeaderModel, error: DispatcherError | null, recordRow: Readonly<Record<string, unknown>> | null, refetchRecord: () => Promise<void>): ReactNode;
33
+ export declare function renderDescriptionAndNotice(model: DashboardHeaderModel, description: string | undefined): ReactNode;
34
+ export {};
@@ -0,0 +1,58 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ErrorReference } from "../../app/error-reference.js";
3
+ import { dispatcherErrorText, HeaderActionsBar, } from "../../index.js";
4
+ import { StatusBadge } from "../widgets/status-badge.js";
5
+ export function recordTitleText(row, field) {
6
+ if (row === null || field === undefined)
7
+ return undefined;
8
+ const value = row[field];
9
+ if (typeof value !== "string" && typeof value !== "number")
10
+ return undefined;
11
+ return String(value) === "" ? undefined : String(value);
12
+ }
13
+ export function buildScopeBadge(screen, translate) {
14
+ const scope = screen.scope;
15
+ return scope !== undefined ? (_jsx(StatusBadge, { tone: "accent", testId: `dashboard-${screen.id}-scope`, children: translate(scope.badge) })) : null;
16
+ }
17
+ export function dashboardDescription(screen, translate) {
18
+ // A raw description is agent-facing prose; only an i18n key is user-facing copy.
19
+ const translatedDescription = screen.description !== undefined ? translate(screen.description) : undefined;
20
+ return translatedDescription !== screen.description ? translatedDescription : undefined;
21
+ }
22
+ export function renderRecordMissing(primitives, t, screenId) {
23
+ const { Banner } = primitives;
24
+ return (_jsx(Banner, { variant: "info", testId: `dashboard-${screenId}-record-missing`, children: t("kumiko.dashboard.record.missing") }));
25
+ }
26
+ export function renderHeaderActionsBar(model) {
27
+ const { screen, headerActions, headerCompactContext, usesPageHeaderSlot, setActionError } = model;
28
+ const { Button, Dialog, ActionOverflowMenu, Grid, PageHeader } = model.primitives;
29
+ return headerActions !== undefined && headerActions.length > 0 ? (_jsx(Grid, { columns: "auto", testId: `dashboard-${screen.id}-actions`, children: _jsx(HeaderActionsBar, { actions: headerActions, Button: Button, Dialog: Dialog, ActionOverflowMenu: ActionOverflowMenu, onError: setActionError, collapseAfterPrimary: usesPageHeaderSlot, compact: headerCompactContext && usesPageHeaderSlot, PageHeader: PageHeader, overflowTestId: `dashboard-${screen.id}-actions-overflow` }) })) : null;
30
+ }
31
+ export function renderPageHeaderSlot(model, headerActionsBar, badgeInHeader) {
32
+ const { PageHeader } = model.primitives;
33
+ const { recordTitle, scopeBadge, usesPageHeaderSlot } = model;
34
+ const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
35
+ return (showPageHeader &&
36
+ PageHeader !== undefined && (_jsx(PageHeader, { ...(recordTitle !== undefined && { title: recordTitle }), ...(badgeInHeader && { status: scopeBadge }), ...(headerActionsBar !== null && { actions: headerActionsBar }) })));
37
+ }
38
+ export function renderTitleRow(model, headerActionsBar, badgeInHeader) {
39
+ const { Heading } = model.primitives;
40
+ const { screen, usesPageHeaderSlot, recordTitle, scopeBadge, picker, rangeControl } = model;
41
+ return (_jsxs("div", { className: "flex flex-wrap items-end justify-between gap-3", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [!usesPageHeaderSlot && recordTitle !== undefined && (_jsx(Heading, { variant: "page", testId: `dashboard-${screen.id}-record-title`, children: recordTitle })), !badgeInHeader && scopeBadge, picker] }), _jsxs("div", { className: "flex flex-wrap items-center gap-3", children: [!usesPageHeaderSlot && headerActionsBar, screen.showUpdatedAt !== false && (_jsx("span", { "data-testid": `dashboard-${screen.id}-updated-at`, className: "text-xs tabular-nums text-muted-foreground", children: model.t("kumiko.dashboard.updated-at", {
42
+ time: model.formatDateTime(model.loadedAtMs),
43
+ }) })), rangeControl] })] }));
44
+ }
45
+ export function renderActionErrorBanner(primitives, actionError) {
46
+ const { Banner } = primitives;
47
+ return (actionError !== null && (_jsxs(Banner, { variant: "error", testId: "render-edit-action-error", children: [actionError.message, _jsx(ErrorReference, { requestId: actionError.requestId })] })));
48
+ }
49
+ export function renderRecordError(model, error, recordRow, refetchRecord) {
50
+ const { Button } = model.primitives;
51
+ return (error !== null &&
52
+ recordRow === null && (_jsxs("div", { role: "alert", "data-testid": `dashboard-${model.screen.id}-record-error`, className: "flex flex-col items-start gap-2 text-sm", children: [_jsx("span", { className: "text-status-bad", children: dispatcherErrorText(error, model.t) }), _jsx(Button, { variant: "secondary", onClick: () => void refetchRecord(), children: model.t("kumiko.dashboard.panel.error.retry") })] })));
53
+ }
54
+ export function renderDescriptionAndNotice(model, description) {
55
+ const { Text, Banner } = model.primitives;
56
+ const { screen } = model;
57
+ return (_jsxs(_Fragment, { children: [description !== undefined && (_jsx(Text, { variant: "muted", testId: `dashboard-${screen.id}-description`, children: description })), screen.scope?.notice !== undefined && (_jsx(Banner, { variant: "info", testId: `dashboard-${screen.id}-scope-notice`, children: model.translate(screen.scope.notice) }))] }));
58
+ }
@@ -0,0 +1,11 @@
1
+ import type { DashboardScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import type { ReactNode } from "react";
3
+ import type { Translate } from "../../headless/index.ts";
4
+ export declare function useFilterParams(screen: DashboardScreenDefinition, enabled: boolean): {
5
+ readonly params: Readonly<Record<string, unknown>>;
6
+ readonly picker: ReactNode;
7
+ };
8
+ export declare function useTimeRange(screen: DashboardScreenDefinition, translate: Translate): {
9
+ readonly params: Readonly<Record<string, unknown>>;
10
+ readonly control: ReactNode;
11
+ };
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useLocale, useNav, usePrimitives, useQuery, useTranslation } from "../../index.js";
3
+ import { ModeSwitch } from "../widgets/mode-switch.js";
4
+ // Value lives in the URL under `filter.id` (like useListUrlState's
5
+ // `<screenId>.page`, same replaceState) so a `navigate` rowAction with
6
+ // `params` can deep-link onto this filter.
7
+ export function useFilterParams(screen, enabled) {
8
+ const { Field, Input } = usePrimitives();
9
+ const t = useTranslation();
10
+ const nav = useNav();
11
+ const filter = screen.filter;
12
+ const value = filter !== undefined ? (nav.searchParams[filter.id] ?? "") : "";
13
+ const optionsQueryResult = useQuery(filter?.optionsQuery ?? "", {}, { enabled: enabled && filter?.optionsQuery !== undefined });
14
+ if (filter === undefined) {
15
+ return { params: {}, picker: null };
16
+ }
17
+ const dynamicOptions = filter.optionsQuery !== undefined
18
+ ? (optionsQueryResult.data?.rows ?? [])
19
+ : (filter.options ?? []).map((o) => ({ value: o.value, label: t(o.label) }));
20
+ const allLabel = t(filter.allLabel ?? "kumiko.dashboard.filter.all");
21
+ const options = [{ value: "", label: allLabel }, ...dynamicOptions];
22
+ const picker = (_jsx("div", { className: "max-w-xs", children: _jsx(Field, { id: `dashboard-filter-${filter.id}`, label: t(filter.label), children: _jsx(Input, { kind: "combobox", id: `dashboard-filter-${filter.id}`, name: filter.id, options: options, value: value, onChange: (next) => nav.setSearchParams({ [filter.id]: next === "" ? null : next }), placeholder: filter.placeholder !== undefined ? t(filter.placeholder) : allLabel }) }) }));
23
+ const params = value === "" ? {} : { [filter.id]: value };
24
+ return { params, picker };
25
+ }
26
+ // An unknown/stale URL value falls back to the declared default.
27
+ export function useTimeRange(screen, translate) {
28
+ const t = useTranslation();
29
+ const nav = useNav();
30
+ const resolver = useLocale();
31
+ const timeRange = screen.timeRange;
32
+ if (timeRange === undefined)
33
+ return { params: {}, control: null };
34
+ const fromUrl = nav.searchParams[timeRange.id];
35
+ const value = timeRange.options.find((o) => o.value === fromUrl)?.value ?? timeRange.default;
36
+ return {
37
+ // Metric buckets are cut server-side; without the user's zone they default
38
+ // to UTC while the axis labels are formatted in the user's zone.
39
+ params: { [timeRange.id]: value, timeZone: resolver.timeZone() },
40
+ control: (_jsx(ModeSwitch, { value: value, options: timeRange.options.map((o) => ({ value: o.value, label: translate(o.label) })), onChange: (next) => nav.setSearchParams({ [timeRange.id]: next }), ariaLabel: t("kumiko.dashboard.time-range"), testId: `dashboard-time-range-${timeRange.id}` })),
41
+ };
42
+ }
@@ -0,0 +1,8 @@
1
+ import type { DashboardScreenPanel } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { type ReactNode } from "react";
3
+ import type { Translate } from "../../headless/index.ts";
4
+ export declare function ScreenPanelTile({ panel, featureName, translate, }: {
5
+ readonly panel: DashboardScreenPanel;
6
+ readonly featureName: string;
7
+ readonly translate: Translate;
8
+ }): ReactNode;