@cosmicdrift/kumiko-renderer 0.374.0 → 0.377.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 (455) hide show
  1. package/README.md +2 -2
  2. package/dist/app/action-form-support.d.ts +9 -2
  3. package/dist/app/action-form-support.js +24 -2
  4. package/dist/app/client-actions.d.ts +2 -2
  5. package/dist/app/column-renderers.js +1 -1
  6. package/dist/app/config-edit-shim.js +1 -1
  7. package/dist/app/content-preview.js +1 -6
  8. package/dist/app/custom-screens.js +1 -1
  9. package/dist/app/dashboard-body.d.ts +1 -1
  10. package/dist/app/entity-list-expanded-row.d.ts +1 -1
  11. package/dist/app/form-schema.d.ts +1 -1
  12. package/dist/app/form-schema.js +2 -2
  13. package/dist/app/kumiko-screen.d.ts +1 -1
  14. package/dist/app/kumiko-screen.js +7 -6
  15. package/dist/app/list-empty-content.d.ts +1 -1
  16. package/dist/app/list-facets.d.ts +1 -1
  17. package/dist/app/nav.js +2 -2
  18. package/dist/app/query-state-banners.d.ts +1 -1
  19. package/dist/app/reference-link.d.ts +1 -1
  20. package/dist/app/row-actions.d.ts +1 -1
  21. package/dist/app/secret-mint-body.d.ts +1 -1
  22. package/dist/app/secrets-edit-body.d.ts +1 -1
  23. package/dist/app/use-record-actions.d.ts +1 -1
  24. package/dist/app/write-failed-error.d.ts +1 -1
  25. package/dist/components/availability-options-select.d.ts +1 -1
  26. package/dist/components/embedded-list-field.d.ts +1 -1
  27. package/dist/components/embedded-list-field.js +1 -1
  28. package/dist/components/file-list-field.d.ts +1 -1
  29. package/dist/components/grid-cell-for-field.d.ts +1 -1
  30. package/dist/components/multi-select-checkboxes.d.ts +1 -1
  31. package/dist/components/multi-select-checkboxes.js +1 -1
  32. package/dist/components/query-options-select.d.ts +1 -1
  33. package/dist/components/related-list-section.d.ts +1 -1
  34. package/dist/components/render-edit-logic.d.ts +1 -1
  35. package/dist/components/render-edit-types.d.ts +2 -2
  36. package/dist/components/render-edit.d.ts +1 -1
  37. package/dist/components/render-edit.js +4 -2
  38. package/dist/components/render-field.d.ts +1 -1
  39. package/dist/components/render-field.js +4 -1
  40. package/dist/components/render-list.d.ts +1 -1
  41. package/dist/components/render-list.js +20 -4
  42. package/dist/components/write-form-section.d.ts +1 -1
  43. package/dist/components/write-form-section.js +1 -1
  44. package/dist/context/dispatcher-context.d.ts +1 -1
  45. package/dist/format-when.js +1 -1
  46. package/dist/headless/apex/css.d.ts +3 -0
  47. package/dist/headless/apex/css.js +298 -0
  48. package/dist/headless/apex/index.d.ts +236 -0
  49. package/dist/headless/apex/index.js +377 -0
  50. package/dist/headless/apex/lightbox.d.ts +4 -0
  51. package/dist/headless/apex/lightbox.js +81 -0
  52. package/dist/headless/contracts/asset.d.ts +15 -0
  53. package/dist/headless/contracts/asset.js +17 -0
  54. package/dist/headless/contracts/index.d.ts +3 -0
  55. package/dist/headless/contracts/index.js +1 -0
  56. package/dist/headless/contracts/locale.d.ts +7 -0
  57. package/dist/headless/contracts/locale.js +11 -0
  58. package/dist/headless/contracts/primitives.d.ts +93 -0
  59. package/dist/headless/contracts/primitives.js +21 -0
  60. package/dist/headless/dispatcher/index.d.ts +2 -0
  61. package/dist/headless/dispatcher/index.js +1 -0
  62. package/dist/headless/dispatcher/types.d.ts +86 -0
  63. package/dist/headless/dispatcher/types.js +34 -0
  64. package/dist/headless/form/form-controller.d.ts +2 -0
  65. package/dist/headless/form/form-controller.js +409 -0
  66. package/dist/headless/form/index.d.ts +3 -0
  67. package/dist/headless/form/index.js +2 -0
  68. package/dist/headless/form/types.d.ts +76 -0
  69. package/dist/headless/form/types.js +1 -0
  70. package/dist/headless/form/zod-bridge.d.ts +10 -0
  71. package/dist/headless/form/zod-bridge.js +103 -0
  72. package/dist/headless/format/document-values.d.ts +12 -0
  73. package/dist/headless/format/document-values.js +70 -0
  74. package/dist/headless/format/escape.d.ts +5 -0
  75. package/dist/headless/format/escape.js +75 -0
  76. package/dist/headless/format/html-template.d.ts +9 -0
  77. package/dist/headless/format/html-template.js +39 -0
  78. package/dist/headless/format/index.d.ts +12 -0
  79. package/dist/headless/format/index.js +237 -0
  80. package/dist/headless/format/money.d.ts +3 -0
  81. package/dist/headless/format/money.js +39 -0
  82. package/dist/headless/index.d.ts +14 -0
  83. package/dist/headless/index.js +9 -0
  84. package/dist/headless/locale-routing/index.d.ts +41 -0
  85. package/dist/headless/locale-routing/index.js +145 -0
  86. package/dist/headless/nav/index.d.ts +2 -0
  87. package/dist/headless/nav/index.js +1 -0
  88. package/dist/headless/nav/resolve.d.ts +2 -0
  89. package/dist/headless/nav/resolve.js +71 -0
  90. package/dist/headless/nav/types.d.ts +26 -0
  91. package/dist/headless/nav/types.js +1 -0
  92. package/dist/headless/store/create-store.d.ts +2 -0
  93. package/dist/headless/store/create-store.js +43 -0
  94. package/dist/headless/store/equality.d.ts +1 -0
  95. package/dist/headless/store/equality.js +34 -0
  96. package/dist/headless/store/index.d.ts +3 -0
  97. package/dist/headless/store/index.js +2 -0
  98. package/dist/headless/store/types.d.ts +7 -0
  99. package/dist/headless/store/types.js +10 -0
  100. package/dist/headless/view-model/edit.d.ts +18 -0
  101. package/dist/headless/view-model/edit.js +501 -0
  102. package/dist/headless/view-model/embedded-list.d.ts +27 -0
  103. package/dist/headless/view-model/embedded-list.js +63 -0
  104. package/dist/headless/view-model/index.d.ts +7 -0
  105. package/dist/headless/view-model/index.js +3 -0
  106. package/dist/headless/view-model/list-file-display.d.ts +3 -0
  107. package/dist/headless/view-model/list-file-display.js +27 -0
  108. package/dist/headless/view-model/list.d.ts +16 -0
  109. package/dist/headless/view-model/list.js +229 -0
  110. package/dist/headless/view-model/types.d.ts +382 -0
  111. package/dist/headless/view-model/types.js +1 -0
  112. package/dist/hooks/use-ai-text.d.ts +1 -1
  113. package/dist/hooks/use-form.d.ts +1 -1
  114. package/dist/hooks/use-form.js +1 -1
  115. package/dist/hooks/use-mutation.d.ts +1 -1
  116. package/dist/hooks/use-query.d.ts +1 -1
  117. package/dist/hooks/use-store.d.ts +1 -1
  118. package/dist/hooks/use-store.js +7 -7
  119. package/dist/hooks/use-stream-handler.d.ts +1 -1
  120. package/dist/i18n.d.ts +1 -1
  121. package/dist/i18n.js +1 -1
  122. package/dist/index.js +4 -4
  123. package/dist/live/csrf.d.ts +2 -0
  124. package/dist/live/csrf.js +60 -0
  125. package/dist/live/dispatcher-live.d.ts +7 -0
  126. package/dist/live/dispatcher-live.js +286 -0
  127. package/dist/live/envelopes.d.ts +101 -0
  128. package/dist/live/envelopes.js +35 -0
  129. package/dist/live/error-mapping.d.ts +16 -0
  130. package/dist/live/error-mapping.js +82 -0
  131. package/dist/live/index.d.ts +7 -0
  132. package/dist/live/index.js +5 -0
  133. package/dist/live/locale.d.ts +2 -0
  134. package/dist/live/locale.js +25 -0
  135. package/dist/live/sse-stream.d.ts +14 -0
  136. package/dist/live/sse-stream.js +128 -0
  137. package/dist/primitives.d.ts +35 -36
  138. package/dist/primitives.js +1 -1
  139. package/dist/session/session-ended.d.ts +1 -1
  140. package/dist/tokens.d.ts +3 -4
  141. package/dist/tokens.js +4 -5
  142. package/dist/web/app/app-schema-boot.d.ts +19 -0
  143. package/dist/web/app/app-schema-boot.js +124 -0
  144. package/dist/web/app/browser-locale.d.ts +22 -0
  145. package/dist/web/app/browser-locale.js +78 -0
  146. package/dist/web/app/client-plugin.d.ts +106 -0
  147. package/dist/web/app/client-plugin.js +12 -0
  148. package/dist/web/app/create-app.d.ts +104 -0
  149. package/dist/web/app/create-app.js +498 -0
  150. package/dist/web/app/create-public-surface.d.ts +36 -0
  151. package/dist/web/app/create-public-surface.js +40 -0
  152. package/dist/web/app/dashboard-body.d.ts +3 -0
  153. package/dist/web/app/dashboard-body.js +865 -0
  154. package/dist/web/app/dashboard-refresh.d.ts +6 -0
  155. package/dist/web/app/dashboard-refresh.js +15 -0
  156. package/dist/web/app/document-lang-sync.d.ts +4 -0
  157. package/dist/web/app/document-lang-sync.js +27 -0
  158. package/dist/web/app/draft-storage.d.ts +11 -0
  159. package/dist/web/app/draft-storage.js +53 -0
  160. package/dist/web/app/nav-providers-context.d.ts +13 -0
  161. package/dist/web/app/nav-providers-context.js +18 -0
  162. package/dist/web/app/nav.d.ts +40 -0
  163. package/dist/web/app/nav.js +231 -0
  164. package/dist/web/app/plain-content-editor.d.ts +3 -0
  165. package/dist/web/app/plain-content-editor.js +30 -0
  166. package/dist/web/app/relative-time.d.ts +10 -0
  167. package/dist/web/app/relative-time.js +56 -0
  168. package/dist/web/app/resolvers-context.d.ts +12 -0
  169. package/dist/web/app/resolvers-context.js +10 -0
  170. package/dist/web/app/rich-content-editor.d.ts +3 -0
  171. package/dist/web/app/rich-content-editor.js +15 -0
  172. package/dist/web/app/root-error-fallback.d.ts +2 -0
  173. package/dist/web/app/root-error-fallback.js +21 -0
  174. package/dist/web/app/screen-error-fallback.d.ts +6 -0
  175. package/dist/web/app/screen-error-fallback.js +17 -0
  176. package/dist/web/app/tiptap-editor.d.ts +3 -0
  177. package/dist/web/app/tiptap-editor.js +120 -0
  178. package/dist/web/app/toast-action-notifier.d.ts +5 -0
  179. package/dist/web/app/toast-action-notifier.js +17 -0
  180. package/dist/web/components/config-cascade.d.ts +13 -0
  181. package/dist/web/components/config-cascade.js +155 -0
  182. package/dist/web/components/config-source-badge.d.ts +6 -0
  183. package/dist/web/components/config-source-badge.js +55 -0
  184. package/dist/web/default-color-scheme.d.ts +2 -0
  185. package/dist/web/default-color-scheme.js +3 -0
  186. package/dist/web/icons.d.ts +94 -0
  187. package/dist/web/icons.js +100 -0
  188. package/dist/web/index.d.ts +78 -0
  189. package/dist/web/index.js +53 -0
  190. package/dist/web/layout/app-layout.d.ts +21 -0
  191. package/dist/web/layout/app-layout.js +8 -0
  192. package/dist/web/layout/avatar.d.ts +12 -0
  193. package/dist/web/layout/avatar.js +63 -0
  194. package/dist/web/layout/default-app-shell.d.ts +41 -0
  195. package/dist/web/layout/default-app-shell.js +16 -0
  196. package/dist/web/layout/editor-panel.d.ts +10 -0
  197. package/dist/web/layout/editor-panel.js +29 -0
  198. package/dist/web/layout/fill-classes.d.ts +9 -0
  199. package/dist/web/layout/fill-classes.js +15 -0
  200. package/dist/web/layout/filter-app-schema-navs.d.ts +7 -0
  201. package/dist/web/layout/filter-app-schema-navs.js +68 -0
  202. package/dist/web/layout/header-action-group.d.ts +6 -0
  203. package/dist/web/layout/header-action-group.js +8 -0
  204. package/dist/web/layout/header-overflow-menu.d.ts +4 -0
  205. package/dist/web/layout/header-overflow-menu.js +8 -0
  206. package/dist/web/layout/language-menu-items.d.ts +8 -0
  207. package/dist/web/layout/language-menu-items.js +16 -0
  208. package/dist/web/layout/language-switcher.d.ts +25 -0
  209. package/dist/web/layout/language-switcher.js +46 -0
  210. package/dist/web/layout/nav-tree.d.ts +19 -0
  211. package/dist/web/layout/nav-tree.js +536 -0
  212. package/dist/web/layout/page-header-slot.d.ts +25 -0
  213. package/dist/web/layout/page-header-slot.js +50 -0
  214. package/dist/web/layout/profile-menu.d.ts +13 -0
  215. package/dist/web/layout/profile-menu.js +6 -0
  216. package/dist/web/layout/shell-breadcrumb.d.ts +11 -0
  217. package/dist/web/layout/shell-breadcrumb.js +34 -0
  218. package/dist/web/layout/shell-header.d.ts +12 -0
  219. package/dist/web/layout/shell-header.js +159 -0
  220. package/dist/web/layout/sidebar-brand.d.ts +15 -0
  221. package/dist/web/layout/sidebar-brand.js +9 -0
  222. package/dist/web/layout/sidebar-panel.d.ts +41 -0
  223. package/dist/web/layout/sidebar-panel.js +107 -0
  224. package/dist/web/layout/sidebar-user.d.ts +8 -0
  225. package/dist/web/layout/sidebar-user.js +17 -0
  226. package/dist/web/layout/sidebar.d.ts +17 -0
  227. package/dist/web/layout/sidebar.js +11 -0
  228. package/dist/web/layout/target-resolver-stub.d.ts +13 -0
  229. package/dist/web/layout/target-resolver-stub.js +61 -0
  230. package/dist/web/layout/target-url.d.ts +12 -0
  231. package/dist/web/layout/target-url.js +77 -0
  232. package/dist/web/layout/theme-label.d.ts +15 -0
  233. package/dist/web/layout/theme-label.js +35 -0
  234. package/dist/web/layout/theme-menu-item.d.ts +12 -0
  235. package/dist/web/layout/theme-menu-item.js +16 -0
  236. package/dist/web/layout/theme-toggle.d.ts +17 -0
  237. package/dist/web/layout/theme-toggle.js +22 -0
  238. package/dist/web/layout/topbar.d.ts +8 -0
  239. package/dist/web/layout/topbar.js +4 -0
  240. package/dist/web/layout/workspace-shell.d.ts +63 -0
  241. package/dist/web/layout/workspace-shell.js +220 -0
  242. package/dist/web/layout/workspace-switcher.d.ts +15 -0
  243. package/dist/web/layout/workspace-switcher.js +36 -0
  244. package/dist/web/lib/accept-attr.d.ts +1 -0
  245. package/dist/web/lib/accept-attr.js +6 -0
  246. package/dist/web/lib/clamp.d.ts +1 -0
  247. package/dist/web/lib/clamp.js +3 -0
  248. package/dist/web/lib/cn.d.ts +6 -0
  249. package/dist/web/lib/cn.js +9 -0
  250. package/dist/web/lib/download.d.ts +2 -0
  251. package/dist/web/lib/download.js +24 -0
  252. package/dist/web/lib/file-ref-download.d.ts +15 -0
  253. package/dist/web/lib/file-ref-download.js +62 -0
  254. package/dist/web/lib/file-upload-form-data.d.ts +7 -0
  255. package/dist/web/lib/file-upload-form-data.js +11 -0
  256. package/dist/web/lib/resize-image.d.ts +1 -0
  257. package/dist/web/lib/resize-image.js +66 -0
  258. package/dist/web/lib/upload-file-ref.d.ts +9 -0
  259. package/dist/web/lib/upload-file-ref.js +29 -0
  260. package/dist/web/primitives/action-menu.d.ts +34 -0
  261. package/dist/web/primitives/action-menu.js +30 -0
  262. package/dist/web/primitives/calendar-popover.d.ts +13 -0
  263. package/dist/web/primitives/calendar-popover.js +76 -0
  264. package/dist/web/primitives/combobox.d.ts +56 -0
  265. package/dist/web/primitives/combobox.js +127 -0
  266. package/dist/web/primitives/date-field.d.ts +18 -0
  267. package/dist/web/primitives/date-field.js +74 -0
  268. package/dist/web/primitives/date-input.d.ts +1 -0
  269. package/dist/web/primitives/date-input.js +5 -0
  270. package/dist/web/primitives/date-parse.d.ts +11 -0
  271. package/dist/web/primitives/date-parse.js +159 -0
  272. package/dist/web/primitives/dialog.d.ts +3 -0
  273. package/dist/web/primitives/dialog.js +42 -0
  274. package/dist/web/primitives/drawer.d.ts +3 -0
  275. package/dist/web/primitives/drawer.js +17 -0
  276. package/dist/web/primitives/dropdown-menu.d.ts +15 -0
  277. package/dist/web/primitives/dropdown-menu.js +56 -0
  278. package/dist/web/primitives/embedded-list-input.d.ts +9 -0
  279. package/dist/web/primitives/embedded-list-input.js +382 -0
  280. package/dist/web/primitives/file-thumbnail.d.ts +25 -0
  281. package/dist/web/primitives/file-thumbnail.js +90 -0
  282. package/dist/web/primitives/file-upload.d.ts +21 -0
  283. package/dist/web/primitives/file-upload.js +56 -0
  284. package/dist/web/primitives/index.d.ts +34 -0
  285. package/dist/web/primitives/index.js +2021 -0
  286. package/dist/web/primitives/json-view.d.ts +7 -0
  287. package/dist/web/primitives/json-view.js +84 -0
  288. package/dist/web/primitives/layout.d.ts +27 -0
  289. package/dist/web/primitives/layout.js +30 -0
  290. package/dist/web/primitives/lightbox.d.ts +3 -0
  291. package/dist/web/primitives/lightbox.js +36 -0
  292. package/dist/web/primitives/locale-number.d.ts +2 -0
  293. package/dist/web/primitives/locale-number.js +54 -0
  294. package/dist/web/primitives/located-timestamp-input.d.ts +22 -0
  295. package/dist/web/primitives/located-timestamp-input.js +22 -0
  296. package/dist/web/primitives/metric.d.ts +5 -0
  297. package/dist/web/primitives/metric.js +18 -0
  298. package/dist/web/primitives/modal-shell.d.ts +17 -0
  299. package/dist/web/primitives/modal-shell.js +9 -0
  300. package/dist/web/primitives/modal.d.ts +3 -0
  301. package/dist/web/primitives/modal.js +13 -0
  302. package/dist/web/primitives/money-input.d.ts +18 -0
  303. package/dist/web/primitives/money-input.js +122 -0
  304. package/dist/web/primitives/narrow-pinned-footer.d.ts +12 -0
  305. package/dist/web/primitives/narrow-pinned-footer.js +153 -0
  306. package/dist/web/primitives/number-input.d.ts +18 -0
  307. package/dist/web/primitives/number-input.js +53 -0
  308. package/dist/web/primitives/option-groups.d.ts +11 -0
  309. package/dist/web/primitives/option-groups.js +27 -0
  310. package/dist/web/primitives/page-header.d.ts +3 -0
  311. package/dist/web/primitives/page-header.js +52 -0
  312. package/dist/web/primitives/promo-panel.d.ts +3 -0
  313. package/dist/web/primitives/promo-panel.js +11 -0
  314. package/dist/web/primitives/segmented-select.d.ts +21 -0
  315. package/dist/web/primitives/segmented-select.js +62 -0
  316. package/dist/web/primitives/share-actions.d.ts +4 -0
  317. package/dist/web/primitives/share-actions.js +56 -0
  318. package/dist/web/primitives/status-badge.d.ts +3 -0
  319. package/dist/web/primitives/status-badge.js +5 -0
  320. package/dist/web/primitives/sticky-action-bar.d.ts +5 -0
  321. package/dist/web/primitives/sticky-action-bar.js +15 -0
  322. package/dist/web/primitives/tabs.d.ts +3 -0
  323. package/dist/web/primitives/tabs.js +107 -0
  324. package/dist/web/primitives/timestamp-input.d.ts +27 -0
  325. package/dist/web/primitives/timestamp-input.js +87 -0
  326. package/dist/web/primitives/toast.d.ts +19 -0
  327. package/dist/web/primitives/toast.js +63 -0
  328. package/dist/web/primitives/tz-input.d.ts +11 -0
  329. package/dist/web/primitives/tz-input.js +8 -0
  330. package/dist/web/primitives/tz-options.d.ts +4 -0
  331. package/dist/web/primitives/tz-options.js +17 -0
  332. package/dist/web/primitives/use-keyboard-inset.d.ts +1 -0
  333. package/dist/web/primitives/use-keyboard-inset.js +28 -0
  334. package/dist/web/primitives/use-narrow-viewport.d.ts +4 -0
  335. package/dist/web/primitives/use-narrow-viewport.js +46 -0
  336. package/dist/web/react-css-custom-properties.d.ts +6 -0
  337. package/dist/web/react-css-custom-properties.js +1 -0
  338. package/dist/web/sse/live-events.d.ts +15 -0
  339. package/dist/web/sse/live-events.js +148 -0
  340. package/dist/web/theme-plugin.d.ts +41 -0
  341. package/dist/web/theme-plugin.js +123 -0
  342. package/dist/web/tokens.d.ts +24 -0
  343. package/dist/web/tokens.js +203 -0
  344. package/dist/web/ui/avatar.d.ts +11 -0
  345. package/dist/web/ui/avatar.js +25 -0
  346. package/dist/web/ui/badge.d.ts +9 -0
  347. package/dist/web/ui/badge.js +26 -0
  348. package/dist/web/ui/breadcrumb.d.ts +11 -0
  349. package/dist/web/ui/breadcrumb.js +29 -0
  350. package/dist/web/ui/button.d.ts +10 -0
  351. package/dist/web/ui/button.js +37 -0
  352. package/dist/web/ui/checkbox.d.ts +4 -0
  353. package/dist/web/ui/checkbox.js +11 -0
  354. package/dist/web/ui/input.d.ts +3 -0
  355. package/dist/web/ui/input.js +8 -0
  356. package/dist/web/ui/label.d.ts +4 -0
  357. package/dist/web/ui/label.js +10 -0
  358. package/dist/web/ui/resizable.d.ts +7 -0
  359. package/dist/web/ui/resizable.js +16 -0
  360. package/dist/web/ui/separator.d.ts +4 -0
  361. package/dist/web/ui/separator.js +10 -0
  362. package/dist/web/ui/sheet.d.ts +14 -0
  363. package/dist/web/ui/sheet.js +42 -0
  364. package/dist/web/ui/sidebar.d.ts +73 -0
  365. package/dist/web/ui/sidebar.js +211 -0
  366. package/dist/web/ui/skeleton.d.ts +2 -0
  367. package/dist/web/ui/skeleton.js +7 -0
  368. package/dist/web/ui/switch.d.ts +6 -0
  369. package/dist/web/ui/switch.js +10 -0
  370. package/dist/web/ui/table.d.ts +10 -0
  371. package/dist/web/ui/table.js +30 -0
  372. package/dist/web/ui/tabs.d.ts +11 -0
  373. package/dist/web/ui/tabs.js +31 -0
  374. package/dist/web/ui/textarea.d.ts +3 -0
  375. package/dist/web/ui/textarea.js +8 -0
  376. package/dist/web/ui/tooltip.d.ts +7 -0
  377. package/dist/web/ui/tooltip.js +19 -0
  378. package/dist/web/ui/use-mobile.d.ts +1 -0
  379. package/dist/web/ui/use-mobile.js +16 -0
  380. package/dist/web/version/update-checker.d.ts +9 -0
  381. package/dist/web/version/update-checker.js +88 -0
  382. package/dist/web/widgets/ai-text-field.d.ts +36 -0
  383. package/dist/web/widgets/ai-text-field.js +137 -0
  384. package/dist/web/widgets/chart-window.d.ts +58 -0
  385. package/dist/web/widgets/chart-window.js +190 -0
  386. package/dist/web/widgets/charts.d.ts +208 -0
  387. package/dist/web/widgets/charts.js +685 -0
  388. package/dist/web/widgets/collapsible-section.d.ts +10 -0
  389. package/dist/web/widgets/collapsible-section.js +17 -0
  390. package/dist/web/widgets/dashboard-list.d.ts +21 -0
  391. package/dist/web/widgets/dashboard-list.js +45 -0
  392. package/dist/web/widgets/detail-list.d.ts +15 -0
  393. package/dist/web/widgets/detail-list.js +8 -0
  394. package/dist/web/widgets/drawer.d.ts +65 -0
  395. package/dist/web/widgets/drawer.js +142 -0
  396. package/dist/web/widgets/feed-list.d.ts +13 -0
  397. package/dist/web/widgets/feed-list.js +9 -0
  398. package/dist/web/widgets/floating-panel.d.ts +30 -0
  399. package/dist/web/widgets/floating-panel.js +242 -0
  400. package/dist/web/widgets/form-fields.d.ts +93 -0
  401. package/dist/web/widgets/form-fields.js +50 -0
  402. package/dist/web/widgets/index.d.ts +27 -0
  403. package/dist/web/widgets/index.js +30 -0
  404. package/dist/web/widgets/infinity-list.d.ts +46 -0
  405. package/dist/web/widgets/infinity-list.js +211 -0
  406. package/dist/web/widgets/mode-switch.d.ts +21 -0
  407. package/dist/web/widgets/mode-switch.js +25 -0
  408. package/dist/web/widgets/photo-slots.d.ts +29 -0
  409. package/dist/web/widgets/photo-slots.js +52 -0
  410. package/dist/web/widgets/plan-card.d.ts +36 -0
  411. package/dist/web/widgets/plan-card.js +13 -0
  412. package/dist/web/widgets/progress-bar.d.ts +19 -0
  413. package/dist/web/widgets/progress-bar.js +24 -0
  414. package/dist/web/widgets/progress-list.d.ts +19 -0
  415. package/dist/web/widgets/progress-list.js +11 -0
  416. package/dist/web/widgets/public-shell.d.ts +22 -0
  417. package/dist/web/widgets/public-shell.js +15 -0
  418. package/dist/web/widgets/query-table.d.ts +29 -0
  419. package/dist/web/widgets/query-table.js +30 -0
  420. package/dist/web/widgets/result-panel.d.ts +69 -0
  421. package/dist/web/widgets/result-panel.js +30 -0
  422. package/dist/web/widgets/section-card.d.ts +12 -0
  423. package/dist/web/widgets/section-card.js +9 -0
  424. package/dist/web/widgets/sheet-parts.d.ts +9 -0
  425. package/dist/web/widgets/sheet-parts.js +11 -0
  426. package/dist/web/widgets/side-by-side-table.d.ts +31 -0
  427. package/dist/web/widgets/side-by-side-table.js +11 -0
  428. package/dist/web/widgets/stat.d.ts +54 -0
  429. package/dist/web/widgets/stat.js +65 -0
  430. package/dist/web/widgets/states.d.ts +23 -0
  431. package/dist/web/widgets/states.js +20 -0
  432. package/dist/web/widgets/status-badge.d.ts +15 -0
  433. package/dist/web/widgets/status-badge.js +24 -0
  434. package/dist/web/widgets/step-bar.d.ts +35 -0
  435. package/dist/web/widgets/step-bar.js +60 -0
  436. package/dist/web/widgets/upload-zone.d.ts +24 -0
  437. package/dist/web/widgets/upload-zone.js +108 -0
  438. package/dist/web/widgets/use-draft.d.ts +20 -0
  439. package/dist/web/widgets/use-draft.js +21 -0
  440. package/dist/web/widgets/use-pointer-drag.d.ts +21 -0
  441. package/dist/web/widgets/use-pointer-drag.js +53 -0
  442. package/package.json +105 -7
  443. package/src/changes.json +53 -0
  444. package/src/web/fonts/OFL.txt +93 -0
  445. package/src/web/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  446. package/src/web/fonts/ibm-plex-mono-latin-500.woff2 +0 -0
  447. package/src/web/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
  448. package/src/web/fonts/ibm-plex-mono-latin-ext-500.woff2 +0 -0
  449. package/src/web/fonts/ibm-plex-sans-latin-400.woff2 +0 -0
  450. package/src/web/fonts/ibm-plex-sans-latin-500.woff2 +0 -0
  451. package/src/web/fonts/ibm-plex-sans-latin-600.woff2 +0 -0
  452. package/src/web/fonts/ibm-plex-sans-latin-ext-400.woff2 +0 -0
  453. package/src/web/fonts/ibm-plex-sans-latin-ext-500.woff2 +0 -0
  454. package/src/web/fonts/ibm-plex-sans-latin-ext-600.woff2 +0 -0
  455. package/src/web/styles.css +401 -0
@@ -0,0 +1,865 @@
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
3
+ import { DASHBOARD_STAT_TONES } from "@cosmicdrift/kumiko-types/screen";
4
+ import { isOneOf } from "@cosmicdrift/kumiko-types/type-guards";
5
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
6
+ import { currencyDecimals, EMPTY_VALUE_PLACEHOLDER } from "../../headless/index.js";
7
+ import { dispatcherErrorText, EmbeddedScreenProvider, evalVisibleWhen, extensionSectionName, HeaderActionsBar, KumikoScreen, useEmbeddedScreen, useExtensionSectionComponent, useLocale, useNav, usePageHeaderCompact, usePageHeaderSlotAvailable, usePrimitives, useQuery, useRecordActions, useTranslation, } from "../../index.js";
8
+ import { cn } from "../lib/cn.js";
9
+ import { EmbeddedFormProvider } from "../primitives/index.js";
10
+ import { PageSection } from "../primitives/layout.js";
11
+ import { resolveSafeLocale } from "../primitives/locale-number.js";
12
+ import { formatMoney } from "../primitives/money-input.js";
13
+ import { Skeleton } from "../ui/skeleton.js";
14
+ import { initialWindowSelection, } from "../widgets/chart-window.js";
15
+ import { chartToneColor, SegmentBarChart, StackedAreaChart, StackedBarChart, TimeseriesChart, } from "../widgets/charts.js";
16
+ import { DashboardListTable } from "../widgets/dashboard-list.js";
17
+ import { FeedList } from "../widgets/feed-list.js";
18
+ import { ModeSwitch } from "../widgets/mode-switch.js";
19
+ import { ProgressList } from "../widgets/progress-list.js";
20
+ import { SectionCard } from "../widgets/section-card.js";
21
+ import { StatCard, StatStripCell } from "../widgets/stat.js";
22
+ import { EmptyState, LoadingState } from "../widgets/states.js";
23
+ import { StatusBadge } from "../widgets/status-badge.js";
24
+ import { resolveRefreshIntervalMs } from "./dashboard-refresh.js";
25
+ import { useRelativeTimeFormatter } from "./relative-time.js";
26
+ const SKELETON_SEGMENT_WIDTHS = ["100%", "72%", "48%"];
27
+ const WIDE_PANEL = "sm:col-span-2 lg:col-span-4";
28
+ const HALF_PANEL = "sm:col-span-2 lg:col-span-2";
29
+ const CURRENCY_CODE_PATTERN = /^[A-Za-z]{3}$/;
30
+ const HOUR_MS = 3_600_000;
31
+ const DAY_MS = 24 * HOUR_MS;
32
+ const SKELETON_BAR_HEIGHTS = ["40%", "65%", "50%", "85%", "60%", "75%", "45%", "70%"];
33
+ function panelPayload(panel, screenParams) {
34
+ return {
35
+ ...(panel.ignoreScreenFilter === true ? {} : screenParams.filterParams),
36
+ ...screenParams.rangeParams,
37
+ ...screenParams.recordParams,
38
+ ...panel.params,
39
+ };
40
+ }
41
+ const DashboardQueryContext = createContext({
42
+ refreshIntervalMs: undefined,
43
+ register: () => () => { },
44
+ });
45
+ function useRefetchRegistry() {
46
+ const registered = useRef(new Set());
47
+ const register = useCallback((refetch) => {
48
+ registered.current.add(refetch);
49
+ return () => {
50
+ registered.current.delete(refetch);
51
+ };
52
+ }, []);
53
+ const refetchRegistered = useCallback(async () => {
54
+ await Promise.all([...registered.current].map((refetch) => refetch()));
55
+ }, []);
56
+ return { register, refetchRegistered };
57
+ }
58
+ /** Live query of a panel or gate that takes part in the dashboard's refresh:
59
+ * polls at the screen's interval and is refetched by `refetchAll` after an
60
+ * action. A disabled query registers nothing. */
61
+ function useDashboardQuery(type, payload, options = {}) {
62
+ const { refreshIntervalMs, register } = useContext(DashboardQueryContext);
63
+ const enabled = options.enabled ?? true;
64
+ const result = useQuery(type, payload, {
65
+ live: true,
66
+ enabled,
67
+ ...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
68
+ });
69
+ const { refetch } = result;
70
+ useEffect(() => (enabled ? register(refetch) : undefined), [enabled, register, refetch]);
71
+ return result;
72
+ }
73
+ function useDashboardFormats() {
74
+ const resolver = useLocale();
75
+ const locale = resolver.locale();
76
+ const timeZone = resolver.timeZone();
77
+ const formatRelative = useRelativeTimeFormatter(locale);
78
+ const number = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 });
79
+ // Without the significant-digit cap, values below the locale's first
80
+ // abbreviation step keep a decimal ("298.989,9 €" in de); lessPrecision
81
+ // picks whichever rule rounds more, so that becomes "299.000 €" while
82
+ // "1,5 Mio. €" keeps its one decimal.
83
+ const compactOptions = {
84
+ notation: "compact",
85
+ maximumFractionDigits: 1,
86
+ maximumSignificantDigits: 3,
87
+ roundingPriority: "lessPrecision",
88
+ };
89
+ const compactNumber = new Intl.NumberFormat(locale, compactOptions);
90
+ const percent = new Intl.NumberFormat(locale, { style: "percent", maximumFractionDigits: 0 });
91
+ const day = new Intl.DateTimeFormat(locale, { day: "2-digit", month: "2-digit", timeZone });
92
+ const month = new Intl.DateTimeFormat(locale, { month: "2-digit", year: "numeric", timeZone });
93
+ const hour = new Intl.DateTimeFormat(locale, { hour: "2-digit", minute: "2-digit", timeZone });
94
+ const dateTime = new Intl.DateTimeFormat(locale, {
95
+ dateStyle: "medium",
96
+ timeStyle: "short",
97
+ timeZone,
98
+ });
99
+ const medium = new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone });
100
+ const formatMinor = (amountMinor, currency, fractionDigits) => formatMoney(amountMinor, currency, locale, fractionDigits);
101
+ return {
102
+ timeZone,
103
+ formatMedium: (atMs) => medium.format(atMs),
104
+ formatMinor,
105
+ formatRelative,
106
+ formatPanelValue: (valueFormat) => valueFormat === undefined
107
+ ? (value) => number.format(value)
108
+ : (value) => formatMinor(value, valueFormat.currency, valueFormat.fractionDigits),
109
+ formatPanelAxisValue: (valueFormat) => {
110
+ if (valueFormat === undefined || !CURRENCY_CODE_PATTERN.test(valueFormat.currency)) {
111
+ return (value) => compactNumber.format(value);
112
+ }
113
+ const compactMoney = new Intl.NumberFormat(resolveSafeLocale(locale), {
114
+ ...compactOptions,
115
+ style: "currency",
116
+ currency: valueFormat.currency,
117
+ });
118
+ const decimals = currencyDecimals(valueFormat.currency);
119
+ return (value) => compactMoney.format(value / 10 ** decimals);
120
+ },
121
+ formatNumber: (value) => number.format(value),
122
+ formatPercent: (fraction) => percent.format(fraction),
123
+ formatDay: (atMs) => day.format(atMs),
124
+ formatMonth: (atMs) => month.format(atMs),
125
+ formatHour: (atMs) => hour.format(atMs),
126
+ formatDateTime: (atMs) => dateTime.format(atMs),
127
+ };
128
+ }
129
+ const MAX_TEXT_PARAM_DEPTH = 4;
130
+ function isRecord(value) {
131
+ return typeof value === "object" && value !== null && !Array.isArray(value);
132
+ }
133
+ function isDashboardI18nText(value) {
134
+ return isRecord(value) && typeof value["i18nKey"] === "string";
135
+ }
136
+ function isMoneyParam(value) {
137
+ return (isRecord(value) &&
138
+ value["kind"] === "money" &&
139
+ typeof value["amountMinor"] === "number" &&
140
+ typeof value["currency"] === "string");
141
+ }
142
+ function isDateParam(value) {
143
+ return isRecord(value) && value["kind"] === "date" && typeof value["atMs"] === "number";
144
+ }
145
+ function isRelativeTimeParam(value) {
146
+ return isRecord(value) && value["kind"] === "relativeTime" && typeof value["atMs"] === "number";
147
+ }
148
+ function isTextParam(value) {
149
+ return (typeof value === "string" ||
150
+ typeof value === "number" ||
151
+ isDashboardI18nText(value) ||
152
+ isMoneyParam(value) ||
153
+ isDateParam(value) ||
154
+ isRelativeTimeParam(value));
155
+ }
156
+ function resolveTextParam(param, translate, formats, depth) {
157
+ if (typeof param === "string" || typeof param === "number")
158
+ return param;
159
+ if (isMoneyParam(param))
160
+ return formats.formatMinor(param.amountMinor, param.currency);
161
+ if (isDateParam(param))
162
+ return formats.formatMedium(param.atMs);
163
+ if (isRelativeTimeParam(param))
164
+ return formats.formatRelative(param.atMs);
165
+ if (isDashboardI18nText(param)) {
166
+ // Too deep: show the key instead of recursing further.
167
+ return depth >= MAX_TEXT_PARAM_DEPTH
168
+ ? param.i18nKey
169
+ : resolveI18nText(param, translate, formats, depth + 1);
170
+ }
171
+ return "";
172
+ }
173
+ function resolveI18nText(text, translate, formats, depth) {
174
+ if (text.i18nParams === undefined)
175
+ return translate(text.i18nKey);
176
+ const params = {};
177
+ for (const [name, param] of Object.entries(text.i18nParams)) {
178
+ if (isTextParam(param))
179
+ params[name] = resolveTextParam(param, translate, formats, depth);
180
+ }
181
+ return translate(text.i18nKey, params);
182
+ }
183
+ /** Plain strings pass through; DashboardI18nText is translated (nested params first).
184
+ * Anything else yields undefined so callers fall back to their own placeholder. */
185
+ function resolveDashboardText(value, translate, formats) {
186
+ if (typeof value === "string")
187
+ return value;
188
+ if (typeof value === "number")
189
+ return String(value);
190
+ if (isDashboardI18nText(value))
191
+ return resolveI18nText(value, translate, formats, 1);
192
+ return undefined;
193
+ }
194
+ function PanelShell({ testId, title, subtitle, action, children, }) {
195
+ return (_jsxs("section", { "data-testid": testId, className: "flex h-full flex-col rounded-lg border border-border bg-card text-card-foreground", children: [_jsxs("header", { className: "flex min-h-11 shrink-0 flex-wrap items-center justify-between gap-x-3 gap-y-2 border-b border-border-row px-4 py-2", children: [_jsxs("div", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("h2", { className: "text-sm font-semibold break-words", children: title }), subtitle !== undefined && _jsx("p", { className: "text-xs text-muted-foreground", children: subtitle })] }), action !== undefined && _jsx("div", { className: "shrink-0", children: action })] }), _jsx("div", { className: "grow p-4", children: children })] }));
196
+ }
197
+ function PanelError({ label, error, onRetry, testId, }) {
198
+ const { Button } = usePrimitives();
199
+ const t = useTranslation();
200
+ return (_jsxs("div", { role: "alert", "data-testid": testId, className: "flex flex-col items-start gap-2 rounded-lg border border-dashed border-status-bad/40 px-4 py-6 text-sm", children: [_jsx("div", { className: "font-medium text-status-bad", children: t("kumiko.dashboard.panel.error.title", { label }) }), _jsx("div", { className: "text-muted-foreground", children: dispatcherErrorText(error, t) }), _jsx(Button, { variant: "secondary", onClick: onRetry, children: t("kumiko.dashboard.panel.error.retry") }), _jsx("div", { className: "text-xs text-muted-foreground", children: t("kumiko.dashboard.panel.error.isolated") })] }));
201
+ }
202
+ function PanelSkeleton({ shape }) {
203
+ const t = useTranslation();
204
+ if (shape === "rows")
205
+ return _jsx(LoadingState, { rows: 3 });
206
+ if (shape === "segments") {
207
+ return (_jsx("output", { "aria-label": t("kumiko.widget.loading"), className: "flex flex-col gap-4", children: SKELETON_SEGMENT_WIDTHS.map((width) => (_jsxs("div", { className: "flex flex-col gap-1.5", children: [_jsx(Skeleton, { className: "h-3 w-24" }), _jsx(Skeleton, { className: "h-2.5 rounded-full", style: { width } })] }, width))) }));
208
+ }
209
+ return (_jsx("output", { "aria-label": t("kumiko.widget.loading"), className: "flex h-40 items-end gap-1", children: SKELETON_BAR_HEIGHTS.map((height) => (_jsx(Skeleton, { className: "flex-1", style: { height } }, height))) }));
210
+ }
211
+ function QueryPanel({ panel, label, subtitle, action, screenParams, translate, skeleton, isEmpty, children, }) {
212
+ const formats = useDashboardFormats();
213
+ const { data, error, loading, refetch } = useDashboardQuery(panel.query, panelPayload(panel, screenParams));
214
+ useReportPanelLoaded(data);
215
+ let body;
216
+ if (loading && data === null) {
217
+ body = _jsx(PanelSkeleton, { shape: skeleton });
218
+ }
219
+ else if (error !== null) {
220
+ body = _jsx(PanelError, { label: label, error: error, onRetry: () => void refetch() });
221
+ }
222
+ else if (data === null || isEmpty(data)) {
223
+ body = (_jsx(EmptyState, { title: translate(panel.emptyLabel ?? "kumiko.list.no-entries"), description: panel.emptyHint !== undefined ? translate(panel.emptyHint) : undefined }));
224
+ }
225
+ else {
226
+ body = children(data, formats);
227
+ }
228
+ return (_jsx(PanelShell, { testId: `dashboard-panel-${panel.id}`, title: label, subtitle: subtitle, action: action, children: body }));
229
+ }
230
+ function isSparkPoint(value) {
231
+ return (typeof value === "object" &&
232
+ value !== null &&
233
+ "value" in value &&
234
+ (typeof value.value === "number" || value.value === null));
235
+ }
236
+ function readSparkline(panel, record) {
237
+ if (panel.sparklineField === undefined)
238
+ return undefined;
239
+ const raw = record[panel.sparklineField];
240
+ if (!Array.isArray(raw))
241
+ return undefined;
242
+ return raw.filter(isSparkPoint).flatMap((p) => (p.value === null ? [] : [p.value]));
243
+ }
244
+ function readTone(raw) {
245
+ return isOneOf(DASHBOARD_STAT_TONES, raw) ? raw : undefined;
246
+ }
247
+ function fallbackText(raw, placeholder) {
248
+ if (raw === undefined || raw === null || isRecord(raw))
249
+ return placeholder;
250
+ return String(raw);
251
+ }
252
+ function StatPanelBody({ panel, label, screenId, screenParams, translate, variant, }) {
253
+ const formats = useDashboardFormats();
254
+ // Resolved HERE (not in a separate always-rendered child) so `icon` on
255
+ // <StatCard> is `undefined` — not a React element that renders empty —
256
+ // when the icon name isn't registered. StatCard gates its accent chip on
257
+ // `icon !== undefined`, so a resolved-but-hidden element used to leave a
258
+ // stray accent-colored chip next to the label.
259
+ const iconName = panel.icon !== undefined ? extensionSectionName(panel.icon) : undefined;
260
+ const Icon = useExtensionSectionComponent(iconName);
261
+ useEffect(() => {
262
+ if (panel.icon !== undefined && iconName !== undefined && Icon === undefined) {
263
+ // biome-ignore lint/suspicious/noConsole: dev-warning für Setup-Fehler
264
+ console.warn(`[kumiko] Dashboard stat-panel "${panel.id}" on screen "${screenId}" references icon ` +
265
+ `"${iconName}", which is not registered in clientFeatures.extensionSectionComponents.`);
266
+ }
267
+ }, [panel.icon, panel.id, iconName, Icon, screenId]);
268
+ const { data, error, loading, refetch } = useDashboardQuery(panel.query, panelPayload(panel, screenParams));
269
+ useReportPanelLoaded(data);
270
+ if (loading && data === null)
271
+ return _jsx(LoadingState, { rows: 2 });
272
+ if (error !== null) {
273
+ return _jsx(PanelError, { label: label, error: error, onRetry: () => void refetch() });
274
+ }
275
+ const record = data ?? {};
276
+ const rawValue = record[panel.valueField];
277
+ const tone = readTone(panel.toneField !== undefined ? record[panel.toneField] : undefined) ??
278
+ // A fixed tone must not paint an all-clear "0" as an alarm.
279
+ (rawValue === 0 ? undefined : panel.tone);
280
+ const sub = panel.subField !== undefined ? record[panel.subField] : undefined;
281
+ const delta = readDelta(panel, record);
282
+ const valueFormat = panel.valueFormat;
283
+ let value;
284
+ if (typeof rawValue !== "number") {
285
+ value =
286
+ resolveDashboardText(rawValue, translate, formats) ??
287
+ fallbackText(rawValue, EMPTY_VALUE_PLACEHOLDER);
288
+ }
289
+ else if (valueFormat?.kind === "relativeTime") {
290
+ value = formats.formatRelative(rawValue);
291
+ }
292
+ else {
293
+ value = formats.formatPanelValue(valueFormat)(rawValue);
294
+ }
295
+ const spark = readSparkline(panel, record);
296
+ const testId = `dashboard-panel-${panel.id}`;
297
+ const subText = resolveDashboardText(sub, translate, formats) ?? fallbackText(sub, undefined);
298
+ const icon = Icon !== undefined ? (_jsx(Icon, { entityName: screenId, entityId: null, screenId: screenId, filterParams: screenParams.filterParams })) : undefined;
299
+ if (variant === "strip") {
300
+ return (_jsx(StatStripCell, { icon: icon, label: label, value: value, tone: tone ?? "default", ...(Icon !== undefined && { accentColor: panel.accentColor }), ...(subText !== undefined && { sub: subText }), ...(delta !== undefined && { delta }), ...(spark !== undefined && { spark }), testId: testId }));
301
+ }
302
+ return (_jsx(StatCard, { icon: icon, label: label, value: value, tone: tone ?? "default", ...(Icon !== undefined && { accentColor: panel.accentColor }), ...(subText !== undefined && { sub: subText }), ...(delta !== undefined && { delta }), ...(spark !== undefined && { spark }), testId: testId }));
303
+ }
304
+ function readDelta(panel, record) {
305
+ if (panel.deltaField === undefined || panel.deltaDirectionField === undefined)
306
+ return undefined;
307
+ const value = record[panel.deltaField];
308
+ const direction = record[panel.deltaDirectionField];
309
+ if (value === undefined || value === null)
310
+ return undefined;
311
+ if (direction !== "up" && direction !== "down")
312
+ return undefined;
313
+ const tone = readTone(panel.deltaToneField !== undefined ? record[panel.deltaToneField] : undefined);
314
+ return { value: String(value), direction, ...(tone !== undefined && { tone }) };
315
+ }
316
+ function StatGroupPanelBody({ panel, label, screenId, screenParams, translate, }) {
317
+ const testId = `dashboard-panel-${panel.id}`;
318
+ if (label === undefined) {
319
+ return (_jsx("div", { "data-testid": testId, className: "grid grid-cols-2 gap-y-2 rounded-lg border border-border bg-card py-1 lg:auto-cols-fr lg:grid-flow-col lg:divide-x lg:divide-border-row", children: panel.stats.map((stat) => (_jsx(StatPanelBody, { panel: stat, label: translate(stat.label), screenId: screenId, screenParams: screenParams, translate: translate, variant: "strip" }, stat.id))) }));
320
+ }
321
+ return (_jsx(SectionCard, { title: label, ...(panel.subtitle !== undefined && { subtitle: translate(panel.subtitle) }), testId: testId, children: _jsx("section", { className: "grid grid-cols-[repeat(auto-fit,minmax(min(12rem,100%),1fr))] gap-3", children: panel.stats.map((stat) => (_jsx(StatPanelBody, { panel: stat, label: translate(stat.label), screenId: screenId, screenParams: screenParams, translate: translate, variant: "card" }, stat.id))) }) }));
322
+ }
323
+ function markerKindColor(kind) {
324
+ if (kind === undefined)
325
+ return undefined;
326
+ if (kind.color !== undefined)
327
+ return kind.color;
328
+ return kind.tone !== undefined ? chartToneColor(kind.tone) : undefined;
329
+ }
330
+ function resolveMarkers(markers, translate, formats, markerKinds) {
331
+ return markers?.map((marker) => {
332
+ const color = typeof marker.kind === "string" ? markerKindColor(markerKinds?.[marker.kind]) : undefined;
333
+ const legendKey = typeof marker.kind === "string" ? markerKinds?.[marker.kind]?.label : undefined;
334
+ const legendLabel = legendKey !== undefined ? translate(legendKey) : undefined;
335
+ return {
336
+ atMs: marker.atMs,
337
+ label: resolveDashboardText(marker.label, translate, formats) ?? "",
338
+ ...(color !== undefined && { color }),
339
+ ...(legendLabel !== undefined && { legendLabel }),
340
+ };
341
+ });
342
+ }
343
+ // A plain bar chart ("changes per day") ships `points` without `series`.
344
+ const SINGLE_SERIES_KEY = "value";
345
+ function chartSeries(data, panelLabel, translate) {
346
+ if (data.series !== undefined && data.series.length > 0) {
347
+ return data.series.map((s) => ({ ...s, label: translate(s.label) }));
348
+ }
349
+ if (data.points !== undefined && data.points.length > 0) {
350
+ return [{ key: SINGLE_SERIES_KEY, label: panelLabel, points: data.points }];
351
+ }
352
+ return [];
353
+ }
354
+ function distinctBucketCount(series) {
355
+ return new Set(series.flatMap((s) => s.points.map((p) => p.atMs))).size;
356
+ }
357
+ function isChartEmpty(chart, data, series) {
358
+ if (chart === "segment-bars")
359
+ return (data.rows ?? []).length === 0;
360
+ if (chart === "stacked-bars")
361
+ return distinctBucketCount(series) === 0;
362
+ if (chart === "stacked-area")
363
+ return distinctBucketCount(series) < 2;
364
+ const measured = (data.points ?? []).filter((p) => p.value !== null);
365
+ return measured.length < 2;
366
+ }
367
+ function windowOf(data, series) {
368
+ const times = series.flatMap((s) => s.points.map((p) => p.atMs));
369
+ const startMs = data.windowStartMs ?? (times.length > 0 ? Math.min(...times) : 0);
370
+ const endMs = data.windowEndMs ?? (times.length > 0 ? Math.max(...times) : 1);
371
+ return { startMs, endMs };
372
+ }
373
+ function ChartPanelBody({ panel, label, screenParams, translate, }) {
374
+ const t = useTranslation();
375
+ const todayLabel = t("kumiko.dashboard.today");
376
+ const tones = panel.seriesTones;
377
+ const colors = panel.seriesColors;
378
+ const testId = `dashboard-chart-${panel.id}`;
379
+ const ranges = panel.chart === "stacked-area" && panel.ranges !== undefined
380
+ ? {
381
+ default: panel.ranges.default,
382
+ options: panel.ranges.options.map((option) => ({
383
+ ...option,
384
+ label: translate(option.label),
385
+ })),
386
+ }
387
+ : undefined;
388
+ const [windowSelection, setWindowSelection] = useState(() => initialWindowSelection(ranges, panel.chart === "stacked-area" ? panel.initialWindow : undefined));
389
+ const rangeSwitch = ranges !== undefined ? (_jsx(ModeSwitch, { value: windowSelection?.kind === "range" ? windowSelection.value : null, options: ranges.options.map((option) => ({
390
+ value: option.value,
391
+ label: option.label,
392
+ })), onChange: (value) => setWindowSelection({ kind: "range", value }), ariaLabel: label, variant: "pill", testId: `dashboard-chart-range-${panel.id}` })) : undefined;
393
+ return (_jsx(QueryPanel, { panel: panel, label: label, subtitle: panel.subtitle !== undefined ? translate(panel.subtitle) : undefined, action: rangeSwitch, screenParams: screenParams, translate: translate, skeleton: panel.chart === "segment-bars" ? "segments" : "bars", isEmpty: (data) => isChartEmpty(panel.chart, data, chartSeries(data, label, translate)), children: (data, formats) => {
394
+ const series = chartSeries(data, label, translate);
395
+ const { startMs, endMs } = windowOf(data, series);
396
+ const markers = resolveMarkers(data.markers, translate, formats, panel.markerKinds);
397
+ const formatValue = formats.formatPanelValue(panel.valueFormat);
398
+ if (panel.chart === "segment-bars") {
399
+ return (_jsx(SegmentBarChart, { rows: (data.rows ?? []).map((row) => ({
400
+ ...row,
401
+ label: translate(row.label),
402
+ segments: row.segments.map((seg) => ({ ...seg, label: translate(seg.label) })),
403
+ })), tones: tones, colors: colors, ariaLabel: label, formatValue: formatValue, testId: testId }));
404
+ }
405
+ if (panel.chart === "stacked-bars") {
406
+ const times = series.flatMap((s) => s.points.map((p) => p.atMs));
407
+ const spacing = Math.min(...[...new Set(times)]
408
+ .sort((a, b) => a - b)
409
+ .flatMap((atMs, i, all) => {
410
+ const prev = all[i - 1];
411
+ return prev === undefined ? [] : [atMs - prev];
412
+ }), DAY_MS);
413
+ return (_jsx(StackedBarChart, { series: series, windowEndMs: endMs, tones: tones, colors: colors, ariaLabel: label, todayLabel: todayLabel, formatBucketLabel: spacing < DAY_MS - HOUR_MS ? formats.formatHour : formats.formatDay, formatValue: formatValue, formatAxisValue: formats.formatPanelAxisValue(panel.valueFormat), testId: testId }));
414
+ }
415
+ if (panel.chart === "stacked-area") {
416
+ const lines = (data.lines ?? []).map((line) => ({
417
+ ...line,
418
+ label: translate(line.label),
419
+ }));
420
+ const todayMs = data.todayMs ?? undefined;
421
+ const formatDateByFormat = (atMs, format) => format === "month" ? formats.formatMonth(atMs) : formats.formatDay(atMs);
422
+ return (_jsx(StackedAreaChart, { series: series, lines: lines, windowStartMs: startMs, windowEndMs: endMs, ...(todayMs !== undefined && { todayMs }), tones: tones, colors: colors, markers: markers, ...(ranges !== undefined && { ranges }), brush: panel.brush === true, windowSelection: windowSelection, onWindowSelectionChange: setWindowSelection, brushLabels: {
423
+ start: t("kumiko.dashboard.brush-start"),
424
+ end: t("kumiko.dashboard.brush-end"),
425
+ }, ariaLabel: label, todayLabel: todayLabel, formatBucketLabel: formatDateByFormat, formatValue: formatValue, formatAxisValue: formats.formatPanelAxisValue(panel.valueFormat), markerLabels: panel.markerLabels === true, formatMarkerTime: formatDateByFormat, timeZone: formats.timeZone, ...(panel.dateFormat !== undefined && { dateFormat: panel.dateFormat }), ...(panel.markerLegend !== undefined && { markerLegend: panel.markerLegend }), scrollable: panel.scrollable === true, showLegendTotals: panel.legendTotals !== false, emptyContent: _jsx(EmptyState, { title: translate(panel.emptyLabel ?? "kumiko.list.no-entries") }), testId: testId }));
426
+ }
427
+ return (_jsx(TimeseriesChart, { points: data.points ?? [], windowStartMs: startMs, windowEndMs: endMs, markers: markers, formatMarkerTime: formats.formatDay, ...((panel.valueFormat !== undefined || panel.unit !== undefined) && {
428
+ yAxis: {
429
+ ticks: 3,
430
+ format: formats.formatPanelAxisValue(panel.valueFormat),
431
+ ...(panel.unit !== undefined && { unit: translate(panel.unit) }),
432
+ },
433
+ }), ariaLabel: label, testId: testId }));
434
+ } }));
435
+ }
436
+ function ListPanelBody({ panel, label, screenParams, translate, }) {
437
+ const { formatPercent, formatNumber, formatDateTime } = useDashboardFormats();
438
+ const columns = panel.columns.map((c) => {
439
+ const normalized = normalizeListColumn(c);
440
+ return {
441
+ field: normalized.field,
442
+ label: translate(normalized.label ?? normalized.field),
443
+ ...(normalized.display !== undefined && { display: normalized.display }),
444
+ ...(normalized.badgeToneField !== undefined && {
445
+ badgeToneField: normalized.badgeToneField,
446
+ }),
447
+ };
448
+ });
449
+ return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data) => (_jsx(DashboardListTable, { columns: columns, rows: data.rows ?? [], formatPercent: formatPercent, formatNumber: formatNumber, formatDateTime: formatDateTime, translate: translate })) }));
450
+ }
451
+ function FeedPanelBody({ panel, label, screenParams, translate, }) {
452
+ return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data, formats) => {
453
+ const rows = (data.rows ?? []).map((row, i) => {
454
+ const trailing = resolveDashboardText(row.trailing, translate, formats);
455
+ return {
456
+ id: String(i),
457
+ primary: resolveDashboardText(row.primary, translate, formats) ?? EMPTY_VALUE_PLACEHOLDER,
458
+ ...(trailing !== undefined && { trailing }),
459
+ };
460
+ });
461
+ return _jsx(FeedList, { rows: rows });
462
+ } }));
463
+ }
464
+ function isFiniteNumber(value) {
465
+ return typeof value === "number" && Number.isFinite(value);
466
+ }
467
+ function isProgressTone(value) {
468
+ return value === "default" || value === "danger";
469
+ }
470
+ function progressUsageOf(row) {
471
+ const { used, limit } = row;
472
+ if (!isFiniteNumber(used))
473
+ return undefined;
474
+ if (limit === null)
475
+ return { used, limit: null };
476
+ return isFiniteNumber(limit) ? { used, limit } : undefined;
477
+ }
478
+ function toProgressListRow(row, index, translate, formats) {
479
+ const sub = resolveDashboardText(row["sub"], translate, formats);
480
+ const usage = progressUsageOf(row);
481
+ const explicitTone = isProgressTone(row["tone"]) ? row["tone"] : undefined;
482
+ const common = {
483
+ id: String(index),
484
+ label: resolveDashboardText(row["label"], translate, formats) ?? EMPTY_VALUE_PLACEHOLDER,
485
+ ...(sub !== undefined && { sub }),
486
+ };
487
+ if (usage === undefined) {
488
+ return {
489
+ ...common,
490
+ value: resolveDashboardText(row["value"], translate, formats) ?? EMPTY_VALUE_PLACEHOLDER,
491
+ fraction: isFiniteNumber(row["fraction"]) ? row["fraction"] : 0,
492
+ ...(explicitTone !== undefined && { tone: explicitTone }),
493
+ };
494
+ }
495
+ const { used, limit } = usage;
496
+ const usedText = formats.formatNumber(used);
497
+ const limitText = limit === null ? translate("kumiko.dashboard.progress.unlimited") : formats.formatNumber(limit);
498
+ const exhausted = limit !== null && used >= limit;
499
+ const tone = explicitTone ?? (exhausted ? "danger" : undefined);
500
+ let fraction = null;
501
+ if (limit !== null)
502
+ fraction = limit === 0 ? 1 : used / limit;
503
+ return {
504
+ ...common,
505
+ value: `${usedText} / ${limitText}`,
506
+ fraction,
507
+ ...(tone !== undefined && { tone }),
508
+ };
509
+ }
510
+ function ProgressListPanelBody({ panel, label, screenParams, translate, }) {
511
+ return (_jsx(QueryPanel, { panel: panel, label: label, screenParams: screenParams, translate: translate, skeleton: "rows", isEmpty: (data) => (data.rows ?? []).length === 0, children: (data, formats) => {
512
+ const rows = (data.rows ?? []).map((row, i) => toProgressListRow(row, i, translate, formats));
513
+ return _jsx(ProgressList, { rows: rows });
514
+ } }));
515
+ }
516
+ function TextBlockView({ block, translate, }) {
517
+ switch (block.kind) {
518
+ case "heading":
519
+ return (_jsx("h3", { "data-block": "heading", className: "text-sm font-semibold text-foreground", children: translate(block.text) }));
520
+ case "paragraph":
521
+ return (_jsx("p", { "data-block": "paragraph", className: block.emphasis === "strong"
522
+ ? "text-sm font-medium text-foreground"
523
+ : "text-sm text-muted-foreground", children: translate(block.text) }));
524
+ case "list":
525
+ return (_jsx("ul", { "data-block": "list", className: "list-disc space-y-1 pl-5 text-sm text-muted-foreground", children: block.items.map((item) => (_jsx("li", { children: translate(item) }, item))) }));
526
+ case "code":
527
+ return (_jsx("pre", { "data-block": "code", className: "overflow-x-auto rounded-md bg-muted p-3 text-xs", children: _jsx("code", { children: block.code }) }));
528
+ case "hint":
529
+ return (_jsx("p", { "data-block": "hint", className: "text-xs text-muted-foreground", children: translate(block.text) }));
530
+ }
531
+ }
532
+ function textBlockKey(block) {
533
+ switch (block.kind) {
534
+ case "list":
535
+ return `list:${block.items.join("|")}`;
536
+ case "code":
537
+ return `code:${block.code}`;
538
+ default:
539
+ return `${block.kind}:${block.text}`;
540
+ }
541
+ }
542
+ function TextPanelBody({ panel, label, translate, }) {
543
+ const blocks = (_jsx("div", { className: "flex flex-col gap-3", children: panel.blocks.map((block) => (_jsx(TextBlockView, { block: block, translate: translate }, textBlockKey(block)))) }));
544
+ const testId = `dashboard-panel-${panel.id}`;
545
+ if (label !== undefined) {
546
+ return (_jsx(PanelShell, { testId: testId, title: label, children: blocks }));
547
+ }
548
+ return (_jsx("section", { "data-testid": testId, className: "h-full rounded-lg border border-border bg-card p-4 text-card-foreground", children: blocks }));
549
+ }
550
+ // Löst panel.component über dieselbe extensionSectionComponents-Registry auf
551
+ // wie entityEdit-Extension-Sections und List-Header-Slots — bleibt an seiner
552
+ // Array-Position statt in einen separaten Slot zu wandern (siehe Datei-Kopf-
553
+ // Kommentar zur Registry). Rendert nichts + dev-Warnung bei unregistriertem
554
+ // Namen, analog zu ListHeaderSlotMount in render-list.tsx.
555
+ function CustomPanelBody({ panel, screenId, filterParams, }) {
556
+ const name = extensionSectionName(panel.component);
557
+ const Component = useExtensionSectionComponent(name);
558
+ useEffect(() => {
559
+ if (name !== undefined && Component === undefined) {
560
+ // biome-ignore lint/suspicious/noConsole: dev-warning für Setup-Fehler
561
+ console.warn(`[kumiko] Dashboard custom-panel "${panel.id}" on screen "${screenId}" references component ` +
562
+ `"${name}", which is not registered in clientFeatures.extensionSectionComponents — the panel renders nothing.`);
563
+ }
564
+ }, [name, Component, panel.id, screenId]);
565
+ if (Component === undefined)
566
+ return null;
567
+ // Dashboard-Panels haben keine Entity — entityName trägt die screen.id,
568
+ // damit ExtensionSectionProps nicht extra für diesen einen Mount-Ort
569
+ // aufgeweicht werden muss (das bräche jede bestehende registrierte
570
+ // Section, die entityName als garantiert gesetzten string erwartet).
571
+ return (_jsx(Component, { entityName: screenId, entityId: null, screenId: screenId, filterParams: filterParams }));
572
+ }
573
+ // Value lives in the URL under `filter.id` (like useListUrlState's
574
+ // `<screenId>.page`, same replaceState) so a `navigate` rowAction with
575
+ // `params` can deep-link onto this filter.
576
+ function useFilterParams(screen, enabled) {
577
+ const { Field, Input } = usePrimitives();
578
+ const t = useTranslation();
579
+ const nav = useNav();
580
+ const filter = screen.filter;
581
+ const value = filter !== undefined ? (nav.searchParams[filter.id] ?? "") : "";
582
+ const optionsQueryResult = useQuery(filter?.optionsQuery ?? "", {}, { enabled: enabled && filter?.optionsQuery !== undefined });
583
+ if (filter === undefined) {
584
+ return { params: {}, picker: null };
585
+ }
586
+ const dynamicOptions = filter.optionsQuery !== undefined
587
+ ? (optionsQueryResult.data?.rows ?? [])
588
+ : (filter.options ?? []).map((o) => ({ value: o.value, label: t(o.label) }));
589
+ const allLabel = t(filter.allLabel ?? "kumiko.dashboard.filter.all");
590
+ const options = [{ value: "", label: allLabel }, ...dynamicOptions];
591
+ 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 }) }) }));
592
+ const params = value === "" ? {} : { [filter.id]: value };
593
+ return { params, picker };
594
+ }
595
+ // An unknown/stale URL value falls back to the declared default.
596
+ function useTimeRange(screen, translate) {
597
+ const t = useTranslation();
598
+ const nav = useNav();
599
+ const resolver = useLocale();
600
+ const timeRange = screen.timeRange;
601
+ if (timeRange === undefined)
602
+ return { params: {}, control: null };
603
+ const fromUrl = nav.searchParams[timeRange.id];
604
+ const value = timeRange.options.find((o) => o.value === fromUrl)?.value ?? timeRange.default;
605
+ return {
606
+ // Metric buckets are cut server-side; without the user's zone they default
607
+ // to UTC while the axis labels are formatted in the user's zone.
608
+ params: { [timeRange.id]: value, timeZone: resolver.timeZone() },
609
+ 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}` })),
610
+ };
611
+ }
612
+ function panelSpanClassName(panel) {
613
+ if (panel.kind === "stat")
614
+ return undefined;
615
+ if ("span" in panel && panel.span !== undefined)
616
+ return panel.span === "half" ? HALF_PANEL : WIDE_PANEL;
617
+ if (panel.kind === "feed" || panel.kind === "progress-list")
618
+ return HALF_PANEL;
619
+ return WIDE_PANEL;
620
+ }
621
+ function PanelBody({ panel, label, screenId, screenParams, translate, }) {
622
+ if (panel.kind === "stat-group") {
623
+ return (_jsx(StatGroupPanelBody, { panel: panel, label: label, screenId: screenId, screenParams: screenParams, translate: translate }));
624
+ }
625
+ if (panel.kind === "custom") {
626
+ return (_jsx(CustomPanelBody, { panel: panel, screenId: screenId, filterParams: {
627
+ ...screenParams.filterParams,
628
+ ...screenParams.rangeParams,
629
+ ...screenParams.recordParams,
630
+ } }));
631
+ }
632
+ const text = label ?? "";
633
+ if (panel.kind === "stat") {
634
+ return (_jsx(StatPanelBody, { panel: panel, label: text, screenId: screenId, screenParams: screenParams, translate: translate, variant: "card" }));
635
+ }
636
+ if (panel.kind === "chart") {
637
+ return (_jsx(ChartPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
638
+ }
639
+ if (panel.kind === "list") {
640
+ return (_jsx(ListPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
641
+ }
642
+ if (panel.kind === "feed") {
643
+ return (_jsx(FeedPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
644
+ }
645
+ if (panel.kind === "text") {
646
+ return _jsx(TextPanelBody, { panel: panel, label: label, translate: translate });
647
+ }
648
+ return (_jsx(ProgressListPanelBody, { panel: panel, label: text, screenParams: screenParams, translate: translate }));
649
+ }
650
+ const VisibilityQueryContext = createContext(new Map());
651
+ const NO_GATE_PAYLOAD = {};
652
+ function visibilityGate(query, payload) {
653
+ return { key: `${query}|${JSON.stringify(payload)}`, query, payload };
654
+ }
655
+ // Embedded screens carry no screen filter; in-grid panels pass the filter
656
+ // (unless ignoreScreenFilter, never for a stat-group) and the time range.
657
+ function panelVisibilityGate(panel, screenParams) {
658
+ if (panel.kind === "custom" || panel.visibleWhen === undefined)
659
+ return undefined;
660
+ if (panel.kind === "screen")
661
+ return visibilityGate(panel.visibleWhen.query, NO_GATE_PAYLOAD);
662
+ const dropsFilter = panel.kind !== "stat-group" && panel.kind !== "text" && panel.ignoreScreenFilter === true;
663
+ return visibilityGate(panel.visibleWhen.query, {
664
+ ...(dropsFilter ? {} : screenParams.filterParams),
665
+ ...screenParams.rangeParams,
666
+ ...screenParams.recordParams,
667
+ });
668
+ }
669
+ // One live query per distinct gate (query + payload), shared by every panel
670
+ // that gates on it — N panels on the same status query would otherwise each
671
+ // open their own request, SSE subscription and refetch per event. The queries
672
+ // run as keyed siblings of the grid so a changing gate count never changes the
673
+ // grid's position in the tree (which would remount every panel).
674
+ function VisibilityGateQuery({ gate, onResult, onForget, }) {
675
+ const { data, error, loading, refetch } = useDashboardQuery(gate.query, gate.payload);
676
+ const { key } = gate;
677
+ useEffect(() => {
678
+ onResult(key, { data, error, loading, refetch });
679
+ }, [key, data, error, loading, refetch, onResult]);
680
+ useEffect(() => () => onForget(key), [key, onForget]);
681
+ return null;
682
+ }
683
+ function VisibilityGateQueries({ gates, onResult, onForget, }) {
684
+ return gates.map((gate) => (_jsx(VisibilityGateQuery, { gate: gate, onResult: onResult, onForget: onForget }, gate.key)));
685
+ }
686
+ function useVisibilityGateResults() {
687
+ const [results, setResults] = useState(new Map());
688
+ const report = useCallback((key, result) => {
689
+ setResults((prev) => new Map(prev).set(key, result));
690
+ }, []);
691
+ const forget = useCallback((key) => {
692
+ setResults((prev) => {
693
+ if (!prev.has(key))
694
+ return prev;
695
+ const next = new Map(prev);
696
+ next.delete(key);
697
+ return next;
698
+ });
699
+ }, []);
700
+ return { results, report, forget };
701
+ }
702
+ function distinctVisibilityGates(panels, screenParams) {
703
+ const gates = new Map();
704
+ for (const panel of panels) {
705
+ const gate = panelVisibilityGate(panel, screenParams);
706
+ if (gate !== undefined)
707
+ gates.set(gate.key, gate);
708
+ }
709
+ return [...gates.values()];
710
+ }
711
+ function GatedGridCell({ panel, screenParams, label, className, children, }) {
712
+ const gate = panelVisibilityGate(panel, screenParams);
713
+ const visibility = useContext(VisibilityQueryContext).get(gate?.key ?? "");
714
+ const visibleWhen = panel.kind === "custom" ? undefined : panel.visibleWhen;
715
+ if (visibleWhen !== undefined) {
716
+ const verdict = evalVisibleWhen(visibleWhen, visibility);
717
+ if (verdict === "error" && visibility?.error) {
718
+ 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() }) }));
719
+ }
720
+ // Loading renders nothing too: a skeleton that then vanishes flickers.
721
+ if (verdict !== "visible")
722
+ return null;
723
+ }
724
+ return _jsx("div", { className: cn("min-w-0", className), children: children });
725
+ }
726
+ function EmbeddedScreenPanelTile({ panel, target, translate, }) {
727
+ const screenBody = _jsx(KumikoScreen, { schema: target.schema, qn: target.qn, translate: translate });
728
+ const embedded = panel.chromeless === true ? (_jsx(EmbeddedFormProvider, { children: _jsx(EmbeddedScreenProvider, { children: screenBody }) })) : (screenBody);
729
+ 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] })) }));
730
+ }
731
+ // Own component so the target lookup (a hook) only runs for panels that actually fall back.
732
+ function FallbackScreenPanelTile({ panel, fallback, featureName, translate, }) {
733
+ const target = useEmbeddedScreen(featureName, fallback);
734
+ if (target === undefined)
735
+ return null;
736
+ return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
737
+ }
738
+ function ScreenPanelTile({ panel, featureName, translate, }) {
739
+ const visibleWhen = panel.visibleWhen;
740
+ const visibility = useContext(VisibilityQueryContext).get(visibleWhen !== undefined ? visibilityGate(visibleWhen.query, NO_GATE_PAYLOAD).key : "");
741
+ const target = useEmbeddedScreen(featureName, panel.screen);
742
+ const verdict = visibleWhen === undefined ? "visible" : evalVisibleWhen(visibleWhen, visibility);
743
+ // Nothing while the gate loads: a fallback that flashes before the real panel flickers.
744
+ if (verdict === "loading")
745
+ return null;
746
+ if (verdict !== "visible" && panel.fallback !== undefined) {
747
+ return (_jsx(FallbackScreenPanelTile, { panel: panel, fallback: panel.fallback, featureName: featureName, translate: translate }));
748
+ }
749
+ if (target === undefined)
750
+ return null;
751
+ // A failed gate query must not silently drop the panel: on the
752
+ // account-security page that would hide every MFA enable/disable path.
753
+ if (verdict === "error" && visibility?.error) {
754
+ 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() }) }));
755
+ }
756
+ if (verdict !== "visible")
757
+ return null;
758
+ return _jsx(EmbeddedScreenPanelTile, { panel: panel, target: target, translate: translate });
759
+ }
760
+ // Starts at render time and restarts whenever the merged query params change;
761
+ // panels additionally report each successful (re)load, so live refetches and
762
+ // retries keep the stamp current.
763
+ function useLoadedAt(screenParams) {
764
+ const paramsKey = JSON.stringify([
765
+ screenParams.filterParams,
766
+ screenParams.rangeParams,
767
+ screenParams.recordParams,
768
+ ]);
769
+ const [loaded, setLoaded] = useState(() => ({ paramsKey, atMs: Date.now() }));
770
+ if (loaded.paramsKey !== paramsKey)
771
+ setLoaded({ paramsKey, atMs: Date.now() });
772
+ const reportLoaded = useCallback((atMs) => setLoaded((prev) => (prev.atMs >= atMs ? prev : { ...prev, atMs })), []);
773
+ return { loadedAtMs: loaded.atMs, reportLoaded };
774
+ }
775
+ const PanelLoadedContext = createContext(() => { });
776
+ function useReportPanelLoaded(data) {
777
+ const reportLoaded = useContext(PanelLoadedContext);
778
+ useEffect(() => {
779
+ if (data !== null)
780
+ reportLoaded(Date.now());
781
+ }, [data, reportLoaded]);
782
+ }
783
+ const NO_RECORD_ROW = {};
784
+ function recordTitleText(row, field) {
785
+ if (row === null || field === undefined)
786
+ return undefined;
787
+ const value = row[field];
788
+ if (typeof value !== "string" && typeof value !== "number")
789
+ return undefined;
790
+ return String(value) === "" ? undefined : String(value);
791
+ }
792
+ export function WebDashboardBody({ featureName, screen, translate, }) {
793
+ const t = useTranslation();
794
+ const effectiveTranslate = translate ?? t;
795
+ const { Text, Banner, PageHeader, Heading, Button, Dialog, ActionOverflowMenu, Grid } = usePrimitives();
796
+ const nav = useNav();
797
+ const headerSlotAvailable = usePageHeaderSlotAvailable();
798
+ const headerCompactContext = usePageHeaderCompact();
799
+ const { formatDateTime } = useDashboardFormats();
800
+ const recordBinding = screen.record;
801
+ const recordId = recordBinding !== undefined ? (nav.searchParams[recordBinding.param] ?? "") : "";
802
+ const recordMissing = recordBinding !== undefined && recordId === "";
803
+ const recordPayloadKey = recordBinding?.payloadKey ?? recordBinding?.param;
804
+ const recordParams = recordPayloadKey !== undefined && recordId !== "" ? { [recordPayloadKey]: recordId } : {};
805
+ const recordQueryName = recordBinding?.query;
806
+ const { params: filterParams, picker } = useFilterParams(screen, !recordMissing);
807
+ const { params: rangeParams, control: rangeControl } = useTimeRange(screen, effectiveTranslate);
808
+ const screenParams = { filterParams, rangeParams, recordParams };
809
+ const { loadedAtMs, reportLoaded } = useLoadedAt(screenParams);
810
+ const visibilityGates = distinctVisibilityGates(screen.panels, screenParams);
811
+ const gateResults = useVisibilityGateResults();
812
+ // The record query is not a panel query: it needs the interval before the
813
+ // registry context below exists, and `refresh.field` reads from its own row.
814
+ const [refreshIntervalMs, setRefreshIntervalMs] = useState(() => resolveRefreshIntervalMs(screen.refresh, null));
815
+ const recordQuery = useQuery(recordQueryName ?? "", recordParams, {
816
+ enabled: recordQueryName !== undefined && !recordMissing,
817
+ live: true,
818
+ ...(refreshIntervalMs !== undefined && { refetchIntervalMs: refreshIntervalMs }),
819
+ });
820
+ const { data: recordRow, refetch: refetchRecord } = recordQuery;
821
+ useEffect(() => {
822
+ setRefreshIntervalMs(resolveRefreshIntervalMs(screen.refresh, recordRow));
823
+ }, [screen.refresh, recordRow]);
824
+ const { register, refetchRegistered } = useRefetchRegistry();
825
+ const queryRuntime = useMemo(() => ({ refreshIntervalMs, register }), [refreshIntervalMs, register]);
826
+ const refetchAll = useCallback(async () => {
827
+ await Promise.all([
828
+ recordQueryName !== undefined ? refetchRecord() : Promise.resolve(),
829
+ refetchRegistered(),
830
+ ]);
831
+ }, [recordQueryName, refetchRecord, refetchRegistered]);
832
+ const [actionError, setActionError] = useState(null);
833
+ const idOnlyRow = useMemo(() => (recordPayloadKey !== undefined ? { id: recordId, [recordPayloadKey]: recordId } : null), [recordId, recordPayloadKey]);
834
+ const actionRow = recordQueryName !== undefined ? recordRow : idOnlyRow;
835
+ const headerActions = useRecordActions({
836
+ actions: actionRow !== null && !recordMissing ? screen.actions : undefined,
837
+ record: actionRow ?? NO_RECORD_ROW,
838
+ recordId,
839
+ translate: effectiveTranslate,
840
+ onWriteSuccess: refetchAll,
841
+ });
842
+ const scope = screen.scope;
843
+ // A raw description is agent-facing prose; only an i18n key is user-facing copy.
844
+ const translatedDescription = screen.description !== undefined ? effectiveTranslate(screen.description) : undefined;
845
+ const description = translatedDescription !== screen.description ? translatedDescription : undefined;
846
+ const scopeBadge = scope !== undefined ? (_jsx(StatusBadge, { tone: "accent", testId: `dashboard-${screen.id}-scope`, children: effectiveTranslate(scope.badge) })) : null;
847
+ if (recordMissing) {
848
+ 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") }) }));
849
+ }
850
+ const usesPageHeaderSlot = headerSlotAvailable && PageHeader !== undefined;
851
+ const recordTitle = recordTitleText(recordRow, recordBinding?.titleField);
852
+ 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;
853
+ // The shell header owns the title; the badge and the actions sit in it there.
854
+ const badgeInHeader = scopeBadge !== null && usesPageHeaderSlot;
855
+ const showPageHeader = usesPageHeaderSlot && (badgeInHeader || recordTitle !== undefined || headerActionsBar !== null);
856
+ 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 && (_jsx(Banner, { variant: "error", testId: "render-edit-action-error", children: actionError })), 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: gateResults.results, 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) => {
857
+ if (panel.kind === "screen") {
858
+ return (_jsx(ScreenPanelTile, { panel: panel, featureName: featureName, translate: effectiveTranslate }, panel.id));
859
+ }
860
+ const label = panel.kind === "custom" || panel.label === undefined
861
+ ? undefined
862
+ : effectiveTranslate(panel.label);
863
+ 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));
864
+ }) })] })] }) }) }));
865
+ }