@ketvietlab/ketsuite 0.1.22 → 0.1.25

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 (237) hide show
  1. package/dist/cli.js +9 -3
  2. package/dist/cli.js.map +1 -1
  3. package/dist/demo/crm.d.ts +3 -0
  4. package/dist/demo/crm.d.ts.map +1 -0
  5. package/dist/demo/crm.js +418 -0
  6. package/dist/demo/crm.js.map +1 -0
  7. package/dist/demo/partner.d.ts +8 -0
  8. package/dist/demo/partner.d.ts.map +1 -0
  9. package/dist/demo/partner.js +179 -0
  10. package/dist/demo/partner.js.map +1 -0
  11. package/dist/demo/product.d.ts +25 -0
  12. package/dist/demo/product.d.ts.map +1 -0
  13. package/dist/demo/product.js +579 -0
  14. package/dist/demo/product.js.map +1 -0
  15. package/dist/demo/purchase.d.ts +4 -0
  16. package/dist/demo/purchase.d.ts.map +1 -0
  17. package/dist/demo/purchase.js +58 -0
  18. package/dist/demo/purchase.js.map +1 -0
  19. package/dist/demo/sale.d.ts +4 -0
  20. package/dist/demo/sale.d.ts.map +1 -0
  21. package/dist/demo/sale.js +52 -0
  22. package/dist/demo/sale.js.map +1 -0
  23. package/dist/demo/seed.d.ts +14 -0
  24. package/dist/demo/seed.d.ts.map +1 -0
  25. package/dist/demo/seed.js +88 -0
  26. package/dist/demo/seed.js.map +1 -0
  27. package/dist/demo/stock.d.ts +7 -0
  28. package/dist/demo/stock.d.ts.map +1 -0
  29. package/dist/demo/stock.js +26 -0
  30. package/dist/demo/stock.js.map +1 -0
  31. package/dist/demo/user.d.ts +4 -0
  32. package/dist/demo/user.d.ts.map +1 -0
  33. package/dist/demo/user.js +41 -0
  34. package/dist/demo/user.js.map +1 -0
  35. package/dist/demo/util.d.ts +7 -0
  36. package/dist/demo/util.d.ts.map +1 -0
  37. package/dist/demo/util.js +19 -0
  38. package/dist/demo/util.js.map +1 -0
  39. package/dist/modules/backend/design/client/backend-shell.d.mts +2 -2
  40. package/dist/modules/backend/design/client/backend-shell.d.mts.map +1 -1
  41. package/dist/modules/backend/design/client/backend-shell.mjs +1 -1
  42. package/dist/modules/backend/design/client/backend-shell.mjs.map +3 -3
  43. package/dist/modules/backend/design/forms.css +7 -0
  44. package/dist/modules/backend/screen.d.ts.map +1 -1
  45. package/dist/modules/backend/screen.js +11 -2
  46. package/dist/modules/backend/screen.js.map +1 -1
  47. package/dist/modules/crm/functions/access.d.ts +3 -0
  48. package/dist/modules/crm/functions/access.d.ts.map +1 -0
  49. package/dist/modules/crm/functions/access.js +89 -0
  50. package/dist/modules/crm/functions/access.js.map +1 -0
  51. package/dist/modules/crm/functions/activities.d.ts +3 -0
  52. package/dist/modules/crm/functions/activities.d.ts.map +1 -0
  53. package/dist/modules/crm/functions/activities.js +174 -0
  54. package/dist/modules/crm/functions/activities.js.map +1 -0
  55. package/dist/modules/crm/functions/cases.d.ts +3 -0
  56. package/dist/modules/crm/functions/cases.d.ts.map +1 -0
  57. package/dist/modules/crm/functions/cases.js +585 -0
  58. package/dist/modules/crm/functions/cases.js.map +1 -0
  59. package/dist/modules/crm/functions/configuration.d.ts +3 -0
  60. package/dist/modules/crm/functions/configuration.d.ts.map +1 -0
  61. package/dist/modules/crm/functions/configuration.js +331 -0
  62. package/dist/modules/crm/functions/configuration.js.map +1 -0
  63. package/dist/modules/crm/functions/gamification.d.ts +3 -0
  64. package/dist/modules/crm/functions/gamification.d.ts.map +1 -0
  65. package/dist/modules/crm/functions/gamification.js +58 -0
  66. package/dist/modules/crm/functions/gamification.js.map +1 -0
  67. package/dist/modules/crm/functions/index.d.ts +4 -0
  68. package/dist/modules/crm/functions/index.d.ts.map +1 -0
  69. package/dist/modules/crm/functions/index.js +33 -0
  70. package/dist/modules/crm/functions/index.js.map +1 -0
  71. package/dist/modules/crm/functions/modal-context.d.ts +3 -0
  72. package/dist/modules/crm/functions/modal-context.d.ts.map +1 -0
  73. package/dist/modules/crm/functions/modal-context.js +248 -0
  74. package/dist/modules/crm/functions/modal-context.js.map +1 -0
  75. package/dist/modules/crm/functions/previews.d.ts +3 -0
  76. package/dist/modules/crm/functions/previews.d.ts.map +1 -0
  77. package/dist/modules/crm/functions/previews.js +20 -0
  78. package/dist/modules/crm/functions/previews.js.map +1 -0
  79. package/dist/modules/crm/functions/shared.d.ts +31 -0
  80. package/dist/modules/crm/functions/shared.d.ts.map +1 -0
  81. package/dist/modules/crm/functions/shared.js +194 -0
  82. package/dist/modules/crm/functions/shared.js.map +1 -0
  83. package/dist/modules/crm/index.d.ts +1 -1
  84. package/dist/modules/crm/index.d.ts.map +1 -1
  85. package/dist/modules/crm/index.js +2 -2
  86. package/dist/modules/crm/index.js.map +1 -1
  87. package/dist/modules/crm/messages.d.ts +4 -1
  88. package/dist/modules/crm/messages.d.ts.map +1 -1
  89. package/dist/modules/crm/messages.js +6 -0
  90. package/dist/modules/crm/messages.js.map +1 -1
  91. package/dist/modules/crm/record-modal-labels.d.ts +9 -0
  92. package/dist/modules/crm/record-modal-labels.d.ts.map +1 -0
  93. package/dist/modules/crm/record-modal-labels.js +36 -0
  94. package/dist/modules/crm/record-modal-labels.js.map +1 -0
  95. package/dist/modules/crm/types.d.ts +5 -0
  96. package/dist/modules/crm/types.d.ts.map +1 -1
  97. package/dist/modules/crm/types.js +9 -0
  98. package/dist/modules/crm/types.js.map +1 -1
  99. package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts +352 -0
  100. package/dist/modules/crm_backend/client/crm-configuration-modal.d.mts.map +1 -0
  101. package/dist/modules/crm_backend/client/crm-configuration-modal.mjs +2 -0
  102. package/dist/modules/crm_backend/client/crm-configuration-modal.mjs.map +7 -0
  103. package/dist/modules/crm_backend/index.d.ts.map +1 -1
  104. package/dist/modules/crm_backend/index.js +5 -1
  105. package/dist/modules/crm_backend/index.js.map +1 -1
  106. package/dist/modules/crm_backend/islands.d.ts +27 -0
  107. package/dist/modules/crm_backend/islands.d.ts.map +1 -1
  108. package/dist/modules/crm_backend/islands.js +22 -0
  109. package/dist/modules/crm_backend/islands.js.map +1 -1
  110. package/dist/modules/crm_backend/messages.d.ts +30 -0
  111. package/dist/modules/crm_backend/messages.d.ts.map +1 -1
  112. package/dist/modules/crm_backend/messages.js +30 -0
  113. package/dist/modules/crm_backend/messages.js.map +1 -1
  114. package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts +55 -0
  115. package/dist/modules/crm_backend/modal/configuration-modal-view.d.ts.map +1 -0
  116. package/dist/modules/crm_backend/modal/configuration-modal-view.js +601 -0
  117. package/dist/modules/crm_backend/modal/configuration-modal-view.js.map +1 -0
  118. package/dist/modules/crm_backend/routes.d.ts.map +1 -1
  119. package/dist/modules/crm_backend/routes.js +69 -359
  120. package/dist/modules/crm_backend/routes.js.map +1 -1
  121. package/dist/modules/crm_backend/screens/configuration.d.ts +24 -32
  122. package/dist/modules/crm_backend/screens/configuration.d.ts.map +1 -1
  123. package/dist/modules/crm_backend/screens/configuration.js +183 -274
  124. package/dist/modules/crm_backend/screens/configuration.js.map +1 -1
  125. package/dist/modules/crm_backend/screens/index.d.ts +2 -2
  126. package/dist/modules/crm_backend/screens/index.d.ts.map +1 -1
  127. package/dist/modules/crm_backend/screens/index.js +1 -1
  128. package/dist/modules/crm_backend/screens/index.js.map +1 -1
  129. package/dist/modules/product/functions.d.ts +14 -1
  130. package/dist/modules/product/functions.d.ts.map +1 -1
  131. package/dist/modules/product/functions.js +36 -31
  132. package/dist/modules/product/functions.js.map +1 -1
  133. package/dist/modules/product/product-modal-context.d.ts +3 -0
  134. package/dist/modules/product/product-modal-context.d.ts.map +1 -0
  135. package/dist/modules/product/product-modal-context.js +173 -0
  136. package/dist/modules/product/product-modal-context.js.map +1 -0
  137. package/dist/modules/product/product-record-tabs.d.ts +14 -0
  138. package/dist/modules/product/product-record-tabs.d.ts.map +1 -0
  139. package/dist/modules/product/product-record-tabs.js +23 -0
  140. package/dist/modules/product/product-record-tabs.js.map +1 -0
  141. package/dist/modules/product_backend/client/product-modal.d.mts +144 -0
  142. package/dist/modules/product_backend/client/product-modal.d.mts.map +1 -0
  143. package/dist/modules/product_backend/client/product-modal.mjs +2 -0
  144. package/dist/modules/product_backend/client/product-modal.mjs.map +7 -0
  145. package/dist/modules/product_backend/index.d.ts.map +1 -1
  146. package/dist/modules/product_backend/index.js +25 -0
  147. package/dist/modules/product_backend/index.js.map +1 -1
  148. package/dist/modules/product_backend/islands.d.ts +1 -0
  149. package/dist/modules/product_backend/islands.d.ts.map +1 -1
  150. package/dist/modules/product_backend/islands.js +9 -0
  151. package/dist/modules/product_backend/islands.js.map +1 -1
  152. package/dist/modules/product_backend/modal/product-modal-view.d.ts +49 -0
  153. package/dist/modules/product_backend/modal/product-modal-view.d.ts.map +1 -0
  154. package/dist/modules/product_backend/modal/product-modal-view.js +609 -0
  155. package/dist/modules/product_backend/modal/product-modal-view.js.map +1 -0
  156. package/dist/modules/product_backend/routes.d.ts.map +1 -1
  157. package/dist/modules/product_backend/routes.js +44 -330
  158. package/dist/modules/product_backend/routes.js.map +1 -1
  159. package/dist/modules/product_backend/screens/detail.d.ts.map +1 -1
  160. package/dist/modules/product_backend/screens/detail.js +6 -108
  161. package/dist/modules/product_backend/screens/detail.js.map +1 -1
  162. package/dist/modules/product_backend/screens/index.d.ts +0 -1
  163. package/dist/modules/product_backend/screens/index.d.ts.map +1 -1
  164. package/dist/modules/product_backend/screens/index.js +0 -1
  165. package/dist/modules/product_backend/screens/index.js.map +1 -1
  166. package/dist/modules/product_backend/screens/list.d.ts +3 -1
  167. package/dist/modules/product_backend/screens/list.d.ts.map +1 -1
  168. package/dist/modules/product_backend/screens/list.js +6 -5
  169. package/dist/modules/product_backend/screens/list.js.map +1 -1
  170. package/dist/modules/user/authorization.d.ts.map +1 -1
  171. package/dist/modules/user/authorization.js +114 -0
  172. package/dist/modules/user/authorization.js.map +1 -1
  173. package/dist/modules/user/functions.d.ts.map +1 -1
  174. package/dist/modules/user/functions.js +2 -0
  175. package/dist/modules/user/functions.js.map +1 -1
  176. package/dist/modules/user/user-modal-context.d.ts +3 -0
  177. package/dist/modules/user/user-modal-context.d.ts.map +1 -0
  178. package/dist/modules/user/user-modal-context.js +151 -0
  179. package/dist/modules/user/user-modal-context.js.map +1 -0
  180. package/dist/modules/user_backend/client/user-modal.d.mts +72 -0
  181. package/dist/modules/user_backend/client/user-modal.d.mts.map +1 -0
  182. package/dist/modules/user_backend/client/user-modal.mjs +2 -0
  183. package/dist/modules/user_backend/client/user-modal.mjs.map +7 -0
  184. package/dist/modules/user_backend/index.d.ts.map +1 -1
  185. package/dist/modules/user_backend/index.js +9 -0
  186. package/dist/modules/user_backend/index.js.map +1 -1
  187. package/dist/modules/user_backend/islands.d.ts +15 -0
  188. package/dist/modules/user_backend/islands.d.ts.map +1 -0
  189. package/dist/modules/user_backend/islands.js +14 -0
  190. package/dist/modules/user_backend/islands.js.map +1 -0
  191. package/dist/modules/user_backend/messages.d.ts +30 -0
  192. package/dist/modules/user_backend/messages.d.ts.map +1 -1
  193. package/dist/modules/user_backend/messages.js +30 -0
  194. package/dist/modules/user_backend/messages.js.map +1 -1
  195. package/dist/modules/user_backend/modal/user-modal-view.d.ts +26 -0
  196. package/dist/modules/user_backend/modal/user-modal-view.d.ts.map +1 -0
  197. package/dist/modules/user_backend/modal/user-modal-view.js +320 -0
  198. package/dist/modules/user_backend/modal/user-modal-view.js.map +1 -0
  199. package/dist/modules/user_backend/routes.d.ts.map +1 -1
  200. package/dist/modules/user_backend/routes.js +15 -3
  201. package/dist/modules/user_backend/routes.js.map +1 -1
  202. package/dist/permission-catalogue.d.ts.map +1 -1
  203. package/dist/permission-catalogue.js +8 -0
  204. package/dist/permission-catalogue.js.map +1 -1
  205. package/dist/scaffold/index.js +1 -1
  206. package/dist/ui/client/record-modal-form.d.ts +67 -0
  207. package/dist/ui/client/record-modal-form.d.ts.map +1 -0
  208. package/dist/ui/client/record-modal-form.js +45 -0
  209. package/dist/ui/client/record-modal-form.js.map +1 -0
  210. package/dist/ui/client/record-modal.d.ts +103 -6
  211. package/dist/ui/client/record-modal.d.ts.map +1 -1
  212. package/dist/ui/client/record-modal.js +333 -65
  213. package/dist/ui/client/record-modal.js.map +1 -1
  214. package/dist/ui/index.d.ts +2 -1
  215. package/dist/ui/index.d.ts.map +1 -1
  216. package/dist/ui/index.js +2 -1
  217. package/dist/ui/index.js.map +1 -1
  218. package/dist/ui/nav.d.ts +7 -0
  219. package/dist/ui/nav.d.ts.map +1 -1
  220. package/dist/ui/nav.js +1 -1
  221. package/dist/ui/nav.js.map +1 -1
  222. package/dist/ui/record-modal.d.ts +15 -0
  223. package/dist/ui/record-modal.d.ts.map +1 -1
  224. package/dist/ui/record-modal.js +12 -0
  225. package/dist/ui/record-modal.js.map +1 -1
  226. package/dist/ui/table.d.ts.map +1 -1
  227. package/dist/ui/table.js +1 -1
  228. package/dist/ui/table.js.map +1 -1
  229. package/package.json +4 -4
  230. package/dist/modules/crm/functions.d.ts +0 -10
  231. package/dist/modules/crm/functions.d.ts.map +0 -1
  232. package/dist/modules/crm/functions.js +0 -1394
  233. package/dist/modules/crm/functions.js.map +0 -1
  234. package/dist/modules/product_backend/screens/create.d.ts +0 -16
  235. package/dist/modules/product_backend/screens/create.d.ts.map +0 -1
  236. package/dist/modules/product_backend/screens/create.js +0 -86
  237. package/dist/modules/product_backend/screens/create.js.map +0 -1
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../../design-system/src/primitives/actions/index.tsx", "../../../../../design-system/src/primitives/status/index.tsx", "../../../../../design-system/src/primitives/feedback/index.tsx", "../../../../../design-system/src/primitives/field/index.tsx", "../../../../../design-system/src/layouts/layout/index.tsx", "../../../../../design-system/src/layouts/page-identity/index.tsx", "../../../../../design-system/src/layouts/shell/index.tsx", "../../../../../design-system/src/primitives/field/native-control.tsx", "../../../../../design-system/src/primitives/navigation/index.tsx", "../../../../../design-system/src/primitives/progress/index.tsx", "../../../../../design-system/src/interactions/menu/index.tsx", "../../../../../design-system/src/interactions/popover/index.tsx", "../../../../../design-system/src/interactions/tooltip/index.tsx", "../../../../../design-system/src/patterns/modal-sheet/index.tsx", "../../../../../design-system/src/interactions/dialog/index.tsx", "../../../../../design-system/src/interactions/toast/index.tsx", "../../../../../design-system/src/interactions/spinner/index.tsx", "../../../../../design-system/src/interactions/skeleton/index.tsx", "../../../../../design-system/src/runtime/index.js", "../../../../../design-system/src/forms/shared.tsx", "../../../../../design-system/src/forms/scalar-fields/index.tsx", "../../../../../design-system/src/forms/combobox/index.tsx", "../../../../../design-system/src/forms/date-time/index.tsx", "../../../../../design-system/src/forms/upload/index.tsx", "../../../../../design-system/src/forms/relation-picker/index.tsx", "../../../../../design-system/src/data-operations/list-controls/index.tsx", "../../../../../design-system/src/data-operations/inline-edit/index.tsx", "../../../../../design-system/src/data-display/resource-list/index.tsx", "../../../../../design-system/src/patterns/list-chrome/index.tsx", "../../../../../design-system/src/data-display/data-grid/index.tsx", "../../../../../design-system/src/data-display/tree/index.tsx", "../../../../../design-system/src/data-display/matrix/index.tsx", "../../../../../design-system/src/data-display/bar-chart/index.tsx", "../../../../../design-system/src/data-operations/timeframe-filter/index.tsx", "../../../../../design-system/src/record/display/index.tsx", "../../../../../design-system/src/record/formatted-values/index.tsx", "../../../../../design-system/src/record/composition/index.tsx", "../../../../../design-system/src/record/activity/index.tsx", "../../../../../design-system/src/record/media/index.tsx", "../../../../../design-system/src/layouts/app-navigation/index.tsx", "../../../../../design-system/src/patterns/data-table/index.tsx", "../../../../../design-system/src/patterns/list-page/index.tsx", "../../../../../design-system/src/patterns/record-page/index.tsx", "../../../../../design-system/src/patterns/form-page/index.tsx", "../../../../../design-system/src/patterns/workspace-page/index.tsx", "../../../../../design-system/src/patterns/dashboard-page/index.tsx", "../../../../../design-system/src/patterns/board-page/index.tsx", "../../../../../design-system/src/patterns/pipeline/index.tsx", "../../../../../design-system/src/patterns/record-form/index.tsx", "../../../../../design-system/src/contract/index.ts", "../../../ui/client/record-modal.tsx", "../../../ui/record-modal.tsx", "../../../ui/client/record-modal-form.tsx", "../modal/user-modal-view.tsx"],
4
+ "sourcesContent": ["import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['action', 'action-leading', 'action-label', 'action-spinner', 'action-group'] as const\n\nexport type ActionVariant = 'primary' | 'secondary' | 'tertiary' | 'destructive'\nexport type ActionSize = 'compact' | 'default' | 'prominent'\n\ntype ActionBase = {\n label: string\n variant?: ActionVariant\n size?: ActionSize\n leading?: JSXChild\n disabled?: boolean\n loading?: boolean\n describedBy?: string | null\n}\n\nexport type ButtonProps = ActionBase & {\n type?: 'button' | 'submit' | 'reset'\n name?: string | null\n value?: string | null\n form?: string | null\n}\n\nexport type LinkButtonProps = ActionBase & { href: string }\nexport type IconButtonProps = Omit<ButtonProps, 'leading'> & {\n icon: JSXChild\n pressed?: boolean\n}\n\nconst ActionContent = (props: ActionBase & { iconOnly?: boolean }): TemplateResult => (\n <>\n {props.loading === true && <span data-ui=\"action-spinner\" aria-hidden=\"true\" />}\n {props.loading !== true && props.leading !== undefined && (\n <span data-ui=\"action-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n {!props.iconOnly && <span data-ui=\"action-label\">{props.label}</span>}\n </>\n)\n\nexport const Button = (props: ButtonProps): TemplateResult => (\n <button\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n type={props.type ?? 'button'}\n name={props.name ?? null}\n value={props.value ?? null}\n form={props.form ?? null}\n disabled={props.disabled === true || props.loading === true}\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </button>\n)\n\n/** An icon-only action with a visible tooltip and accessible name. */\nexport const IconButton = (props: IconButtonProps): TemplateResult => (\n <button\n data-ui=\"action\"\n data-icon-only=\"true\"\n data-variant={props.variant ?? 'tertiary'}\n data-size={props.size ?? 'default'}\n type={props.type ?? 'button'}\n name={props.name ?? null}\n value={props.value ?? null}\n form={props.form ?? null}\n disabled={props.disabled === true || props.loading === true}\n aria-label={props.label}\n aria-pressed={props.pressed === undefined ? null : String(props.pressed)}\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n title={props.label}\n >\n <ActionContent {...props} leading={props.icon} iconOnly />\n </button>\n)\n\nexport const LinkButton = (props: LinkButtonProps): TemplateResult =>\n props.disabled || props.loading ? (\n <button\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n type=\"button\"\n disabled\n aria-busy={props.loading === true ? 'true' : null}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </button>\n ) : (\n <a\n data-ui=\"action\"\n data-variant={props.variant ?? 'secondary'}\n data-size={props.size ?? 'default'}\n href={props.href}\n aria-describedby={props.describedBy ?? null}\n >\n <ActionContent {...props} />\n </a>\n )\n\nexport const ActionGroup = (props: {\n actions: readonly JSXChild[]\n label?: string | null\n}): TemplateResult => (\n <div data-ui=\"action-group\" role=\"group\" aria-label={props.label ?? null}>\n {each(\n props.actions,\n (_, index) => index,\n (action) => (\n <>{action}</>\n ),\n )}\n </div>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['badge', 'tag', 'tag-remove', 'count-badge', 'avatar', 'code'] as const\n\nexport type Tone = 'neutral' | 'info' | 'positive' | 'warning' | 'danger'\n\nexport const Badge = (props: { label: string; tone?: Tone; value?: string }): TemplateResult => (\n <span\n data-ui=\"badge\"\n data-pattern=\"badge\"\n data-tone={props.tone ?? 'neutral'}\n data-value={props.value ?? ''}\n >\n {props.label}\n </span>\n)\n\nexport const Tag = (props: {\n label: string\n removeHref?: string | null\n removeLabel?: string | null\n}): TemplateResult => (\n <span data-ui=\"tag\">\n {props.label}\n {!!props.removeHref && (\n <a\n data-ui=\"tag-remove\"\n href={props.removeHref}\n aria-label={props.removeLabel ?? `Remove ${props.label}`}\n >\n \u00D7\n </a>\n )}\n </span>\n)\n\nexport const CountBadge = (props: { count: number; label: string }): TemplateResult => (\n <span data-ui=\"count-badge\" role=\"status\" aria-label={props.label}>\n {String(props.count)}\n </span>\n)\n\nexport const initials = (name: string): string => {\n const parts = name.trim().split(/\\s+/).filter(Boolean)\n if (!parts.length) return '?'\n const first = parts.length > 1 ? parts[parts.length - 1]! : parts[0]!\n const second = parts.length > 2 ? parts[parts.length - 2]! : ''\n return (second.slice(0, 1) + first.slice(0, 1)).toLocaleUpperCase('vi')\n}\n\nexport const Avatar = (props: { name: string; size?: 'small' | 'default' | 'large' }): TemplateResult => (\n <span\n data-ui=\"avatar\"\n data-pattern=\"avatar\"\n data-size={props.size ?? 'default'}\n title={props.name}\n aria-hidden=\"true\"\n >\n {initials(props.name)}\n </span>\n)\n\nexport const Code = (props: { value: string; context?: string | null }): TemplateResult => (\n <code data-ui=\"code\" data-context={props.context ?? null}>\n {props.value}\n </code>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'notice',\n 'notice-mark',\n 'notice-copy',\n 'notice-title',\n 'notice-message',\n 'notice-actions',\n 'empty',\n 'empty-mark',\n 'empty-title',\n 'empty-message',\n 'empty-actions',\n 'loading',\n 'loading-label',\n] as const\n\nexport type NoticeTone = 'info' | 'positive' | 'warning' | 'danger'\n\n// Lucide (ISC) glyphs, vendored per tone: info, circle-check, triangle-alert, circle-alert.\nconst NOTICE_ICONS: Record<NoticeTone, () => TemplateResult> = {\n info: () => (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 16v-4\" />\n <path d=\"M12 8h.01\" />\n </>\n ),\n positive: () => (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"m9 12 2 2 4-4\" />\n </>\n ),\n warning: () => (\n <>\n <path d=\"m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\" />\n <path d=\"M12 9v4\" />\n <path d=\"M12 17h.01\" />\n </>\n ),\n danger: () => (\n <>\n <circle cx=\"12\" cy=\"12\" r=\"10\" />\n <path d=\"M12 8v4\" />\n <path d=\"M12 16h.01\" />\n </>\n ),\n}\n\nexport const Notice = (props: {\n title: string\n message: string\n tone?: NoticeTone\n actions?: JSXChild\n}): TemplateResult => (\n <aside\n data-ui=\"notice\"\n data-pattern=\"notice\"\n data-tone={props.tone ?? 'info'}\n role={props.tone === 'danger' ? 'alert' : 'status'}\n >\n <span data-ui=\"notice-mark\" aria-hidden=\"true\">\n <svg\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n {NOTICE_ICONS[props.tone ?? 'info']()}\n </svg>\n </span>\n <div data-ui=\"notice-copy\">\n <p data-ui=\"notice-title\">{props.title}</p>\n <p data-ui=\"notice-message\">{props.message}</p>\n </div>\n {props.actions !== undefined && <div data-ui=\"notice-actions\">{props.actions}</div>}\n </aside>\n)\n\nexport const EmptyState = (props: { title: string; message: string; actions?: JSXChild }): TemplateResult => (\n <div data-ui=\"empty\" role=\"status\">\n <span data-ui=\"empty-mark\" aria-hidden=\"true\">\n \u25C7\n </span>\n <p data-ui=\"empty-title\">{props.title}</p>\n <p data-ui=\"empty-message\">{props.message}</p>\n {props.actions !== undefined && <div data-ui=\"empty-actions\">{props.actions}</div>}\n </div>\n)\n\nexport const LoadingState = (props: { label: string; lines?: number }): TemplateResult => (\n <div data-ui=\"loading\" role=\"status\" aria-live=\"polite\">\n <span data-ui=\"loading-label\">{props.label}</span>\n <div data-ui=\"skeleton\" aria-hidden=\"true\">\n {each(\n Array.from({ length: Math.max(1, props.lines ?? 3) }),\n (_, index) => index,\n (_, index) => (\n <span data-ui=\"skeleton-line\" data-line={String(index + 1)} />\n ),\n )}\n </div>\n </div>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Disclosure } from '../../layouts/index.tsx'\nimport { NativeFieldControl } from './native-control.tsx'\n\nexport const HOOKS = [\n 'field',\n 'field-label',\n 'field-required',\n 'field-control',\n 'field-options',\n 'field-option',\n 'field-option-input',\n 'field-help',\n 'field-error',\n] as const\n\nexport type FieldOption = {\n value: string\n label: string\n name?: string\n checked?: boolean\n disabled?: boolean\n}\nexport type FieldProps = {\n /** Nested fields for a collapsible group inside a larger record form. */\n fields?: readonly FieldProps[]\n open?: boolean\n\n id: string\n name: string\n label: string\n /** A trusted, progressively enhanced control such as a relation selector. */\n control?: JSXChild\n type?:\n | 'text'\n | 'email'\n | 'tel'\n | 'number'\n | 'decimal'\n | 'password'\n | 'time'\n | 'color'\n | 'date'\n | 'datetime-local'\n | 'month'\n | 'week'\n | 'select'\n | 'textarea'\n | 'checkbox'\n | 'checkbox-group'\n | 'radio'\n value?: string | number | boolean | null\n placeholder?: string | null\n required?: boolean\n disabled?: boolean\n readOnly?: boolean\n min?: string | number\n max?: string | number\n help?: string | null\n error?: string | null\n options?: readonly FieldOption[]\n /**\n * How checkbox-group and radio options flow. `horizontal` (default) wraps them on a line; `vertical` lists\n * one option per line under the shared label, for choices whose labels should scan as a column.\n */\n optionsOrientation?: 'horizontal' | 'vertical'\n span?: 'half' | 'full'\n step?: string | null\n autocomplete?: string | null\n}\n\nconst hasError = (props: FieldProps): boolean => !!props.error || (props.fields?.some(hasError) ?? false)\n\nexport const Field = (props: FieldProps): TemplateResult => {\n const helpId = props.help ? `${props.id}-help` : null\n const errorId = props.error ? `${props.id}-error` : null\n const describedBy = [helpId, errorId].filter(Boolean).join(' ') || null\n const label = (\n <span data-ui=\"field-label\">\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n )\n\n if (props.fields)\n return (\n <div data-ui=\"field\" data-kind=\"group\" data-span=\"full\" data-invalid={String(!!props.error)}>\n <Disclosure\n summary={props.label}\n open={props.open || hasError(props)}\n body={\n <div data-ui=\"form-grid\">\n {each(\n props.fields.map((field) => ({\n ...field,\n disabled: props.disabled || field.disabled,\n readOnly: props.readOnly || field.readOnly,\n })),\n (field) => field.id,\n (field) => (\n <Field {...field} />\n ),\n )}\n </div>\n }\n />\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n if (props.type === 'checkbox-group')\n return (\n <div\n data-ui=\"field\"\n data-kind=\"checkbox-group\"\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <span data-ui=\"field-label\" id={`${props.id}-label`}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n <div\n data-ui=\"field-options\"\n data-orientation={props.optionsOrientation === 'vertical' ? 'vertical' : null}\n role=\"group\"\n aria-labelledby={`${props.id}-label`}\n >\n {each(\n props.options ?? [],\n (option) => option.name ?? option.value,\n (option) => (\n <label data-ui=\"field-option\">\n <input\n data-ui=\"field-option-input\"\n type=\"checkbox\"\n name={option.name ?? `${props.name}[]`}\n value={option.value}\n checked={option.checked === true}\n disabled={props.disabled === true || option.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete=\"off\"\n />\n <span>{option.label}</span>\n </label>\n ),\n )}\n </div>\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n if (props.type === 'radio')\n return (\n <div\n data-ui=\"field\"\n data-kind=\"radio\"\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <span data-ui=\"field-label\" id={`${props.id}-label`}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </span>\n <div\n data-ui=\"field-options\"\n data-orientation={props.optionsOrientation === 'vertical' ? 'vertical' : null}\n role=\"radiogroup\"\n aria-labelledby={`${props.id}-label`}\n >\n {each(\n props.options ?? [],\n (option) => option.value,\n (option) => (\n <label data-ui=\"field-option\">\n <input\n data-ui=\"field-option-input\"\n type=\"radio\"\n name={props.name}\n value={option.value}\n checked={String(props.value ?? '') === option.value}\n required={props.required === true}\n disabled={props.disabled === true || option.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete=\"off\"\n />\n <span>{option.label}</span>\n </label>\n ),\n )}\n </div>\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n\n return (\n <label\n data-ui=\"field\"\n data-kind={props.type ?? 'text'}\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n for={props.id}\n >\n {props.type === 'checkbox' && NativeFieldControl(props, describedBy)}\n {label}\n {props.type !== 'checkbox' && NativeFieldControl(props, describedBy)}\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </label>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status.tsx'\n\nexport const HOOKS = [\n 'stack',\n 'inline',\n 'grid',\n 'card-grid',\n 'surface',\n 'surface-head',\n 'surface-heading',\n 'surface-title',\n 'surface-description',\n 'surface-actions',\n 'disclosure',\n 'disclosure-summary',\n 'disclosure-body',\n 'section',\n 'section-head',\n 'section-eyebrow',\n 'section-title',\n 'section-description',\n 'section-actions',\n 'section-body',\n 'content-card',\n 'card-media',\n 'card-head',\n 'card-leading',\n 'card-heading',\n 'card-eyebrow',\n 'card-title',\n 'card-summary',\n 'card-status',\n 'card-body',\n 'card-meta',\n 'card-actions',\n 'kanban',\n 'kanban-card',\n 'kanban-media',\n 'kanban-title',\n 'kanban-meta',\n 'kanban-note',\n 'kanban-actions',\n 'metric',\n 'metric-icon',\n 'metric-label',\n 'metric-value',\n 'metric-detail',\n] as const\n\nexport { AppShell, Page, PageHeader, RecordCanvas, RecordSection } from '../shell.tsx'\n\nconst Items = (props: { items: readonly JSXChild[] }): TemplateResult => (\n <>\n {each(\n props.items,\n (_, index) => index,\n // biome-ignore lint/complexity/noUselessFragments: `each` requires a TemplateResult callback.\n (item) => (typeof item === 'string' || typeof item === 'number' ? <span>{item}</span> : <>{item}</>),\n )}\n </>\n)\n\nexport const Stack = (props: {\n items: readonly JSXChild[]\n gap?: 'compact' | 'default' | 'loose'\n}): TemplateResult => (\n <div data-ui=\"stack\" data-pattern=\"stack\" data-gap={props.gap ?? 'default'}>\n <Items items={props.items} />\n </div>\n)\n\nexport const Inline = (props: { items: readonly JSXChild[] }): TemplateResult => (\n <div data-ui=\"inline\">\n <Items items={props.items} />\n </div>\n)\n\n/**\n * Fixed columns. `align=\"stretch\"` gives every block in a row the row's height,\n * so side-by-side dashboard cards end on one line instead of leaving a hole\n * under the shorter one.\n */\nexport const Grid = (props: {\n items: readonly JSXChild[]\n columns?: 2 | 3 | 4\n align?: 'start' | 'stretch'\n}): TemplateResult => (\n <div\n data-ui=\"grid\"\n data-columns={String(props.columns ?? 3)}\n data-align={props.align === 'stretch' ? 'stretch' : null}\n >\n <Items items={props.items} />\n </div>\n)\n\nexport type CardGridProps<T> = {\n items: readonly T[]\n id: (item: T) => unknown\n card: (item: T) => TemplateResult\n minimum?: 'compact' | 'default' | 'wide'\n}\n\n/** An adaptive card collection; unlike `Grid`, column count follows available width. */\nexport const CardGrid = <T,>(props: CardGridProps<T>): TemplateResult => (\n <div data-ui=\"card-grid\" data-minimum={props.minimum === 'default' ? null : props.minimum}>\n {each(props.items, props.id, (item) => props.card(item))}\n </div>\n)\n\nexport const Surface = (props: {\n body: JSXChild\n title?: string\n /** One line under the title. A dashboard block keeps its heading, context and content in one card. */\n description?: string | null\n actions?: JSXChild\n tone?: 'default' | 'subtle' | 'raised'\n padding?: 'none' | 'compact' | 'default'\n}): TemplateResult => (\n <div\n data-ui=\"surface\"\n data-tone={props.tone ?? 'default'}\n data-padding={props.padding ?? 'default'}\n data-has-heading={props.title ? 'true' : null}\n >\n {props.title && (\n <header data-ui=\"surface-head\">\n {props.description ? (\n <div data-ui=\"surface-heading\">\n <h2 data-ui=\"surface-title\">{props.title}</h2>\n <p data-ui=\"surface-description\">{props.description}</p>\n </div>\n ) : (\n <h2 data-ui=\"surface-title\">{props.title}</h2>\n )}\n {props.actions !== undefined && <div data-ui=\"surface-actions\">{props.actions}</div>}\n </header>\n )}\n {props.body}\n </div>\n)\n\n/** Native, keyboard-accessible progressive disclosure for dense secondary detail. */\nexport const Disclosure = (props: { summary: string; body: JSXChild; open?: boolean }): TemplateResult => (\n <details data-ui=\"disclosure\" open={props.open === true ? true : undefined}>\n <summary data-ui=\"disclosure-summary\">{props.summary}</summary>\n <div data-ui=\"disclosure-body\">{props.body}</div>\n </details>\n)\n\nexport const Section = (props: {\n title: string\n body: JSXChild\n description?: string | null\n eyebrow?: string | null\n actions?: JSXChild\n}): TemplateResult => (\n <section data-ui=\"section\">\n <header data-ui=\"section-head\">\n <div>\n {!!props.eyebrow && <p data-ui=\"section-eyebrow\">{props.eyebrow}</p>}\n <h2 data-ui=\"section-title\">{props.title}</h2>\n {!!props.description && <p data-ui=\"section-description\">{props.description}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"section-actions\">{props.actions}</div>}\n </header>\n <div data-ui=\"section-body\">{props.body}</div>\n </section>\n)\n\nexport type ContentCardProps = {\n title: string\n eyebrow?: string | null\n summary?: string | null\n leading?: JSXChild\n media?: JSXChild\n status?: JSXChild\n body?: JSXChild\n meta?: JSXChild\n actions?: JSXChild\n href?: string | null\n selected?: boolean\n tone?: 'default' | 'subtle' | 'raised'\n padding?: 'compact' | 'default'\n}\n\nexport const ContentCard = (props: ContentCardProps): TemplateResult => (\n <article\n data-ui=\"content-card\"\n data-interactive={String(!!props.href)}\n data-selected={String(props.selected === true)}\n data-tone={props.tone ?? 'default'}\n data-padding={props.padding ?? 'default'}\n >\n {props.media !== undefined && <div data-ui=\"card-media\">{props.media}</div>}\n <header data-ui=\"card-head\">\n {props.leading !== undefined && (\n <span data-ui=\"card-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n <div data-ui=\"card-heading\">\n {!!props.eyebrow && <p data-ui=\"card-eyebrow\">{props.eyebrow}</p>}\n <h3 data-ui=\"card-title\">{props.href ? <a href={props.href}>{props.title}</a> : props.title}</h3>\n {!!props.summary && <p data-ui=\"card-summary\">{props.summary}</p>}\n </div>\n {props.status !== undefined && <div data-ui=\"card-status\">{props.status}</div>}\n </header>\n {props.body !== undefined && <div data-ui=\"card-body\">{props.body}</div>}\n {props.meta !== undefined && <div data-ui=\"card-meta\">{props.meta}</div>}\n {props.actions !== undefined && <div data-ui=\"card-actions\">{props.actions}</div>}\n </article>\n)\n\nexport type KanbanCardProps = {\n id: string\n title: string\n href?: string | null\n media?: JSXChild\n meta?: JSXChild\n note?: string | null\n actions?: JSXChild\n selected?: boolean\n}\n\nexport const KanbanCard = (props: KanbanCardProps): TemplateResult => (\n <article\n data-ui=\"kanban-card\"\n data-key={props.id}\n data-interactive={String(!!props.href)}\n data-selected={String(props.selected === true)}\n >\n {props.media !== undefined && <div data-ui=\"kanban-media\">{props.media}</div>}\n <h3 data-ui=\"kanban-title\">{props.href ? <a href={props.href}>{props.title}</a> : props.title}</h3>\n {props.meta !== undefined && <div data-ui=\"kanban-meta\">{props.meta}</div>}\n {!!props.note && <p data-ui=\"kanban-note\">{props.note}</p>}\n {props.actions !== undefined && <div data-ui=\"kanban-actions\">{props.actions}</div>}\n </article>\n)\n\nexport type KanbanGridProps<T> = {\n rows: readonly T[]\n id: (row: T) => unknown\n card: (row: T) => TemplateResult\n}\n\nexport const KanbanGrid = <T,>(props: KanbanGridProps<T>): TemplateResult => (\n <div data-ui=\"kanban\">{each(props.rows, props.id, (row) => props.card(row))}</div>\n)\n\n/**\n * One number, with the context that makes it mean something.\n *\n * `tone` colours a dot beside the label and nothing else. The number itself stays\n * in the page's ink: a KPI row is read by comparing figures, and four differently\n * coloured figures are compared by colour instead. The dot is drawn only when a\n * tone is passed, so a metric that has no state to report does not grow a grey\n * bullet that means nothing.\n *\n * `href` makes the whole card the link rather than the label, because the card is\n * what the pointer is already over. There is nothing else inside a metric to\n * click, which is exactly why `ContentCard` does the opposite.\n */\nexport const Metric = (props: {\n label: string\n value: string | number\n detail?: string | null\n tone?: Tone\n /**\n * A glyph naming the quantity, in a tile beside it. Decorative: the label is\n * the name of the number, so this is hidden from assistive technology.\n */\n icon?: JSXChild\n href?: string | null\n}): TemplateResult => {\n const attributes = {\n 'data-ui': 'metric',\n 'data-tone': props.tone ?? 'neutral',\n // One marker to a card, never two. The dot reports state for a metric that\n // has no glyph; where there is a glyph, the glyph is already the thing the\n // eye lands on, and a coloured bullet beside the label competes with it.\n 'data-dot': String(props.tone !== undefined && props.icon === undefined),\n }\n const body = (\n <>\n {props.icon !== undefined && (\n <span data-ui=\"metric-icon\" aria-hidden=\"true\">\n {props.icon}\n </span>\n )}\n <p data-ui=\"metric-label\">{props.label}</p>\n <p data-ui=\"metric-value\">{String(props.value)}</p>\n {!!props.detail && <p data-ui=\"metric-detail\">{props.detail}</p>}\n </>\n )\n return props.href ? (\n <a {...attributes} data-interactive=\"true\" href={props.href}>\n {body}\n </a>\n ) : (\n <article {...attributes} data-interactive=\"false\">\n {body}\n </article>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport type PageIdentityKind =\n | 'page'\n | 'list-page'\n | 'record-page'\n | 'form-page'\n | 'workspace-page'\n | 'dashboard-page'\n | 'board-page'\n\nexport type PageIdentityProps = {\n context?: JSXChild\n eyebrow?: string | null\n title: string\n description?: string | null\n status?: JSXChild\n actions?: JSXChild\n meta?: JSXChild\n}\n\ntype PageIdentityContentProps = PageIdentityProps & {\n groupDescription?: boolean\n}\n\n/** The single identity band shared by every full-page component and recipe. */\nexport const pageIdentityContent = (\n kind: PageIdentityKind,\n props: PageIdentityContentProps,\n): TemplateResult => (\n <>\n <div data-ui={`${kind}-heading`} data-kv-page-identity=\"heading\">\n {!!props.eyebrow && (\n <p data-ui={`${kind}-eyebrow`} data-kv-page-identity=\"eyebrow\">\n {props.eyebrow}\n </p>\n )}\n <div data-ui={`${kind}-title-row`} data-kv-page-identity=\"title-row\">\n <h1 data-ui={`${kind}-title`} data-kv-page-identity=\"title\">\n {props.title}\n </h1>\n {props.actions !== undefined && (\n <div data-ui={`${kind}-actions`} data-kv-page-identity=\"actions\">\n {props.actions}\n </div>\n )}\n </div>\n {(props.groupDescription || props.status !== undefined) &&\n (!!props.description || props.status !== undefined) ? (\n <div data-ui={`${kind}-subline`} data-kv-page-identity=\"subline\">\n {!!props.description && (\n <p data-ui={`${kind}-description`} data-kv-page-identity=\"description\">\n {props.description}\n </p>\n )}\n {props.status !== undefined && (\n <span data-ui={`${kind}-status`} data-kv-page-identity=\"status\">\n {props.status}\n </span>\n )}\n </div>\n ) : (\n !!props.description && (\n <p data-ui={`${kind}-description`} data-kv-page-identity=\"description\">\n {props.description}\n </p>\n )\n )}\n </div>\n {props.meta !== undefined && (\n <div data-ui={`${kind}-meta`} data-kv-page-identity=\"meta\">\n {props.meta}\n </div>\n )}\n </>\n)\n\nexport const pageIdentity = (kind: PageIdentityKind, props: PageIdentityProps): TemplateResult => (\n <>\n {props.context !== undefined && (\n <div data-ui={`${kind}-context`} data-kv-page-identity=\"context\">\n {props.context}\n </div>\n )}\n <header data-ui={`${kind}-header`} data-kv-page-identity=\"header\">\n {pageIdentityContent(kind, props)}\n </header>\n </>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity, pageIdentityContent, type PageIdentityProps } from '../page-identity/index.tsx'\n\nexport const HOOKS = [\n 'app-shell',\n 'app-sidebar',\n 'app-main',\n 'app-right-rail',\n 'page',\n 'page-context',\n 'page-header',\n 'page-heading',\n 'page-eyebrow',\n 'page-title-row',\n 'page-title',\n 'page-description',\n 'page-subline',\n 'page-status',\n 'page-actions',\n 'page-meta',\n 'page-body',\n 'record-canvas',\n 'record-content',\n 'record-section',\n 'record-section-title',\n] as const\n\nexport const AppShell = (props: {\n sidebar: JSXChild\n main: JSXChild\n rightRail?: JSXChild\n mode?: 'viewport' | 'embedded'\n}): TemplateResult => (\n <div\n data-ui=\"app-shell\"\n data-has-right-rail={String(props.rightRail !== undefined)}\n data-mode={props.mode ?? 'viewport'}\n >\n <aside data-ui=\"app-sidebar\">{props.sidebar}</aside>\n {props.mode === 'embedded' ? (\n <div data-ui=\"app-main\">{props.main}</div>\n ) : (\n <main data-ui=\"app-main\">{props.main}</main>\n )}\n {props.rightRail !== undefined && <aside data-ui=\"app-right-rail\">{props.rightRail}</aside>}\n </div>\n)\n\nexport type PageHeaderProps = Omit<PageIdentityProps, 'context'>\n\nexport const PageHeader = (props: PageHeaderProps): TemplateResult => (\n <header data-ui=\"page-header\" data-kv-page-identity=\"header\">\n {pageIdentityContent('page', props)}\n </header>\n)\n\nexport type PageProps = PageIdentityProps & {\n body: JSXChild\n}\n\nexport const Page = (props: PageProps): TemplateResult => {\n const { body, ...identity } = props\n return (\n <section data-ui=\"page\">\n {pageIdentity('page', identity)}\n <div data-ui=\"page-body\">{body}</div>\n </section>\n )\n}\n\nexport const RecordCanvas = (props: { body: JSXChild }): TemplateResult => (\n <div data-ui=\"record-canvas\">\n <div data-ui=\"record-content\">{props.body}</div>\n </div>\n)\n\nexport const RecordSection = (props: { body: JSXChild; title?: string | null }): TemplateResult => (\n <section data-ui=\"record-section\">\n {!!props.title && <h2 data-ui=\"record-section-title\">{props.title}</h2>}\n {props.body}\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { FieldProps } from './index.tsx'\n\nexport const NativeFieldControl = (props: FieldProps, describedBy: string | null): TemplateResult => {\n if (props.control !== undefined) return <>{props.control}</>\n if (props.type === 'textarea')\n return (\n <textarea\n data-ui=\"field-control\"\n id={props.id}\n name={props.name}\n placeholder={props.placeholder ?? null}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n >\n {String(props.value ?? '')}\n </textarea>\n )\n if (props.type === 'select')\n return (\n <select\n data-ui=\"field-control\"\n id={props.id}\n name={props.name}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n >\n {each(\n props.options ?? [],\n (option) => option.value,\n (option) => (\n <option\n value={option.value}\n selected={String(props.value ?? '') === option.value}\n disabled={option.disabled === true}\n >\n {option.label}\n </option>\n ),\n )}\n </select>\n )\n return (\n <input\n data-ui=\"field-control\"\n id={props.id}\n type={props.type === 'decimal' ? 'number' : (props.type ?? 'text')}\n name={props.name}\n value={props.type === 'checkbox' ? '1' : String(props.value ?? '')}\n checked={props.type === 'checkbox' && (props.value === true || props.value === '1')}\n placeholder={props.placeholder ?? null}\n required={props.required === true}\n disabled={props.disabled === true}\n readonly={props.readOnly === true}\n min={props.min}\n max={props.max}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={describedBy}\n autocomplete={props.autocomplete ?? 'off'}\n step={props.type === 'decimal' ? (props.step ?? 'any') : (props.step ?? null)}\n />\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'breadcrumbs',\n 'breadcrumb',\n 'breadcrumb-overflow',\n 'breadcrumb-overflow-trigger',\n 'breadcrumb-overflow-list',\n 'nav-list',\n 'nav-item',\n 'nav-item-leading',\n 'nav-item-label',\n 'nav-item-count',\n 'tabs',\n 'tab',\n 'tab-count',\n 'tabbed-view',\n 'tabbed-view-context',\n 'tab-panel',\n] as const\n\nexport type BreadcrumbItem = {\n id?: string\n label: string\n href?: string\n}\n\ntype BreadcrumbOverflow = {\n id: '__breadcrumb-overflow'\n kind: 'overflow'\n items: readonly BreadcrumbItem[]\n}\n\nexport type BreadcrumbsProps = {\n label: string\n items: readonly BreadcrumbItem[]\n /** Keep the first and current locations visible; put intermediate ancestors in a native disclosure. */\n maxItems?: number\n /** Accessible label for the collapsed ancestor trigger, supplied in the application's locale. */\n overflowLabel?: string\n}\n\nexport const Breadcrumbs = (props: BreadcrumbsProps): TemplateResult => {\n const limit = Math.max(3, Math.floor(props.maxItems ?? props.items.length))\n const overflow = props.items.length > limit\n const omitted = overflow ? props.items.slice(1, props.items.length - (limit - 2)) : []\n const visible: readonly (BreadcrumbItem | BreadcrumbOverflow)[] = overflow\n ? [\n props.items[0]!,\n { id: '__breadcrumb-overflow', kind: 'overflow', items: omitted },\n ...props.items.slice(props.items.length - (limit - 2)),\n ]\n : props.items\n\n return (\n <nav data-ui=\"breadcrumbs\" data-pattern=\"breadcrumbs\" aria-label={props.label}>\n <ol>\n {each(\n visible,\n (item, index) => item.id ?? `${index}:${'label' in item ? item.label : 'overflow'}`,\n (item, index) =>\n 'kind' in item ? (\n <li data-ui=\"breadcrumb-overflow\">\n <details>\n <summary\n data-ui=\"breadcrumb-overflow-trigger\"\n aria-label={props.overflowLabel ?? props.label}\n >\n \u2026\n </summary>\n <ol data-ui=\"breadcrumb-overflow-list\">\n {each(\n item.items,\n (ancestor, ancestorIndex) => ancestor.id ?? `${ancestorIndex}:${ancestor.label}`,\n (ancestor) => (\n <li>\n {ancestor.href ? (\n <a href={ancestor.href}>{ancestor.label}</a>\n ) : (\n <span>{ancestor.label}</span>\n )}\n </li>\n ),\n )}\n </ol>\n </details>\n </li>\n ) : (\n <li data-ui=\"breadcrumb\">\n {item.href !== undefined && index < visible.length - 1 ? (\n <a href={item.href}>{item.label}</a>\n ) : (\n <span aria-current={index === visible.length - 1 ? 'page' : null}>{item.label}</span>\n )}\n </li>\n ),\n )}\n </ol>\n </nav>\n )\n}\n\nexport type NavItemProps = {\n label: string\n href: string\n leading?: JSXChild\n count?: number | string\n active?: boolean\n}\n\nexport const NavItem = (props: NavItemProps): TemplateResult => (\n <a\n data-ui=\"nav-item\"\n data-active={props.active === true ? 'true' : null}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n >\n {props.leading !== undefined && (\n <span data-ui=\"nav-item-leading\" aria-hidden=\"true\">\n {props.leading}\n </span>\n )}\n <span data-ui=\"nav-item-label\">{props.label}</span>\n {props.count !== undefined && <span data-ui=\"nav-item-count\">{String(props.count)}</span>}\n </a>\n)\n\nexport const NavList = (props: { label: string; items: readonly NavItemProps[] }): TemplateResult => (\n <nav data-ui=\"nav-list\" aria-label={props.label}>\n {each(\n props.items,\n (item) => `${item.href}:${item.label}`,\n (item) => (\n <NavItem {...item} />\n ),\n )}\n </nav>\n)\n\nexport type TabItem = {\n id: string\n label: string\n href: string\n /** A number, or a bounded label such as \"10.000+\" when the exact total is not counted. */\n count?: number | string\n active?: boolean\n}\n\nexport type TabProps = TabItem & {\n /** DOM id used to associate this navigation item with the active panel. */\n elementId?: string\n /** Id of the panel this navigation item opens. */\n controls?: string\n}\n\nexport const Tab = (props: TabProps): TemplateResult => (\n <a\n data-ui=\"tab\"\n data-active={props.active === true ? 'true' : null}\n id={props.elementId}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n aria-controls={props.controls}\n >\n {props.label}\n {props.count !== undefined && <span data-ui=\"tab-count\">{String(props.count)}</span>}\n </a>\n)\n\nexport type TabsProps = {\n /** Stable id used to associate the active tab with a TabPanel. */\n id?: string\n label: string\n items: readonly TabItem[]\n extension?: JSXChild\n /** Panel controlled by these route-navigation tabs. */\n panelId?: string\n}\n\nconst tabElementId = (tabsId: string, itemId: string): string =>\n `${tabsId}-${itemId.replace(/[^a-zA-Z0-9_-]+/gu, '-')}-tab`\n\nexport const Tabs = (props: TabsProps): TemplateResult => (\n <nav data-ui=\"tabs\" data-pattern=\"tabs\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <Tab\n {...item}\n elementId={props.id ? tabElementId(props.id, item.id) : undefined}\n controls={props.panelId}\n />\n ),\n )}\n {props.extension}\n </nav>\n)\n\nexport type TabPanelProps = {\n id?: string\n labelledBy?: string\n body: JSXChild\n}\n\n/**\n * Content associated with route-navigation tabs. It owns scrolling and vertical\n * rhythm, but deliberately adds no left or right padding.\n */\nexport const TabPanel = (props: TabPanelProps): TemplateResult => (\n <section data-ui=\"tab-panel\" id={props.id} aria-labelledby={props.labelledBy} tabindex=\"-1\">\n {props.body}\n </section>\n)\n\nexport type TabbedViewProps = {\n id: string\n label: string\n items: readonly TabItem[]\n body: JSXChild\n /** Stable content above the tab bar, such as a record summary or issues. */\n context?: JSXChild\n extension?: JSXChild\n}\n\n/** A complete tabbed region with one stable navigation bar and one scrolling panel. */\nexport const TabbedView = (props: TabbedViewProps): TemplateResult => {\n const tabsId = `${props.id}-tabs`\n const panelId = `${props.id}-panel`\n const active = props.items.find((item) => item.active === true)\n return (\n <div data-ui=\"tabbed-view\">\n {props.context !== undefined && props.context !== '' ? (\n <div data-ui=\"tabbed-view-context\">{props.context}</div>\n ) : (\n ''\n )}\n <Tabs\n id={tabsId}\n label={props.label}\n items={props.items}\n extension={props.extension}\n panelId={panelId}\n />\n <TabPanel\n id={panelId}\n labelledBy={active ? tabElementId(tabsId, active.id) : undefined}\n body={props.body}\n />\n </div>\n )\n}\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'progress',\n 'progress-label',\n 'progress-value',\n 'progress-track',\n 'progress-bar',\n] as const\n\nexport type ProgressTone = 'primary' | 'positive' | 'warning' | 'danger'\n\nexport const Progress = (props: {\n value: number\n label: string\n tone?: ProgressTone\n showValue?: boolean\n}): TemplateResult => {\n const value = Number.isFinite(props.value) ? Math.max(0, Math.min(100, props.value)) : 0\n return (\n <div data-ui=\"progress\" data-tone={props.tone ?? 'primary'}>\n {(props.showValue ?? true) && (\n <div data-ui=\"progress-label\">\n <span>{props.label}</span>\n <span data-ui=\"progress-value\">{`${value}%`}</span>\n </div>\n )}\n <div\n data-ui=\"progress-track\"\n role=\"progressbar\"\n aria-label={props.label}\n aria-valuemin=\"0\"\n aria-valuemax=\"100\"\n aria-valuenow={String(value)}\n >\n <span data-ui=\"progress-bar\" style={`width: ${value}%`} />\n </div>\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'menu',\n 'menu-trigger',\n 'menu-trigger-count',\n 'menu-panel',\n 'menu-search',\n 'menu-search-input',\n 'menu-label',\n 'menu-separator',\n 'menu-item',\n 'menu-item-leading',\n 'menu-item-copy',\n 'menu-item-check',\n 'menu-item-shortcut',\n] as const\n\nexport type MenuItem = {\n id: string\n kind?: 'item'\n label: string\n href?: string\n name?: string\n value?: string\n form?: string\n leading?: JSXChild\n description?: string\n disabled?: boolean\n destructive?: boolean\n /** Turns the command into a menuitemcheckbox while preserving server-owned state. */\n checked?: boolean\n /** Visible keyboard hint; the browser interaction remains owned by the application. */\n shortcut?: string\n}\n\nexport type MenuLabel = { id: string; kind: 'label'; label: string }\nexport type MenuSeparator = { id: string; kind: 'separator' }\nexport type MenuEntry = MenuItem | MenuLabel | MenuSeparator\n\n/**\n * A search field at the top of the panel, for a menu that picks from more choices\n * than fit at once \u2014 people, tags. It is a GET form: the server narrows the items\n * and renders the menu open again, so the list and what is picked stay server-owned.\n */\nexport type MenuSearch = {\n action: string\n name: string\n value?: string\n label: string\n placeholder?: string\n /** Query parameters the search keeps, so narrowing the list does not drop a filter. */\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type MenuProps = {\n id: string\n label: string\n items: readonly MenuEntry[]\n /** Replaces the label inside the trigger \u2014 an icon, say. The label still names it. */\n trigger?: JSXChild\n open?: boolean\n align?: 'start' | 'end'\n /** `compact` sits in a row of list facets at their height. */\n size?: 'default' | 'compact'\n /** How many choices are in effect, shown on the trigger. Zero shows nothing. */\n count?: number\n search?: MenuSearch\n /**\n * `bottom` (default) opens below the trigger. `top` opens above it, for a\n * trigger near the bottom of a container that clips overflow \u2014 a fixed\n * modal footer, say \u2014 where the panel would otherwise render past the\n * container's edge and be cut off rather than merely overlapping content.\n */\n placement?: 'top' | 'bottom'\n}\n\nexport const Menu = (props: MenuProps): TemplateResult => (\n <details\n data-ui=\"menu\"\n data-align={props.align ?? 'start'}\n data-size={props.size === 'compact' ? 'compact' : null}\n data-active={props.count ? 'true' : null}\n data-placement={props.placement ?? 'bottom'}\n open={props.open === true ? true : undefined}\n >\n {/* `details` exposes the open state natively; the runtime mirrors it into aria-expanded on toggle. */}\n <summary\n data-ui=\"menu-trigger\"\n aria-haspopup=\"menu\"\n aria-controls={`${props.id}-panel`}\n // A trigger that is only an icon still needs a name, and a pointer needs a hint.\n aria-label={props.trigger !== undefined ? props.label : undefined}\n title={props.trigger !== undefined ? props.label : undefined}\n >\n {props.trigger ?? props.label}\n {!!props.count && <span data-ui=\"menu-trigger-count\">{String(props.count)}</span>}\n </summary>\n <div data-ui=\"menu-panel\" id={`${props.id}-panel`} role=\"menu\" aria-label={props.label}>\n {props.search && (\n <form data-ui=\"menu-search\" role=\"search\" method=\"get\" action={props.search.action}>\n {each(\n Object.entries(props.search.hidden ?? {}),\n ([name]) => name,\n ([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ),\n )}\n <input\n data-ui=\"menu-search-input\"\n type=\"search\"\n name={props.search.name}\n value={props.search.value ?? ''}\n placeholder={props.search.placeholder ?? props.search.label}\n aria-label={props.search.label}\n autocomplete=\"off\"\n />\n </form>\n )}\n {each(\n props.items,\n (item) => item.id,\n (item) => {\n if (item.kind === 'separator') return <hr data-ui=\"menu-separator\" />\n if (item.kind === 'label')\n return (\n <span data-ui=\"menu-label\" role=\"presentation\">\n {item.label}\n </span>\n )\n const role = item.checked === undefined ? 'menuitem' : 'menuitemcheckbox'\n const content = (\n <>\n {item.checked !== undefined && (\n <span data-ui=\"menu-item-check\" aria-hidden=\"true\">\n {item.checked ? '\u2713' : ''}\n </span>\n )}\n {item.leading !== undefined && (\n <span data-ui=\"menu-item-leading\" aria-hidden=\"true\">\n {item.leading}\n </span>\n )}\n <span data-ui=\"menu-item-copy\">\n <span>{item.label}</span>\n {item.description && <small>{item.description}</small>}\n </span>\n {item.shortcut && <kbd data-ui=\"menu-item-shortcut\">{item.shortcut}</kbd>}\n </>\n )\n if (item.disabled)\n return (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: `role` is menuitemcheckbox whenever aria-checked is set; otherwise aria-checked is null and not rendered.\n <span\n data-ui=\"menu-item\"\n role={role}\n aria-checked={item.checked === undefined ? null : String(item.checked)}\n aria-disabled=\"true\"\n tabIndex=\"-1\"\n >\n {content}\n </span>\n )\n if (item.href)\n return (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: `role` is menuitemcheckbox whenever aria-checked is set; otherwise aria-checked is null and not rendered.\n <a\n data-ui=\"menu-item\"\n data-destructive={item.destructive ? 'true' : null}\n role={role}\n aria-checked={item.checked === undefined ? null : String(item.checked)}\n href={item.href}\n >\n {content}\n </a>\n )\n return (\n // biome-ignore lint/a11y/useAriaPropsSupportedByRole: `role` is menuitemcheckbox whenever aria-checked is set; otherwise aria-checked is null and not rendered.\n <button\n data-ui=\"menu-item\"\n data-destructive={item.destructive ? 'true' : null}\n role={role}\n aria-checked={item.checked === undefined ? null : String(item.checked)}\n type=\"submit\"\n name={item.name ?? 'intent'}\n value={item.value ?? item.id}\n form={item.form ?? null}\n >\n {content}\n </button>\n )\n },\n )}\n </div>\n </details>\n)\n\nexport const ActionMenu = (props: Omit<MenuProps, 'trigger'> & { triggerLabel?: string }): TemplateResult => (\n <Menu\n {...props}\n trigger={<span>{props.triggerLabel ?? props.label} \u00B7\u00B7\u00B7</span>}\n align={props.align ?? 'end'}\n />\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['popover', 'popover-trigger', 'popover-panel', 'popover-close'] as const\n\nexport type PopoverProps = {\n id: string\n label: string\n trigger: JSXChild\n body: JSXChild\n open: boolean\n openHref: string\n closeHref: string\n closeLabel: string\n placement?: 'bottom-start' | 'bottom-end' | 'top-start' | 'top-end'\n}\n\nexport const Popover = (props: PopoverProps): TemplateResult => (\n <span data-ui=\"popover\" data-open={String(props.open)} data-placement={props.placement ?? 'bottom-start'}>\n <a\n data-ui=\"popover-trigger\"\n href={props.open ? props.closeHref : props.openHref}\n aria-haspopup=\"dialog\"\n aria-expanded={String(props.open)}\n aria-controls={`${props.id}-panel`}\n >\n {props.trigger}\n </a>\n {props.open && (\n <aside\n data-ui=\"popover-panel\"\n id={`${props.id}-panel`}\n role=\"dialog\"\n aria-label={props.label}\n tabindex=\"-1\"\n >\n <a data-ui=\"popover-close\" href={props.closeHref} aria-label={props.closeLabel}>\n \u00D7\n </a>\n {props.body}\n </aside>\n )}\n </span>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['tooltip', 'tooltip-trigger', 'tooltip-content'] as const\n\nexport const Tooltip = (props: {\n id: string\n text: string\n trigger: JSXChild\n placement?: 'top' | 'right' | 'bottom' | 'left'\n}): TemplateResult => (\n <span data-ui=\"tooltip\" data-placement={props.placement ?? 'top'}>\n <span data-ui=\"tooltip-trigger\" aria-describedby={props.id}>\n {props.trigger}\n </span>\n <span data-ui=\"tooltip-content\" id={props.id} role=\"tooltip\">\n {props.text}\n </span>\n </span>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'modal-layer',\n 'modal-backdrop',\n 'modal-sheet',\n 'modal-head',\n 'modal-heading',\n 'modal-title-row',\n 'modal-title',\n 'modal-description',\n 'modal-close',\n 'modal-body',\n 'modal-actions',\n] as const\n\nexport const ModalSheet = (props: {\n id: string\n title: string\n body: JSXChild\n /** Required for `overlay`: a route modal closes by navigating. Ignored in `client` mode. */\n closeHref?: string\n closeLabel: string\n description?: string | null\n /**\n * What state the thing in this dialog is in, beside its title \u2014 a badge, not a\n * sentence. It belongs to the heading, so it stays put when the body scrolls\n * and is read out with the title rather than after it.\n */\n status?: JSXChild\n actions?: JSXChild\n presentation?: 'sheet' | 'dialog'\n size?: 'default' | 'large'\n /**\n * `content` (default) lets a dialog grow with its body up to the viewport cap. `fixed`\n * holds the dialog at that cap so switching between tabs of different heights does not\n * resize it; the body scrolls inside while the head stays put.\n */\n height?: 'content' | 'fixed'\n /**\n * Overrides `fixed`'s height (default: the viewport cap, `100dvh` minus a margin \u2014\n * tall on most screens). A CSS length or `calc()`/`min()` expression, for a dialog\n * whose own content is shorter than the viewport and would otherwise sit in a mostly\n * empty frame. Set inline with `!important`, not as a plain CSS rule: a legacy\n * admin stylesheet (`packages/ketsuite/src/modules/backend/design/forms.css`)\n * still targets these same `data-ui` hooks at equal specificity and would\n * otherwise win the cascade by loading later. Ignored when `height` isn't `fixed`.\n */\n fixedHeight?: string\n /**\n * `overlay` is URL-owned: its close controls are links the route runtime\n * follows. `client` belongs to a modal island that opens and closes it in the\n * browser: close controls are buttons, and it carries no route-modal marker, so\n * no navigation layer ever treats closing it as a page change.\n */\n mode?: 'overlay' | 'embedded' | 'client'\n unsavedPrompt?: string | null\n}): TemplateResult => {\n const client = props.mode === 'client'\n const embedded = props.mode === 'embedded'\n return (\n <div\n data-ui=\"modal-layer\"\n data-mode={props.mode ?? 'overlay'}\n data-route-modal={embedded || client ? null : 'true'}\n data-client-modal={client ? 'true' : null}\n data-presentation={props.presentation ?? 'sheet'}\n data-unsaved-prompt={props.unsavedPrompt ?? null}\n >\n {client ? (\n <button data-ui=\"modal-backdrop\" type=\"button\" aria-label={props.closeLabel} tabindex=\"-1\">\n <span>{props.closeLabel}</span>\n </button>\n ) : (\n <a data-ui=\"modal-backdrop\" href={props.closeHref ?? '#'} aria-label={props.closeLabel}>\n <span>{props.closeLabel}</span>\n </a>\n )}\n <section\n id={props.id}\n data-ui=\"modal-sheet\"\n data-size={props.size ?? 'default'}\n data-height={props.height === 'fixed' ? 'fixed' : null}\n style={props.height === 'fixed' && props.fixedHeight ? `height: ${props.fixedHeight} !important` : ''}\n role=\"dialog\"\n aria-modal={embedded ? 'false' : 'true'}\n aria-labelledby={`${props.id}-title`}\n aria-describedby={props.description ? `${props.id}-description` : null}\n tabindex={embedded ? null : '-1'}\n >\n <header data-ui=\"modal-head\">\n <div data-ui=\"modal-heading\">\n <div data-ui=\"modal-title-row\">\n <h2 data-ui=\"modal-title\" id={`${props.id}-title`}>\n {props.title}\n </h2>\n {props.status !== undefined && props.status}\n </div>\n {!!props.description && (\n <p data-ui=\"modal-description\" id={`${props.id}-description`}>\n {props.description}\n </p>\n )}\n </div>\n {client ? (\n <button\n data-ui=\"modal-close\"\n type=\"button\"\n aria-label={props.closeLabel}\n title={props.closeLabel}\n >\n \u00D7\n </button>\n ) : (\n <a\n data-ui=\"modal-close\"\n href={props.closeHref ?? '#'}\n aria-label={props.closeLabel}\n title={props.closeLabel}\n >\n \u00D7\n </a>\n )}\n </header>\n <div data-ui=\"modal-body\">{props.body}</div>\n {props.actions !== undefined && <footer data-ui=\"modal-actions\">{props.actions}</footer>}\n </section>\n </div>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, LinkButton } from '../../primitives/actions/index.tsx'\nimport type { ActionVariant } from '../../primitives/actions/index.tsx'\nimport { ModalSheet } from '../../patterns/modal-sheet/index.tsx'\n\nexport const HOOKS = ['dialog', 'confirm-dialog-message'] as const\n\nexport type DialogProps = {\n id: string\n title: string\n body: JSXChild\n closeHref: string\n closeLabel: string\n description?: string\n actions?: JSXChild\n mode?: 'overlay' | 'embedded'\n unsavedPrompt?: string\n}\n\nexport const Dialog = (props: DialogProps): TemplateResult => (\n <div data-ui=\"dialog\">\n <ModalSheet {...props} presentation=\"dialog\" />\n </div>\n)\n\nexport type ConfirmDialogProps = Omit<DialogProps, 'body' | 'actions'> & {\n message: string\n confirmLabel: string\n confirmName?: string\n confirmValue?: string\n confirmForm?: string\n confirmVariant?: ActionVariant\n}\n\nexport const ConfirmDialog = (props: ConfirmDialogProps): TemplateResult => (\n <Dialog\n {...props}\n body={<p data-ui=\"confirm-dialog-message\">{props.message}</p>}\n actions={\n <ActionGroup\n label={props.title}\n actions={[\n <LinkButton label={props.closeLabel} href={props.closeHref} />,\n <Button\n label={props.confirmLabel}\n variant={props.confirmVariant ?? 'destructive'}\n type=\"submit\"\n name={props.confirmName ?? 'intent'}\n value={props.confirmValue ?? 'confirm'}\n form={props.confirmForm ?? null}\n />,\n ]}\n />\n }\n />\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = [\n 'toast-region',\n 'toast',\n 'toast-copy',\n 'toast-title',\n 'toast-message',\n 'toast-actions',\n] as const\n\nexport type ToastProps = {\n id: string\n title: string\n message?: string\n tone?: Tone\n actions?: JSXChild\n}\n\nexport const Toast = (props: ToastProps): TemplateResult => (\n <article\n data-ui=\"toast\"\n data-tone={props.tone ?? 'neutral'}\n role={props.tone === 'danger' ? 'alert' : 'status'}\n >\n <div data-ui=\"toast-copy\">\n <strong data-ui=\"toast-title\">{props.title}</strong>\n {props.message && <p data-ui=\"toast-message\">{props.message}</p>}\n </div>\n {props.actions !== undefined && <div data-ui=\"toast-actions\">{props.actions}</div>}\n </article>\n)\n\nexport const ToastRegion = (props: { label: string; toasts: readonly ToastProps[] }): TemplateResult => (\n <section data-ui=\"toast-region\" aria-label={props.label} aria-live=\"polite\" aria-relevant=\"additions text\">\n {each(\n props.toasts,\n (toast) => toast.id,\n (toast) => (\n <Toast {...toast} />\n ),\n )}\n </section>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['spinner', 'visually-hidden'] as const\n\nexport const Spinner = (props: { label: string; size?: 'small' | 'default' | 'large' }): TemplateResult => (\n <span data-ui=\"spinner\" data-size={props.size ?? 'default'} role=\"status\">\n <span data-ui=\"visually-hidden\">{props.label}</span>\n </span>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['skeleton', 'skeleton-line'] as const\n\nexport const Skeleton = (props: { label: string; lines?: number }): TemplateResult => (\n <div data-ui=\"skeleton\" role=\"status\" aria-label={props.label}>\n {each(\n Array.from({ length: Math.max(1, props.lines ?? 3) }),\n (_, index) => index,\n (_, index) => (\n <span data-ui=\"skeleton-line\" data-line={String(index + 1)} />\n ),\n )}\n </div>\n)\n", "// @ts-check\n\nconst focusableSelector = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n 'summary',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',')\n\n/** @param {ParentNode} root */\nconst focusables = (root) =>\n [...root.querySelectorAll(focusableSelector)].filter(\n (element) =>\n element instanceof HTMLElement &&\n !element.hidden &&\n element.getAttribute('aria-hidden') !== 'true' &&\n (typeof element.checkVisibility !== 'function' || element.checkVisibility()),\n )\n\n/**\n * Attach the small browser contract shared by server-rendered overlays.\n * The returned function restores inert state and removes every listener.\n *\n * @param {ParentNode} [root]\n * @returns {() => void}\n */\n/**\n * Popups built on <details> other than the action menu: a period filter and view\n * settings. They close on a click outside them and on Escape, like the menu.\n */\nconst DISMISSIBLE_POPUPS = ['[data-ui=\"timeframe-menu\"]', '[data-ui=\"view-settings\"]']\nconst DISMISSIBLE_POPUPS_OPEN = DISMISSIBLE_POPUPS.map((selector) => `${selector}[open]`).join(', ')\n\nexport const attachDesignSystemInteractions = (root = document) => {\n const activeBeforeOpen = document.activeElement instanceof HTMLElement ? document.activeElement : null\n const modal = root.querySelector('[data-ui=\"modal-layer\"][data-route-modal=\"true\"] [role=\"dialog\"]')\n const appShell = root.querySelector('[data-ui=\"app-shell\"], [data-ui=\"shell\"]')\n const priorInert = appShell instanceof HTMLElement ? appShell.inert : false\n if (modal instanceof HTMLElement) {\n if (appShell instanceof HTMLElement && !appShell.contains(modal)) appShell.inert = true\n const first = focusables(modal)[0]\n ;(first instanceof HTMLElement ? first : modal).focus()\n }\n\n const menus = /** @type {HTMLDetailsElement[]} */ (\n [...root.querySelectorAll('[data-ui=\"menu\"]')].filter((menu) => menu instanceof HTMLDetailsElement)\n )\n /** @param {HTMLDetailsElement} menu */\n const syncMenu = (menu) => {\n const trigger = menu.querySelector('[data-ui=\"menu-trigger\"]')\n if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', String(menu.open))\n }\n const menuCleanups = menus.map((menu) => {\n const onToggle = () => syncMenu(menu)\n menu.addEventListener('toggle', onToggle)\n syncMenu(menu)\n return () => menu.removeEventListener('toggle', onToggle)\n })\n\n const navigationMedia = window.matchMedia('(max-width: 48rem)')\n const navigationRoot = document.documentElement\n const priorNavigationOpen = navigationRoot.dataset.kvNavigationOpen\n const navigations = /** @type {HTMLDetailsElement[]} */ (\n [...root.querySelectorAll('[data-ui=\"app-navigation\"]')].filter(\n (navigation) => navigation instanceof HTMLDetailsElement,\n )\n )\n const navigationStates = new Map()\n\n /** @param {HTMLDetailsElement} navigation */\n const restoreNavigationTargets = (navigation) => {\n const state = navigationStates.get(navigation)\n if (!state?.targets) return\n for (const { element, inert } of state.targets) element.inert = inert\n state.targets = null\n }\n\n const syncNavigationLock = () => {\n const hasOpenNavigation = navigationMedia.matches && navigations.some((navigation) => navigation.open)\n if (hasOpenNavigation) navigationRoot.dataset.kvNavigationOpen = 'true'\n else if (priorNavigationOpen === undefined) delete navigationRoot.dataset.kvNavigationOpen\n else navigationRoot.dataset.kvNavigationOpen = priorNavigationOpen\n }\n\n /** @param {HTMLDetailsElement} navigation */\n const syncNavigation = (navigation) => {\n const trigger = navigation.querySelector('[data-ui=\"navigation-trigger\"]')\n const drawer = navigation.querySelector('[data-ui=\"navigation-drawer\"]')\n if (!(trigger instanceof HTMLElement) || !(drawer instanceof HTMLElement)) return\n const mobile = navigationMedia.matches\n trigger.dataset.open = String(mobile && navigation.open)\n if (!mobile) {\n drawer.removeAttribute('role')\n drawer.removeAttribute('aria-modal')\n drawer.removeAttribute('aria-label')\n restoreNavigationTargets(navigation)\n syncNavigationLock()\n return\n }\n drawer.setAttribute('role', 'dialog')\n drawer.setAttribute('aria-modal', 'true')\n drawer.setAttribute('aria-label', drawer.dataset.navigationLabel ?? 'Navigation')\n if (!navigation.open) {\n restoreNavigationTargets(navigation)\n syncNavigationLock()\n return\n }\n const state = navigationStates.get(navigation) ?? {}\n if (!state.targets) {\n const shell = navigation.closest('[data-ui=\"app-shell\"], [data-ui=\"shell\"]')\n const targets = shell\n ? [\n ...shell.querySelectorAll(\n ':scope > [data-ui=\"app-main\"], :scope > [data-ui=\"main\"], :scope > [data-ui=\"app-right-rail\"]',\n ),\n ]\n .filter((element) => element instanceof HTMLElement)\n .map((element) => ({ element, inert: element.inert }))\n : []\n for (const { element } of targets) element.inert = true\n state.targets = targets\n state.returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : trigger\n navigationStates.set(navigation, state)\n queueMicrotask(() => {\n if (!navigation.open || !navigationMedia.matches) return\n const activeItem = drawer.querySelector('[data-ui=\"navigation-item\"][aria-current=\"page\"]')\n const first = focusables(drawer)[0]\n const focusTarget = activeItem instanceof HTMLElement ? activeItem : first\n ;(focusTarget instanceof HTMLElement ? focusTarget : drawer).focus()\n })\n }\n syncNavigationLock()\n }\n\n /** @param {HTMLDetailsElement} navigation @param {boolean} [returnFocus] */\n const closeNavigation = (navigation, returnFocus = true) => {\n const state = navigationStates.get(navigation)\n navigation.open = false\n syncNavigation(navigation)\n if (returnFocus && state?.returnFocus instanceof HTMLElement && state.returnFocus.isConnected)\n state.returnFocus.focus()\n navigationStates.delete(navigation)\n }\n\n const navigationCleanups = navigations.map((navigation) => {\n const onToggle = () => syncNavigation(navigation)\n /** @param {MouseEvent} event */\n const onClick = (event) => {\n const target = event.target\n if (!(target instanceof Element)) return\n const branchTrigger = target.closest('[data-ui=\"navigation-branch-trigger\"]')\n const branch = branchTrigger?.closest('[data-ui=\"navigation-branch\"]')\n if (branch instanceof HTMLDetailsElement && branch.open) {\n event.preventDefault()\n return\n }\n if (!navigationMedia.matches || !navigation.open) return\n if (\n target.closest('[data-ui=\"navigation-close\"]') ||\n target.closest('[data-ui=\"navigation-backdrop\"]') ||\n target.closest('[data-ui=\"navigation-item\"]')\n )\n closeNavigation(navigation)\n }\n navigation.addEventListener('toggle', onToggle)\n navigation.addEventListener('click', onClick)\n syncNavigation(navigation)\n return () => {\n navigation.removeEventListener('toggle', onToggle)\n navigation.removeEventListener('click', onClick)\n restoreNavigationTargets(navigation)\n }\n })\n const onNavigationMediaChange = () => {\n for (const navigation of navigations) {\n if (navigation.open) closeNavigation(navigation, false)\n else syncNavigation(navigation)\n }\n }\n navigationMedia.addEventListener('change', onNavigationMediaChange)\n\n for (const popover of root.querySelectorAll('[data-ui=\"popover\"][data-open=\"true\"]')) {\n const trigger = popover.querySelector('[data-ui=\"popover-trigger\"]')\n const panel = popover.querySelector('[data-ui=\"popover-panel\"]')\n if (!(trigger instanceof HTMLElement) || !(panel instanceof HTMLElement)) continue\n const box = trigger.getBoundingClientRect()\n const gap = 8\n const placement = popover.getAttribute('data-placement') ?? 'bottom-start'\n const prefersTop = placement.startsWith('top')\n const prefersEnd = placement.endsWith('end')\n const topPosition = box.top - panel.offsetHeight - gap\n const bottomPosition = box.bottom + gap\n const topFits = topPosition >= 8\n const bottomFits = bottomPosition + panel.offsetHeight <= window.innerHeight - 8\n const useTop = prefersTop ? topFits || !bottomFits : !bottomFits && topFits\n const requestedLeft = prefersEnd ? box.right - panel.offsetWidth : box.left\n const left = Math.max(8, Math.min(requestedLeft, window.innerWidth - panel.offsetWidth - 8))\n const top = Math.max(\n 8,\n Math.min(useTop ? topPosition : bottomPosition, window.innerHeight - panel.offsetHeight - 8),\n )\n panel.dataset.runtimePositioned = 'true'\n panel.dataset.runtimePlacement = `${useTop ? 'top' : 'bottom'}-${prefersEnd ? 'end' : 'start'}`\n panel.style.left = `${Math.round(left)}px`\n panel.style.top = `${Math.round(top)}px`\n panel.style.right = 'auto'\n panel.style.bottom = 'auto'\n }\n\n /** @param {KeyboardEvent} event */\n const onKeydown = (event) => {\n const openNavigation = navigations.find((navigation) => navigationMedia.matches && navigation.open)\n if (openNavigation instanceof HTMLDetailsElement) {\n const drawer = openNavigation.querySelector('[data-ui=\"navigation-drawer\"]')\n if (event.key === 'Escape') {\n closeNavigation(openNavigation)\n event.preventDefault()\n return\n }\n if (event.key === 'Tab' && drawer instanceof HTMLElement) {\n const items = focusables(drawer)\n const first = items[0]\n const last = items.at(-1)\n if (!items.length) {\n event.preventDefault()\n drawer.focus()\n return\n }\n if (event.shiftKey && (document.activeElement === first || document.activeElement === drawer)) {\n event.preventDefault()\n if (last instanceof HTMLElement) last.focus()\n return\n }\n if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n if (first instanceof HTMLElement) first.focus()\n return\n }\n }\n }\n const treeItem = document.activeElement?.closest('[data-ui=\"tree\"] [role=\"treeitem\"]')\n if (treeItem instanceof HTMLElement) {\n const tree = treeItem.closest('[data-ui=\"tree\"]')\n const items = /** @type {HTMLElement[]} */ (\n tree\n ? [...tree.querySelectorAll('[role=\"treeitem\"]')].filter((item) => item instanceof HTMLElement)\n : []\n )\n const activeIndex = items.indexOf(treeItem)\n let nextIndex = -1\n if (event.key === 'ArrowDown') nextIndex = Math.min(items.length - 1, activeIndex + 1)\n else if (event.key === 'ArrowUp') nextIndex = Math.max(0, activeIndex - 1)\n else if (event.key === 'Home') nextIndex = 0\n else if (event.key === 'End') nextIndex = items.length - 1\n else if (event.key === 'ArrowRight') {\n const level = Number(treeItem.getAttribute('aria-level'))\n if (\n treeItem.getAttribute('aria-expanded') === 'true' &&\n Number(items[activeIndex + 1]?.getAttribute('aria-level')) > level\n )\n nextIndex = activeIndex + 1\n } else if (event.key === 'ArrowLeft') {\n const level = Number(treeItem.getAttribute('aria-level'))\n if (level > 1)\n nextIndex = items.findLastIndex(\n (item, index) => index < activeIndex && Number(item.getAttribute('aria-level')) < level,\n )\n }\n if (nextIndex >= 0 && items[nextIndex]) {\n for (const item of items) item.tabIndex = -1\n items[nextIndex].tabIndex = 0\n items[nextIndex].focus()\n event.preventDefault()\n return\n }\n }\n const treeGridRow = document.activeElement?.closest('[data-ui=\"tree-grid-row\"]')\n if (treeGridRow instanceof HTMLElement) {\n const grid = treeGridRow.closest('[data-ui=\"tree-grid\"]')\n const rows = /** @type {HTMLElement[]} */ (\n grid\n ? [...grid.querySelectorAll('[data-ui=\"tree-grid-row\"]')].filter(\n (row) => row instanceof HTMLElement,\n )\n : []\n )\n const activeIndex = rows.indexOf(treeGridRow)\n const nextIndex =\n event.key === 'ArrowDown'\n ? Math.min(rows.length - 1, activeIndex + 1)\n : event.key === 'ArrowUp'\n ? Math.max(0, activeIndex - 1)\n : event.key === 'Home'\n ? 0\n : event.key === 'End'\n ? rows.length - 1\n : -1\n if (nextIndex >= 0 && rows[nextIndex]) {\n for (const row of rows) row.tabIndex = -1\n rows[nextIndex].tabIndex = 0\n rows[nextIndex].focus()\n event.preventDefault()\n return\n }\n }\n const activeMenu = document.activeElement?.closest('[data-ui=\"menu\"]')\n if (activeMenu instanceof HTMLDetailsElement && !activeMenu.open) {\n const trigger = activeMenu.querySelector('[data-ui=\"menu-trigger\"]')\n if (document.activeElement === trigger && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {\n activeMenu.open = true\n const items = /** @type {HTMLElement[]} */ (\n [\n ...activeMenu.querySelectorAll(\n '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]',\n ),\n ].filter((item) => item instanceof HTMLElement && item.getAttribute('aria-disabled') !== 'true')\n )\n const item = event.key === 'ArrowUp' ? items.at(-1) : items[0]\n if (item instanceof HTMLElement) item.focus()\n event.preventDefault()\n return\n }\n }\n const openMenu = document.activeElement?.closest('[data-ui=\"menu\"][open]')\n if (openMenu instanceof HTMLDetailsElement) {\n const trigger = openMenu.querySelector('[data-ui=\"menu-trigger\"]')\n const items = /** @type {HTMLElement[]} */ (\n [\n ...openMenu.querySelectorAll(\n '[role=\"menuitem\"], [role=\"menuitemcheckbox\"], [role=\"menuitemradio\"]',\n ),\n ].filter((item) => item instanceof HTMLElement && item.getAttribute('aria-disabled') !== 'true')\n )\n const active = document.activeElement\n const activeIndex = active instanceof HTMLElement ? items.indexOf(active) : -1\n let nextIndex = -1\n if (event.key === 'ArrowDown') nextIndex = activeIndex < 0 ? 0 : (activeIndex + 1) % items.length\n else if (event.key === 'ArrowUp')\n nextIndex = activeIndex < 0 ? items.length - 1 : (activeIndex - 1 + items.length) % items.length\n else if (event.key === 'Home') nextIndex = 0\n else if (event.key === 'End') nextIndex = items.length - 1\n if (nextIndex >= 0 && items[nextIndex] instanceof HTMLElement) {\n items[nextIndex].focus()\n event.preventDefault()\n return\n }\n if ((event.key === 'Enter' || event.key === ' ') && document.activeElement === trigger) {\n if (items[0] instanceof HTMLElement) items[0].focus()\n event.preventDefault()\n return\n }\n }\n if (event.key === 'Escape' && openMenu instanceof HTMLDetailsElement) {\n openMenu.open = false\n const trigger = openMenu.querySelector('[data-ui=\"menu-trigger\"]')\n if (trigger instanceof HTMLElement) trigger.focus()\n event.preventDefault()\n return\n }\n const openPopup = [...root.querySelectorAll(DISMISSIBLE_POPUPS_OPEN)].at(-1)\n if (event.key === 'Escape' && openPopup instanceof HTMLDetailsElement) {\n openPopup.open = false\n const summary = openPopup.querySelector('summary')\n if (summary instanceof HTMLElement) summary.focus()\n event.preventDefault()\n return\n }\n const openPopover = document.activeElement?.closest('[data-ui=\"popover\"][data-open=\"true\"]')\n if (event.key === 'Escape' && openPopover instanceof HTMLElement) {\n const close = openPopover.querySelector('[data-ui=\"popover-close\"]')\n if (close instanceof HTMLElement) close.click()\n event.preventDefault()\n return\n }\n if (!(modal instanceof HTMLElement)) return\n if (event.key === 'Escape') {\n const close = modal.querySelector('[data-ui=\"modal-close\"]')\n if (close instanceof HTMLElement) close.click()\n event.preventDefault()\n return\n }\n if (event.key !== 'Tab') return\n const items = focusables(modal)\n if (!items.length) {\n event.preventDefault()\n modal.focus()\n return\n }\n const first = items[0]\n const last = items.at(-1)\n if (event.shiftKey && (document.activeElement === first || document.activeElement === modal)) {\n event.preventDefault()\n if (last instanceof HTMLElement) last.focus()\n } else if (!event.shiftKey && document.activeElement === last) {\n event.preventDefault()\n if (first instanceof HTMLElement) first.focus()\n }\n }\n /**\n * Table selection. `select-all` toggles the enabled row checkboxes of its own table, a row\n * change keeps the header checked or indeterminate, and every bulk bar bound to the same form\n * (`data-form`) shows the live count and enables its actions only while something is selected.\n *\n * @param {Element} table\n */\n const syncSelection = (table) => {\n const rows = /** @type {HTMLInputElement[]} */ (\n [...table.querySelectorAll('[data-ui=\"row-select\"]')].filter(\n (input) => input instanceof HTMLInputElement && !input.disabled,\n )\n )\n const checked = rows.filter((input) => input.checked)\n const all = table.querySelector('[data-ui=\"select-all\"]')\n if (all instanceof HTMLInputElement) {\n all.checked = rows.length > 0 && checked.length === rows.length\n all.indeterminate = checked.length > 0 && checked.length < rows.length\n }\n for (const input of rows)\n input.closest('[data-ui=\"row\"]')?.toggleAttribute('data-selected', input.checked)\n const form = rows[0]?.getAttribute('form')\n if (!form) return\n for (const bar of document.querySelectorAll(\n `[data-ui=\"bulk-actions\"][data-form=\"${CSS.escape(form)}\"]`,\n )) {\n if (checked.length) bar.setAttribute('data-has-selection', 'true')\n else bar.removeAttribute('data-has-selection')\n for (const count of bar.querySelectorAll('[data-ui=\"bulk-count\"]'))\n count.textContent = String(checked.length)\n for (const button of bar.querySelectorAll('[data-ui=\"bulk-action-list\"] button')) {\n if (button instanceof HTMLButtonElement && button.dataset.kvAlwaysDisabled !== 'true')\n button.disabled = checked.length === 0\n }\n }\n }\n /** @param {Event} event */\n const onSelectionChange = (event) => {\n const target = event.target\n if (!(target instanceof HTMLInputElement)) return\n const hook = target.getAttribute('data-ui')\n if (hook !== 'select-all' && hook !== 'row-select') return\n const table = target.closest('[data-ui=\"table\"]')\n if (!table) return\n if (hook === 'select-all') {\n for (const input of table.querySelectorAll('[data-ui=\"row-select\"]')) {\n if (input instanceof HTMLInputElement && !input.disabled) input.checked = target.checked\n }\n }\n syncSelection(table)\n }\n for (const table of root.querySelectorAll('[data-ui=\"table\"]')) {\n if (table.querySelector('[data-ui=\"row-select\"]')) syncSelection(table)\n }\n document.addEventListener('change', onSelectionChange)\n\n /** @param {MouseEvent} event */\n const onDocumentClick = (event) => {\n const target = event.target\n if (!(target instanceof Node)) return\n for (const menu of root.querySelectorAll('[data-ui=\"menu\"][open]')) {\n if (!(menu instanceof HTMLDetailsElement)) continue\n const item = target instanceof Element ? target.closest('[data-ui=\"menu-item\"]') : null\n if (!menu.contains(target) || (item && item.getAttribute('aria-disabled') !== 'true')) menu.open = false\n }\n // Every other popup built on <details> closes when the reader clicks outside it.\n for (const popup of root.querySelectorAll(DISMISSIBLE_POPUPS_OPEN)) {\n if (popup instanceof HTMLDetailsElement && !popup.contains(target)) popup.open = false\n }\n }\n document.addEventListener('keydown', onKeydown)\n document.addEventListener('click', onDocumentClick)\n\n return () => {\n document.removeEventListener('keydown', onKeydown)\n document.removeEventListener('click', onDocumentClick)\n document.removeEventListener('change', onSelectionChange)\n for (const cleanup of menuCleanups) cleanup()\n navigationMedia.removeEventListener('change', onNavigationMediaChange)\n for (const cleanup of navigationCleanups) cleanup()\n if (priorNavigationOpen === undefined) delete navigationRoot.dataset.kvNavigationOpen\n else navigationRoot.dataset.kvNavigationOpen = priorNavigationOpen\n if (appShell instanceof HTMLElement) appShell.inert = priorInert\n if (activeBeforeOpen?.isConnected) activeBeforeOpen.focus()\n }\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport type FieldIssue = { path: string; message: string }\n\nexport const issueFor = (issues: readonly FieldIssue[] | undefined, name: string): string | null =>\n issues?.find((issue) => issue.path === name)?.message ?? null\n\nexport type FieldFrameProps = {\n id: string\n label: string\n control: JSXChild\n help?: string | null\n error?: string | null\n required?: boolean\n span?: 'half' | 'full'\n kind: string\n}\n\nexport const FieldFrame = (props: FieldFrameProps): TemplateResult => {\n const helpId = props.help ? `${props.id}-help` : null\n const errorId = props.error ? `${props.id}-error` : null\n return (\n <div\n data-ui=\"field\"\n data-kind={props.kind}\n data-span={props.span ?? 'half'}\n data-invalid={String(!!props.error)}\n >\n <label data-ui=\"field-label\" for={props.id}>\n {props.label}\n {props.required && (\n <span data-ui=\"field-required\" aria-hidden=\"true\">\n {' *'}\n </span>\n )}\n </label>\n {props.control}\n {!!props.help && (\n <small data-ui=\"field-help\" id={helpId ?? undefined}>\n {props.help}\n </small>\n )}\n {!!props.error && (\n <small data-ui=\"field-error\" id={errorId ?? undefined}>\n {props.error}\n </small>\n )}\n </div>\n )\n}\n\nexport const describedBy = (id: string, help?: string | null, error?: string | null): string | null =>\n [help ? `${id}-help` : null, error ? `${id}-error` : null].filter(Boolean).join(' ') || null\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Field } from '../../primitives/field/index.tsx'\nimport type { FieldOption, FieldProps } from '../../primitives/field/index.tsx'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['switch-control', 'switch-track'] as const\n\ntype CommonProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst normalize = (props: CommonProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\nexport const TextField = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"text\" />\nexport const TextArea = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"textarea\" />\n)\nexport const NumberField = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"number\" />\n)\nexport const MoneyField = (props: CommonProps): TemplateResult => (\n <Field {...normalize({ ...props, step: props.step ?? '0.01' })} type=\"decimal\" />\n)\nexport const SearchField = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"text\" />\nexport const Checkbox = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"checkbox\" />\n)\nexport const CheckboxGroup = (props: CommonProps): TemplateResult => (\n <Field {...normalize(props)} type=\"checkbox-group\" />\n)\nexport const RadioGroup = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"radio\" />\nexport const Select = (props: CommonProps): TemplateResult => <Field {...normalize(props)} type=\"select\" />\n\nexport type SwitchProps = Omit<CommonProps, 'options' | 'placeholder' | 'step' | 'min' | 'max'> & {\n checked?: boolean\n value?: string\n}\n\nexport const Switch = (props: SwitchProps): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n const checked = props.checked ?? props.value === '1'\n return (\n <FieldFrame\n id={props.id}\n label={props.label}\n help={props.help}\n error={error}\n required={props.required}\n span={props.span}\n kind=\"switch\"\n control={\n <label data-ui=\"switch-control\">\n <input\n id={props.id}\n type=\"checkbox\"\n role=\"switch\"\n name={props.name}\n value={props.value ?? '1'}\n checked={checked}\n aria-checked={String(checked)}\n disabled={props.disabled === true}\n required={props.required === true}\n aria-invalid={error ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, error)}\n />\n <span data-ui=\"switch-track\" aria-hidden=\"true\" />\n </label>\n }\n />\n )\n}\n\nexport type { CommonProps as ScalarFieldProps, FieldIssue, FieldOption }\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = [\n 'combobox',\n 'combobox-input-row',\n 'combobox-toggle',\n 'combobox-listbox',\n 'combobox-option',\n 'combobox-empty',\n 'tag-picker',\n] as const\n\nexport type ComboboxOption = { value: string; label: string; description?: string; disabled?: boolean }\nexport type ComboboxProps = {\n id: string\n name: string\n /** Name used for the human-readable search text; defaults to `${name}Query`. */\n queryName?: string\n label: string\n query: string\n value?: string | null\n options: readonly ComboboxOption[]\n open: boolean\n openHref: string\n closeHref: string\n help?: string | null\n error?: string | null\n issues?: readonly FieldIssue[]\n required?: boolean\n disabled?: boolean\n loading?: boolean\n loadingLabel?: string\n noResultsLabel?: string\n span?: 'half' | 'full'\n}\n\nconst ComboControl = (\n props: ComboboxProps & { resolvedError: string | null; submitValue: boolean },\n): TemplateResult => (\n <div data-ui=\"combobox\" data-open={String(props.open)}>\n {props.submitValue && (\n <input type=\"hidden\" name={props.name} value={props.value ?? ''} disabled={props.disabled === true} />\n )}\n <div data-ui=\"combobox-input-row\">\n <input\n data-ui=\"field-control\"\n id={props.id}\n name={props.queryName ?? `${props.name}Query`}\n value={props.query}\n role=\"combobox\"\n aria-autocomplete=\"list\"\n aria-expanded={String(props.open)}\n aria-controls={`${props.id}-listbox`}\n aria-invalid={props.resolvedError ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, props.resolvedError)}\n required={props.required === true}\n disabled={props.disabled === true}\n autocomplete=\"off\"\n />\n <a\n data-ui=\"combobox-toggle\"\n href={props.open ? props.closeHref : props.openHref}\n aria-label={props.open ? 'Close options' : 'Open options'}\n >\n \u25BE\n </a>\n </div>\n {props.open && (\n <div data-ui=\"combobox-listbox\" id={`${props.id}-listbox`} role=\"listbox\">\n {props.loading ? (\n <p data-ui=\"combobox-empty\">{props.loadingLabel ?? 'Loading\u2026'}</p>\n ) : props.options.length === 0 ? (\n <p data-ui=\"combobox-empty\">{props.noResultsLabel ?? 'No results'}</p>\n ) : (\n each(\n props.options,\n (option) => option.value,\n (option) => (\n <a\n data-ui=\"combobox-option\"\n href={\n option.disabled\n ? null\n : `${props.closeHref}${props.closeHref.includes('?') ? '&' : '?'}${encodeURIComponent(props.name)}=${encodeURIComponent(option.value)}`\n }\n role=\"option\"\n aria-selected={String(option.value === props.value)}\n aria-disabled={option.disabled ? 'true' : null}\n >\n <strong>{option.label}</strong>\n {option.description && <small>{option.description}</small>}\n </a>\n ),\n )\n )}\n </div>\n )}\n </div>\n)\n\nconst ComboboxField = (props: ComboboxProps & { submitValue: boolean }): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n return (\n <FieldFrame\n {...props}\n kind=\"combobox\"\n error={error}\n control={<ComboControl {...props} resolvedError={error} />}\n />\n )\n}\n\nexport const Combobox = (props: ComboboxProps): TemplateResult => <ComboboxField {...props} submitValue />\n\nexport type MultiComboboxProps = Omit<ComboboxProps, 'value'> & {\n values: readonly string[]\n removeHref: (value: string) => string\n}\n\nexport const MultiCombobox = (props: MultiComboboxProps): TemplateResult => (\n <div data-ui=\"tag-picker\">\n <ComboboxField {...props} value={null} submitValue={false} />\n {each(\n props.values,\n (value) => value,\n (value) => (\n <>\n <input type=\"hidden\" name={props.name} value={value} disabled={props.disabled === true} />\n <a\n href={props.removeHref(value)}\n aria-label={`Remove ${props.options.find((option) => option.value === value)?.label ?? value}`}\n >\n {props.options.find((option) => option.value === value)?.label ?? value} \u00D7\n </a>\n </>\n ),\n )}\n </div>\n)\n\nexport const TagPicker = (props: MultiComboboxProps): TemplateResult => <MultiCombobox {...props} />\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Field } from '../../primitives/field/index.tsx'\nimport type { FieldProps } from '../../primitives/field/index.tsx'\nimport { issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['date-range'] as const\n\ntype TemporalProps = Omit<FieldProps, 'type' | 'fields' | 'control' | 'error'> & {\n issues?: readonly FieldIssue[]\n error?: string | null\n}\n\nconst temporal = (props: TemporalProps): FieldProps => ({\n ...props,\n error: props.error ?? issueFor(props.issues, props.name),\n})\n\n/** Civil dates are passed through as YYYY-MM-DD text and are never converted through UTC. */\nexport const DatePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"date\" />\n\nexport type DateRangePickerProps = {\n id: string\n label: string\n start: Omit<TemporalProps, 'label' | 'span'>\n end: Omit<TemporalProps, 'label' | 'span'>\n startLabel: string\n endLabel: string\n span?: 'half' | 'full'\n}\n\nexport const DateRangePicker = (props: DateRangePickerProps): TemplateResult => (\n <fieldset data-ui=\"date-range\" data-span={props.span ?? 'full'}>\n <legend>{props.label}</legend>\n <Field {...temporal({ ...props.start, label: props.startLabel })} type=\"date\" />\n <Field {...temporal({ ...props.end, label: props.endLabel })} type=\"date\" />\n </fieldset>\n)\n\n/** Local date-time text is submitted unchanged; the app adapter owns timezone interpretation. */\nexport const DateTimePicker = (props: TemporalProps): TemplateResult => (\n <Field {...temporal(props)} type=\"datetime-local\" />\n)\n\nexport const TimePicker = (props: TemporalProps): TemplateResult => <Field {...temporal(props)} type=\"time\" />\n\nexport type { TemporalProps }\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { describedBy, FieldFrame, issueFor } from '../shared.tsx'\nimport type { FieldIssue } from '../shared.tsx'\n\nexport const HOOKS = ['file-upload', 'drop-zone', 'upload-status'] as const\n\nexport type FileUploadProps = {\n id: string\n name: string\n label: string\n accept?: string\n multiple?: boolean\n required?: boolean\n disabled?: boolean\n help?: string | null\n error?: string | null\n issues?: readonly FieldIssue[]\n status?: JSXChild\n span?: 'half' | 'full'\n}\n\nconst uploadControl = (props: FileUploadProps, error: string | null, drop: boolean): TemplateResult => (\n <div data-ui={drop ? 'drop-zone' : 'file-upload'}>\n <input\n data-ui=\"field-control\"\n id={props.id}\n type=\"file\"\n name={props.name}\n accept={props.accept}\n multiple={props.multiple === true}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={error ? 'true' : null}\n aria-describedby={describedBy(props.id, props.help, error)}\n />\n {props.status !== undefined && <div data-ui=\"upload-status\">{props.status}</div>}\n </div>\n)\n\nconst renderUpload = (props: FileUploadProps, drop: boolean): TemplateResult => {\n const error = props.error ?? issueFor(props.issues, props.name)\n return (\n <FieldFrame\n {...props}\n kind={drop ? 'drop-zone' : 'file-upload'}\n error={error}\n control={uploadControl(props, error, drop)}\n />\n )\n}\n\nexport const FileUpload = (props: FileUploadProps): TemplateResult => renderUpload(props, false)\nexport const DropZone = (props: FileUploadProps): TemplateResult => renderUpload(props, true)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Combobox } from '../combobox/index.tsx'\nimport type { ComboboxOption, ComboboxProps } from '../combobox/index.tsx'\n\nexport const HOOKS = ['relation-picker'] as const\n\nexport type RelationPickerProps<T> = Omit<ComboboxProps, 'options'> & {\n results: readonly T[]\n getValue: (result: T) => string\n getLabel: (result: T) => string\n getDescription?: (result: T) => string | undefined\n}\n\n/** The application supplies permission-filtered results; this renderer performs no lookup. */\nexport const RelationPicker = <T,>(props: RelationPickerProps<T>): TemplateResult => {\n const options: ComboboxOption[] = props.results.map((result) => ({\n value: props.getValue(result),\n label: props.getLabel(result),\n description: props.getDescription?.(result),\n }))\n return (\n <div data-ui=\"relation-picker\">\n <Combobox {...props} options={options} />\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button } from '../../primitives/actions/index.tsx'\nimport { Menu } from '../../interactions/menu/index.tsx'\n\nexport const HOOKS = [\n 'search-bar',\n 'filter-bar',\n 'applied-filters',\n 'applied-filter',\n 'saved-views',\n 'saved-view',\n 'view-settings',\n 'view-settings-options',\n] as const\n\nexport const withQueryState = (\n href: string,\n changes: Readonly<Record<string, string | number | boolean | null | undefined>>,\n): string => {\n const url = new URL(href, 'http://ket.local')\n for (const [key, value] of Object.entries(changes)) {\n if (value === null || value === undefined || value === '') url.searchParams.delete(key)\n else url.searchParams.set(key, String(value))\n }\n return `${url.pathname}${url.search}${url.hash}`\n}\n\nexport type SearchBarProps = {\n action: string\n value?: string | null\n name?: string\n id?: string\n label?: string\n placeholder?: string\n submitLabel?: string\n hidden?: Readonly<Record<string, string>>\n}\n\nexport const SearchBar = (props: SearchBarProps): TemplateResult => (\n <form data-ui=\"search-bar\" action={props.action} method=\"get\" role=\"search\">\n {each(\n Object.entries(props.hidden ?? {}),\n ([name]) => name,\n ([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ),\n )}\n <label for={props.id ?? 'resource-search'}>{props.label ?? 'Search'}</label>\n <input\n id={props.id ?? 'resource-search'}\n type=\"search\"\n name={props.name ?? 'q'}\n value={props.value ?? ''}\n placeholder={props.placeholder}\n autocomplete=\"off\"\n />\n <Button type=\"submit\" label={props.submitLabel ?? 'Search'} size=\"compact\" />\n </form>\n)\n\nexport const FilterBar = (props: { label?: string; filters: readonly JSXChild[] }): TemplateResult => (\n <section data-ui=\"filter-bar\" aria-label={props.label ?? 'Filters'}>\n {props.filters}\n </section>\n)\n\nexport type AppliedFilter = { id: string; label: string; value: string; removeHref: string }\nexport const AppliedFilters = (props: {\n label?: string\n filters: readonly AppliedFilter[]\n clearHref?: string\n clearLabel?: string\n}): TemplateResult => (\n <nav data-ui=\"applied-filters\" aria-label={props.label ?? 'Applied filters'}>\n {each(\n props.filters,\n (filter) => filter.id,\n (filter) => (\n <a\n data-ui=\"applied-filter\"\n href={filter.removeHref}\n aria-label={`Remove ${filter.label}: ${filter.value}`}\n >\n <strong>{filter.label}</strong> {filter.value} \u00D7\n </a>\n ),\n )}\n {props.clearHref && <a href={props.clearHref}>{props.clearLabel ?? 'Clear all'}</a>}\n </nav>\n)\n\nexport type SortChoice = { id: string; label: string; href: string; active?: boolean }\nexport const SortMenu = (props: {\n id: string\n label?: string\n choices: readonly SortChoice[]\n}): TemplateResult => (\n <Menu\n id={props.id}\n label={props.label ?? 'Sort'}\n align=\"end\"\n items={props.choices.map((choice) => ({\n id: choice.id,\n label: `${choice.active ? '\u2713 ' : ''}${choice.label}`,\n href: choice.href,\n }))}\n />\n)\n\nexport type SavedView = { id: string; label: string; href: string; active?: boolean; description?: string }\nexport const SavedViews = (props: { label?: string; views: readonly SavedView[] }): TemplateResult => (\n <nav data-ui=\"saved-views\" aria-label={props.label ?? 'Saved views'}>\n {each(\n props.views,\n (view) => view.id,\n (view) => (\n <a data-ui=\"saved-view\" href={view.href} aria-current={view.active ? 'page' : null}>\n <strong>{view.label}</strong>\n {view.description && <small>{view.description}</small>}\n </a>\n ),\n )}\n </nav>\n)\n\nexport type ViewSetting = { id: string; label: string; visible: boolean; disabled?: boolean }\nexport const ViewSettings = (props: {\n id: string\n label?: string\n action: string\n settings: readonly ViewSetting[]\n version?: string\n submitLabel?: string\n}): TemplateResult => (\n <details data-ui=\"view-settings\">\n <summary>{props.label ?? 'View settings'}</summary>\n <form action={props.action} method=\"post\">\n {props.version && <input type=\"hidden\" name=\"version\" value={props.version} />}\n <div data-ui=\"view-settings-options\">\n {each(\n props.settings,\n (setting) => setting.id,\n (setting) => (\n <label>\n <input\n type=\"checkbox\"\n name=\"columns\"\n value={setting.id}\n checked={setting.visible}\n disabled={setting.disabled === true}\n />\n {setting.label}\n </label>\n ),\n )}\n </div>\n <Button type=\"submit\" label={props.submitLabel ?? 'Save view'} size=\"compact\" />\n </form>\n </details>\n)\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LinkButton } from '../../primitives/actions/index.tsx'\n\nexport const HOOKS = ['inline-edit', 'inline-edit-value', 'inline-edit-form', 'inline-edit-actions'] as const\n\nexport type InlineEditProps = {\n id: string\n label: string\n value: JSXChild\n editing: boolean\n editHref: string\n cancelHref: string\n action: string\n name: string\n inputValue: string\n version?: string\n error?: string | null\n saveLabel?: string\n cancelLabel?: string\n}\n\nexport const InlineEdit = (props: InlineEditProps): TemplateResult => (\n <section data-ui=\"inline-edit\" aria-labelledby={`${props.id}-label`}>\n <strong id={`${props.id}-label`}>{props.label}</strong>\n {!props.editing ? (\n <span data-ui=\"inline-edit-value\">\n {props.value}\n <LinkButton href={props.editHref} label={`Edit ${props.label}`} size=\"compact\" />\n </span>\n ) : (\n <form data-ui=\"inline-edit-form\" action={props.action} method=\"post\">\n {props.version && <input type=\"hidden\" name=\"version\" value={props.version} />}\n <input\n id={`${props.id}-input`}\n name={props.name}\n value={props.inputValue}\n aria-label={props.label}\n aria-invalid={props.error ? 'true' : null}\n aria-describedby={props.error ? `${props.id}-error` : null}\n />\n {props.error && <small id={`${props.id}-error`}>{props.error}</small>}\n <span data-ui=\"inline-edit-actions\">\n <Button type=\"submit\" label={props.saveLabel ?? 'Save'} size=\"compact\" />\n <LinkButton href={props.cancelHref} label={props.cancelLabel ?? 'Cancel'} size=\"compact\" />\n </span>\n </form>\n )}\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState } from '../../primitives/feedback/index.tsx'\nimport { PagerBar } from '../../patterns/list-chrome/index.tsx'\nimport type { PagerBarProps } from '../../patterns/list-chrome/index.tsx'\n\nexport const HOOKS = [\n 'resource-list',\n 'resource-list-item',\n 'resource-list-select',\n 'resource-list-link',\n] as const\n\nexport type ResourceListProps<Row> = {\n label: string\n rows: readonly Row[]\n id: (row: Row) => string\n href: (row: Row) => string\n primary: (row: Row) => JSXChild\n secondary?: (row: Row) => JSXChild\n meta?: (row: Row) => JSXChild\n selectedIds?: readonly string[]\n selectionName?: string\n selectionForm?: string\n loading?: boolean\n loadingLabel?: string\n emptyTitle?: string\n emptyMessage?: string\n pager?: PagerBarProps\n}\n\nexport const ResourceList = <Row,>(props: ResourceListProps<Row>): TemplateResult => {\n if (props.loading) return <LoadingState label={props.loadingLabel ?? 'Loading resources'} />\n if (props.rows.length === 0)\n return (\n <EmptyState\n title={props.emptyTitle ?? 'No resources'}\n message={props.emptyMessage ?? 'No results match this view.'}\n />\n )\n return (\n <div>\n <ul data-ui=\"resource-list\" aria-label={props.label}>\n {each(props.rows, props.id, (row) => {\n const id = props.id(row)\n return (\n <li data-ui=\"resource-list-item\" data-selected={props.selectedIds?.includes(id) ? 'true' : null}>\n {props.selectedIds !== undefined && (\n <input\n data-ui=\"resource-list-select\"\n type=\"checkbox\"\n name={props.selectionName ?? 'ids'}\n value={id}\n form={props.selectionForm}\n checked={props.selectedIds.includes(id)}\n aria-label={`Select ${id}`}\n />\n )}\n <a data-ui=\"resource-list-link\" href={props.href(row)}>\n <strong>{props.primary(row)}</strong>\n {props.secondary && <span>{props.secondary(row)}</span>}\n </a>\n {props.meta && <span>{props.meta(row)}</span>}\n </li>\n )\n })}\n </ul>\n {props.pager && <PagerBar {...props.pager} />}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LinkButton } from '../../primitives/actions.tsx'\nimport type { ActionVariant } from '../../primitives/actions.tsx'\n\nexport type ListFacet = {\n id: string\n label: string\n href: string\n active?: boolean\n count?: number | string\n}\n\nexport type ListSearch = {\n id?: string\n action: string\n name?: string\n value?: string | null\n placeholder?: string | null\n label?: string\n submitLabel?: string\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type ListSortChoice = {\n value: string\n label: string\n selected?: boolean\n}\n\nexport type ListSort = {\n id?: string\n action: string\n name?: string\n label?: string\n submitLabel?: string\n choices: readonly ListSortChoice[]\n hidden?: Readonly<Record<string, string>>\n}\n\nexport type BulkAction = {\n id: string\n label: string\n name?: string\n value?: string\n variant?: ActionVariant\n disabled?: boolean\n}\n\nexport type BulkActionsProps = {\n form?: string | null\n selectedCount?: number\n summary?: JSXChild\n clearHref?: string | null\n clearLabel?: string\n actions: readonly BulkAction[]\n}\n\nexport type PagerPage = {\n label: string\n href: string\n active?: boolean\n}\n\nexport type PagerBarProps = {\n summary?: JSXChild\n previousHref?: string | null\n nextHref?: string | null\n previousLabel?: string\n nextLabel?: string\n pages?: readonly PagerPage[]\n label?: string\n}\n\nexport type ListChromeProps = {\n filtersLabel?: string\n /**\n * Filters that pick from a list rather than toggle \u2014 a compact `Menu` each.\n * They lead the facet row, at the facets' height.\n */\n filterMenus?: JSXChild\n viewsLabel?: string\n search?: ListSearch\n facets?: readonly ListFacet[]\n views?: readonly ListFacet[]\n sort?: ListSort\n status?: JSXChild\n actions?: JSXChild\n bulk?: BulkActionsProps\n pager?: PagerBarProps\n}\n\nexport const HOOKS = [\n 'list-chrome',\n 'list-chrome-row',\n 'list-search',\n 'list-search-label',\n 'list-search-input',\n 'list-search-submit',\n 'list-filter-menus',\n 'list-facets',\n 'list-facet',\n 'list-facet-count',\n 'list-view-switch',\n 'list-view',\n 'list-sort',\n 'list-sort-label',\n 'list-sort-select',\n 'list-status',\n 'list-actions',\n 'bulk-actions',\n 'bulk-summary',\n 'bulk-count',\n 'bulk-action-list',\n 'pager-bar',\n 'pager-summary',\n 'pager-pages',\n 'pager-page',\n 'pager-actions',\n 'pager-link',\n] as const\n\nconst hiddenFields = (fields: Readonly<Record<string, string>> | undefined): JSXChild =>\n fields\n ? each(\n Object.entries(fields),\n ([name]) => name,\n ([name, value]) => <input type=\"hidden\" name={name} value={value} />,\n )\n : ''\n\nconst SearchControl = (props: ListSearch): TemplateResult => (\n <form data-ui=\"list-search\" action={props.action} method=\"get\" role=\"search\">\n {hiddenFields(props.hidden)}\n <label data-ui=\"list-search-label\" for={props.id ?? `${props.name ?? 'q'}-search`}>\n {props.label ?? 'Search'}\n </label>\n <input\n data-ui=\"list-search-input\"\n id={props.id ?? `${props.name ?? 'q'}-search`}\n type=\"search\"\n name={props.name ?? 'q'}\n value={props.value ?? ''}\n placeholder={props.placeholder ?? ''}\n autocomplete=\"off\"\n />\n <span data-ui=\"list-search-submit\">\n <Button type=\"submit\" label={props.submitLabel ?? 'Search'} variant=\"tertiary\" size=\"compact\" />\n </span>\n </form>\n)\n\nconst FacetNav = (props: {\n label: string\n ui: 'list-facets' | 'list-view-switch'\n itemUi: string\n items: readonly ListFacet[]\n}): TemplateResult => (\n <nav data-ui={props.ui} aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <a\n data-ui={props.itemUi}\n data-active={item.active === true ? 'true' : null}\n href={item.href}\n aria-current={item.active === true ? 'page' : null}\n >\n <span>{item.label}</span>\n {item.count !== undefined && <span data-ui=\"list-facet-count\">{String(item.count)}</span>}\n </a>\n ),\n )}\n </nav>\n)\n\nconst SortControl = (props: ListSort): TemplateResult => (\n <form data-ui=\"list-sort\" action={props.action} method=\"get\">\n {hiddenFields(props.hidden)}\n <label data-ui=\"list-sort-label\" for={props.id ?? `${props.name ?? 'sort'}-select`}>\n {props.label ?? 'Sort'}\n </label>\n <select\n data-ui=\"list-sort-select\"\n id={props.id ?? `${props.name ?? 'sort'}-select`}\n name={props.name ?? 'sort'}\n >\n {each(\n props.choices,\n (choice) => choice.value,\n (choice) => (\n <option value={choice.value} selected={choice.selected === true}>\n {choice.label}\n </option>\n ),\n )}\n </select>\n <Button type=\"submit\" label={props.submitLabel ?? 'Apply'} variant=\"tertiary\" size=\"compact\" />\n </form>\n)\n\nexport const BulkActions = (props: BulkActionsProps): TemplateResult => (\n <div\n data-ui=\"bulk-actions\"\n data-form={props.form ?? null}\n data-has-selection={props.selectedCount && props.selectedCount > 0 ? 'true' : null}\n >\n <div data-ui=\"bulk-summary\">\n {/* `bulk-count` is the live figure the runtime updates as rows are checked. */}\n {props.summary ?? (\n <>\n <span data-ui=\"bulk-count\">{String(props.selectedCount ?? 0)}</span> selected\n </>\n )}\n {props.clearHref && (\n <LinkButton\n href={props.clearHref}\n label={props.clearLabel ?? 'Clear'}\n variant=\"tertiary\"\n size=\"compact\"\n />\n )}\n </div>\n <div data-ui=\"bulk-action-list\">\n {each(\n props.actions,\n (action) => action.id,\n (action) => (\n <Button\n type=\"submit\"\n form={props.form ?? null}\n name={action.name}\n value={action.value}\n label={action.label}\n variant={action.variant ?? 'secondary'}\n size=\"compact\"\n disabled={action.disabled === true || !(props.selectedCount && props.selectedCount > 0)}\n />\n ),\n )}\n </div>\n </div>\n)\n\nexport const PagerBar = (props: PagerBarProps): TemplateResult => (\n <nav data-ui=\"pager-bar\" aria-label={props.label ?? 'Pagination'}>\n {props.summary !== undefined && <span data-ui=\"pager-summary\">{props.summary}</span>}\n {props.pages && props.pages.length > 0 && (\n <span data-ui=\"pager-pages\">\n {each(\n props.pages,\n (page) => page.href,\n (page) => (\n <a\n data-ui=\"pager-page\"\n data-active={page.active === true ? 'true' : null}\n href={page.href}\n aria-current={page.active === true ? 'page' : null}\n >\n {page.label}\n </a>\n ),\n )}\n </span>\n )}\n <span data-ui=\"pager-actions\">\n {props.previousHref ? (\n <a\n data-ui=\"pager-link\"\n href={props.previousHref}\n rel=\"prev\"\n aria-label={props.previousLabel ?? 'Previous page'}\n >\n \u2039\n </a>\n ) : (\n <span data-ui=\"pager-link\" data-disabled=\"true\" aria-hidden=\"true\">\n \u2039\n </span>\n )}\n {props.nextHref ? (\n <a data-ui=\"pager-link\" href={props.nextHref} rel=\"next\" aria-label={props.nextLabel ?? 'Next page'}>\n \u203A\n </a>\n ) : (\n <span data-ui=\"pager-link\" data-disabled=\"true\" aria-hidden=\"true\">\n \u203A\n </span>\n )}\n </span>\n </nav>\n)\n\nexport const ListChrome = (props: ListChromeProps): TemplateResult => {\n const filters =\n props.filterMenus !== undefined ||\n (props.facets?.length ?? 0) > 0 ||\n (props.views?.length ?? 0) > 0 ||\n props.sort ? (\n <div data-ui=\"list-chrome-row\" data-row=\"filters\">\n {props.filterMenus !== undefined && <div data-ui=\"list-filter-menus\">{props.filterMenus}</div>}\n {props.facets && props.facets.length > 0 && (\n <FacetNav\n label={props.filtersLabel ?? 'Filters'}\n ui=\"list-facets\"\n itemUi=\"list-facet\"\n items={props.facets}\n />\n )}\n {props.views && props.views.length > 0 && (\n <FacetNav\n label={props.viewsLabel ?? 'Views'}\n ui=\"list-view-switch\"\n itemUi=\"list-view\"\n items={props.views}\n />\n )}\n {props.sort && <SortControl {...props.sort} />}\n </div>\n ) : null\n const meta =\n props.status !== undefined || props.actions !== undefined || props.pager ? (\n <div data-ui=\"list-chrome-row\" data-row=\"meta\">\n {props.status !== undefined && <div data-ui=\"list-status\">{props.status}</div>}\n {props.actions !== undefined && <div data-ui=\"list-actions\">{props.actions}</div>}\n {props.pager && <PagerBar {...props.pager} />}\n </div>\n ) : null\n const tail =\n filters || meta ? (\n <div data-ui=\"list-chrome-row\" data-row=\"tail\">\n {filters}\n {meta}\n </div>\n ) : null\n return (\n <div data-ui=\"list-chrome\" data-pattern=\"list-chrome\">\n <div data-ui=\"list-chrome-row\" data-row=\"query\">\n {props.search && <SearchControl {...props.search} />}\n {tail}\n </div>\n {props.bulk && <BulkActions {...props.bulk} />}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['data-grid-scroll', 'data-grid', 'data-grid-cap', 'data-grid-cell'] as const\n\nexport type DataGridColumn<Row> = {\n key: string\n label: string\n cell: (row: Row) => JSXChild\n visible?: boolean\n pinned?: 'start' | 'end'\n width?: string\n}\n\nexport type DataGridProps<Row> = {\n label: string\n rows: readonly Row[]\n id: (row: Row) => string\n columns: readonly DataGridColumn<Row>[]\n columnOrder?: readonly string[]\n maxRows?: number\n capLabel?: (shown: number, total: number) => string\n density?: 'compact' | 'default' | 'comfortable'\n}\n\nexport const DataGrid = <Row,>(props: DataGridProps<Row>): TemplateResult => {\n const maxRows = Math.max(1, props.maxRows ?? 500)\n const rows = props.rows.slice(0, maxRows)\n const order = props.columnOrder ?? props.columns.map((column) => column.key)\n const byKey = new Map(props.columns.map((column) => [column.key, column]))\n const columns = order\n .map((key) => byKey.get(key))\n .filter((column): column is DataGridColumn<Row> => !!column && column.visible !== false)\n return (\n <div data-ui=\"data-grid-scroll\" data-density={props.density ?? 'default'}>\n {props.rows.length > rows.length && (\n <p data-ui=\"data-grid-cap\" role=\"status\">\n {props.capLabel?.(rows.length, props.rows.length) ??\n `Showing ${rows.length} of ${props.rows.length} rows`}\n </p>\n )}\n <table\n data-ui=\"data-grid\"\n aria-label={props.label}\n style={`--kv-grid-columns: ${columns.map((column) => column.width ?? 'minmax(9rem, 1fr)').join(' ')}`}\n >\n <thead>\n <tr>\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <th scope=\"col\" data-pinned={column.pinned}>\n {column.label}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(rows, props.id, (row) => (\n <tr>\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <td data-ui=\"data-grid-cell\" data-pinned={column.pinned}>\n {column.cell(row)}\n </td>\n ),\n )}\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['tree', 'tree-item', 'tree-link', 'tree-grid', 'tree-grid-row'] as const\n\nexport type TreeNode = {\n id: string\n label: JSXChild\n href?: string\n active?: boolean\n expanded?: boolean\n children?: readonly TreeNode[]\n}\n\nconst activeNode = (items: readonly TreeNode[]): TreeNode | undefined => {\n for (const item of items) {\n if (item.active) return item\n if (item.children?.length && item.expanded !== false) {\n const nested = activeNode(item.children)\n if (nested) return nested\n }\n }\n return undefined\n}\n\nconst nodes = (items: readonly TreeNode[], level: number, tabbableId: string | undefined): TemplateResult => (\n <ul role={level === 1 ? 'tree' : 'group'}>\n {each(\n items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"tree-item\" role=\"none\">\n {item.href ? (\n <a\n data-ui=\"tree-link\"\n href={item.href}\n role=\"treeitem\"\n tabindex={item.id === tabbableId ? '0' : '-1'}\n aria-level={String(level)}\n aria-expanded={item.children?.length ? String(item.expanded !== false) : null}\n aria-current={item.active ? 'page' : null}\n >\n {item.label}\n </a>\n ) : (\n // biome-ignore lint/a11y/useFocusableInteractive: the serialized lowercase tabindex is the explicit focus contract for this treeitem.\n <span\n data-ui=\"tree-link\"\n role=\"treeitem\"\n tabindex={item.id === tabbableId ? '0' : '-1'}\n aria-level={String(level)}\n aria-expanded={item.children?.length ? String(item.expanded !== false) : null}\n aria-current={item.active ? 'page' : null}\n >\n {item.label}\n </span>\n )}\n {item.children?.length && item.expanded !== false\n ? nodes(item.children, level + 1, tabbableId)\n : null}\n </li>\n ),\n )}\n </ul>\n)\n\nexport const Tree = (props: { label: string; nodes: readonly TreeNode[] }): TemplateResult => (\n <nav data-ui=\"tree\" aria-label={props.label}>\n {nodes(props.nodes, 1, activeNode(props.nodes)?.id ?? props.nodes[0]?.id)}\n </nav>\n)\n\nexport type TreeGridColumn<Row> = { key: string; label: string; cell: (row: Row) => JSXChild }\nexport type TreeGridRow<Row> = { row: Row; level: number; expanded?: boolean; hasChildren?: boolean }\nexport const TreeGrid = <Row,>(props: {\n label: string\n rows: readonly TreeGridRow<Row>[]\n id: (row: Row) => string\n primary: (row: Row) => JSXChild\n columns: readonly TreeGridColumn<Row>[]\n}): TemplateResult => (\n // biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: ARIA treegrid intentionally augments the native table model with expandable hierarchical rows.\n <table data-ui=\"tree-grid\" role=\"treegrid\" aria-label={props.label}>\n <thead>\n <tr>\n <th scope=\"col\">Name</th>\n {each(\n props.columns,\n (column) => column.key,\n (column) => (\n <th scope=\"col\">{column.label}</th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(\n props.rows,\n (item) => props.id(item.row),\n (item) => (\n <tr\n data-ui=\"tree-grid-row\"\n tabindex={\n props.id(item.row) === (props.rows[0] ? props.id(props.rows[0].row) : null) ? '0' : '-1'\n }\n aria-level={String(item.level)}\n aria-expanded={item.hasChildren ? String(item.expanded !== false) : null}\n >\n <th scope=\"row\" style={`--kv-tree-level: ${item.level}`}>\n {props.primary(item.row)}\n </th>\n {each(\n props.columns,\n (column) => column.key,\n (column) => (\n <td>{column.cell(item.row)}</td>\n ),\n )}\n </tr>\n ),\n )}\n </tbody>\n </table>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'data-matrix',\n 'data-matrix-head',\n 'data-matrix-heading',\n 'data-matrix-eyebrow',\n 'data-matrix-title',\n 'data-matrix-description',\n 'data-matrix-summary',\n 'data-matrix-scroll',\n 'data-matrix-table',\n 'data-matrix-column',\n 'data-matrix-row-head',\n 'data-matrix-row-leading',\n 'data-matrix-row-title',\n 'data-matrix-row-description',\n 'data-matrix-row-status',\n 'data-matrix-row-actions',\n 'data-matrix-cell',\n] as const\n\nexport type DataMatrixColumn = {\n id: string\n label: string\n description?: string | null\n}\n\nexport type DataMatrixRow = {\n id: string\n label: string\n description?: string | null\n leading?: JSXChild\n status?: JSXChild\n actions?: JSXChild\n cells: Readonly<Record<string, JSXChild>>\n}\n\nexport type DataMatrixProps = {\n label: string\n /** Localised heading for the row identity column, for example \u201CCheckpoint\u201D. */\n rowLabel: string\n columns: readonly DataMatrixColumn[]\n rows: readonly DataMatrixRow[]\n eyebrow?: string | null\n title?: string | null\n description?: string | null\n summary?: JSXChild\n /** Controls only the scrollable table's readable floor, never the viewport. */\n width?: 'default' | 'wide'\n}\n\n/**\n * Compare the same dimensions across ordered records or checkpoints.\n * Applications own cell content and workflow actions; the matrix owns table\n * semantics, sticky row identity, horizontal containment and responsive chrome.\n */\nexport const DataMatrix = (props: DataMatrixProps): TemplateResult => (\n <section data-ui=\"data-matrix\" aria-label={props.label} data-width={props.width ?? 'default'}>\n {(props.title || props.description || props.summary !== undefined) && (\n <header data-ui=\"data-matrix-head\">\n <div data-ui=\"data-matrix-heading\">\n {!!props.eyebrow && <span data-ui=\"data-matrix-eyebrow\">{props.eyebrow}</span>}\n {!!props.title && <h2 data-ui=\"data-matrix-title\">{props.title}</h2>}\n {!!props.description && <p data-ui=\"data-matrix-description\">{props.description}</p>}\n </div>\n {props.summary !== undefined && <div data-ui=\"data-matrix-summary\">{props.summary}</div>}\n </header>\n )}\n {/* biome-ignore lint/a11y/noNoninteractiveTabindex: a horizontally scrolling region must be reachable by keyboard (WCAG 2.1.1). */}\n <div data-ui=\"data-matrix-scroll\" role=\"region\" tabIndex=\"0\" aria-label={props.label}>\n <table data-ui=\"data-matrix-table\">\n <thead>\n <tr>\n <th scope=\"col\" data-ui=\"data-matrix-row-head\">\n {props.rowLabel}\n </th>\n {each(\n props.columns,\n (column) => column.id,\n (column) => (\n <th scope=\"col\" data-ui=\"data-matrix-column\">\n <strong>{column.label}</strong>\n {!!column.description && <small>{column.description}</small>}\n </th>\n ),\n )}\n </tr>\n </thead>\n <tbody>\n {each(\n props.rows,\n (row) => row.id,\n (row) => (\n <tr>\n <th scope=\"row\" data-ui=\"data-matrix-row-head\">\n <div data-ui=\"data-matrix-row-title\">\n {row.leading !== undefined && (\n <span data-ui=\"data-matrix-row-leading\">{row.leading}</span>\n )}\n <strong>{row.label}</strong>\n {row.status !== undefined && <span data-ui=\"data-matrix-row-status\">{row.status}</span>}\n </div>\n {!!row.description && (\n <small data-ui=\"data-matrix-row-description\">{row.description}</small>\n )}\n {row.actions !== undefined && <div data-ui=\"data-matrix-row-actions\">{row.actions}</div>}\n </th>\n {each(\n props.columns,\n (column) => column.id,\n (column) => (\n <td data-ui=\"data-matrix-cell\">{row.cells[column.id]}</td>\n ),\n )}\n </tr>\n ),\n )}\n </tbody>\n </table>\n </div>\n </section>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'bar-chart',\n 'bar-chart-rows',\n 'bar-chart-row',\n 'bar-chart-label',\n 'bar-chart-caption',\n 'bar-chart-track',\n 'bar-chart-fill',\n 'bar-chart-segment',\n 'bar-chart-value',\n 'bar-chart-scale',\n 'bar-chart-legend',\n 'bar-chart-legend-item',\n 'bar-chart-legend-swatch',\n 'bar-chart-empty',\n] as const\n\n/** How many categorical colours the stylesheet defines. Past this a legend stops being readable. */\nexport const CHART_SERIES = 6\n\n/** One part of a stacked bar. `series` is the 1-based palette slot its legend key also names. */\nexport type BarChartSegment = { series: number; value: number }\n\n/** A named magnitude, drawn against the largest in its set or against `max`. */\nexport type BarChartBar = {\n id: string\n label: string\n value: number\n caption?: string | null\n href?: string | null\n /** Parts of `value`, in legend order. Present on every bar when the chart is stacked. */\n segments?: readonly BarChartSegment[]\n}\n\nexport type BarChartKey = { id: string; label: string; series: number }\n\nexport type BarChartProps = {\n /** The accessible name of the list; the visible heading belongs to the surface around it. */\n label: string\n bars: readonly BarChartBar[]\n value: (bar: BarChartBar) => string\n /** A fixed top for the track, such as 100 for rates. Defaults to a round number above the largest bar. */\n max?: number\n /** What each stacked segment colour means. Required for a stacked chart to be readable. */\n keys?: readonly BarChartKey[]\n /** The scale spelled out under the bars, when the caller wants one printed. */\n scale?: readonly string[]\n empty?: string\n}\n\nconst finite = (value: number): number => (Number.isFinite(value) ? value : 0)\n\nconst slotOf = (series: number): string => String(((Math.max(1, Math.trunc(series)) - 1) % CHART_SERIES) + 1)\n\nconst percent = (value: number, of: number): string =>\n of <= 0 ? '0' : Math.min(100, (Math.abs(finite(value)) / of) * 100).toFixed(2)\n\n/**\n * A round number at or above `max`, for the top of a bar track.\n *\n * A track topped by the largest bar puts that bar against the frame and leaves\n * the reader deciding whether it touches; 1, 2, 2.5 or 5 times a power of ten is\n * the smallest ceiling that still reads as a number.\n */\nexport const axisCeiling = (max: number): number => {\n const value = finite(max)\n if (value <= 0) return 0\n const magnitude = 10 ** Math.floor(Math.log10(value))\n for (const step of [1, 2, 2.5, 5, 10]) if (value <= step * magnitude) return step * magnitude\n return 10 * magnitude\n}\n\n/**\n * Magnitudes against one scale.\n *\n * HTML rather than a canvas or SVG, on purpose. Bars run horizontally because\n * their labels are business names that need room, a row may link to the records\n * behind it, and every number is real text \u2014 so the chart reads the same to a\n * screen reader, a printed page and an agent comparing screens, with no script.\n *\n * Plain bars share one colour: they are one series, and a hue per row would\n * read as a legend that does not exist.\n *\n * A bar with `segments` is stacked: the fill still spans the bar's total, and\n * the segments divide it. `keys` names the segments once, above the rows.\n */\nexport const BarChart = (props: BarChartProps): TemplateResult => {\n const bars = props.bars.filter((bar) => finite(bar.value) !== 0)\n if (!bars.length) return <p data-ui=\"bar-chart-empty\">{props.empty ?? ''}</p>\n const largest = Math.max(...bars.map((bar) => Math.abs(finite(bar.value))), 0)\n const ceiling = props.max !== undefined && props.max > 0 ? props.max : axisCeiling(largest)\n const stacked = bars.some((bar) => bar.segments?.length)\n return (\n <div data-ui=\"bar-chart\" data-stacked={stacked ? 'true' : null}>\n {!!props.keys?.length && (\n <ul data-ui=\"bar-chart-legend\">\n {each(\n props.keys,\n (key) => key.id,\n (key) => (\n <li data-ui=\"bar-chart-legend-item\" data-series={slotOf(key.series)}>\n <span data-ui=\"bar-chart-legend-swatch\" aria-hidden=\"true\" />\n {key.label}\n </li>\n ),\n )}\n </ul>\n )}\n <ul data-ui=\"bar-chart-rows\" aria-label={props.label}>\n {each(\n bars,\n (bar) => bar.id,\n (bar) => (\n <li data-ui=\"bar-chart-row\">\n <span data-ui=\"bar-chart-label\">\n {bar.href ? <a href={bar.href}>{bar.label}</a> : bar.label}\n {!!bar.caption && <small data-ui=\"bar-chart-caption\">{bar.caption}</small>}\n </span>\n <span data-ui=\"bar-chart-track\" aria-hidden=\"true\">\n {bar.segments?.length ? (\n <span data-ui=\"bar-chart-fill\" style={`inline-size: ${percent(bar.value, ceiling)}%`}>\n {each(\n bar.segments,\n (segment) => String(segment.series),\n (segment) => (\n <span\n data-ui=\"bar-chart-segment\"\n data-series={slotOf(segment.series)}\n style={`inline-size: ${percent(segment.value, Math.abs(finite(bar.value)))}%`}\n />\n ),\n )}\n </span>\n ) : (\n <span\n data-ui=\"bar-chart-fill\"\n data-series={props.keys?.length ? slotOf(props.keys[0]!.series) : '1'}\n style={`inline-size: ${percent(bar.value, ceiling)}%`}\n />\n )}\n </span>\n <span data-ui=\"bar-chart-value\">{props.value(bar)}</span>\n </li>\n ),\n )}\n </ul>\n {!!props.scale?.length && (\n <div data-ui=\"bar-chart-scale\" aria-hidden=\"true\">\n {each(\n props.scale,\n (tick, at) => `${tick}:${at}`,\n (tick) => (\n <span>{tick}</span>\n ),\n )}\n </div>\n )}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'timeframe',\n 'timeframe-menu',\n 'timeframe-trigger',\n 'timeframe-label',\n 'timeframe-value',\n 'timeframe-content',\n 'timeframe-option',\n 'timeframe-option-label',\n 'timeframe-option-detail',\n 'timeframe-range',\n 'timeframe-asof',\n 'timeframe-note',\n] as const\n\nexport type TimeframeOption = {\n id: string\n label: string\n href: string\n active?: boolean\n /** Shown under the option, e.g. the dates the preset resolves to. */\n detail?: string | null\n}\n\nexport type TimeframeFilterProps = {\n id: string\n /** What is being filtered, e.g. \"Reporting period\". Also the group's label. */\n label: string\n options: readonly TimeframeOption[]\n /** The range the active option resolves to, e.g. \"2026-08-06 \u2192 2026-09-05\". */\n range?: string | null\n /** When the figures were computed, and the words for it. */\n asOf?: string | null\n asOfLabel?: string | null\n /** Trailing note, e.g. the timezone the range is expressed in. */\n note?: string | null\n}\n\n/**\n * The period a screen is reporting on.\n *\n * A filter, not navigation: tabs mean sibling views, and a row of periods read\n * as tabs looks like five screens rather than one screen under five constraints.\n * Every option is a link, so the choice survives a copied URL and needs no\n * script. The resolved range and the build time sit beside the choice, because\n * a period label alone does not say what a number covers.\n */\nexport const TimeframeFilter = (props: TimeframeFilterProps): TemplateResult => {\n const active = props.options.find((option) => option.active) ?? props.options[0]\n return (\n <div data-ui=\"timeframe\" role=\"group\" aria-label={props.label}>\n <details data-ui=\"timeframe-menu\">\n <summary data-ui=\"timeframe-trigger\" aria-label={props.label} title={props.label}>\n <span data-ui=\"timeframe-label\">{props.label}</span>\n <span data-ui=\"timeframe-value\">{active?.label ?? ''}</span>\n </summary>\n <div data-ui=\"timeframe-content\" id={`${props.id}-options`}>\n {each(\n props.options,\n (option) => option.id,\n (option) => (\n <a\n data-ui=\"timeframe-option\"\n data-active={String(option.active === true)}\n aria-current={option.active === true ? 'true' : null}\n href={option.href}\n >\n <span data-ui=\"timeframe-option-label\">{option.label}</span>\n {!!option.detail && <small data-ui=\"timeframe-option-detail\">{option.detail}</small>}\n </a>\n ),\n )}\n </div>\n </details>\n {!!props.range && <span data-ui=\"timeframe-range\">{props.range}</span>}\n {!!props.asOf && (\n <span data-ui=\"timeframe-asof\">\n {props.asOfLabel ? `${props.asOfLabel} ${props.asOf}` : props.asOf}\n </span>\n )}\n {!!props.note && <span data-ui=\"timeframe-note\">{props.note}</span>}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Avatar, Badge } from '../../primitives/status/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = [\n 'description-list',\n 'key-value',\n 'key-value-label',\n 'key-value-value',\n 'person',\n 'person-body',\n 'avatar-group',\n 'status',\n] as const\n\nexport type KeyValueProps = { label: JSXChild; value: JSXChild; emptyLabel?: string }\nexport const KeyValue = (props: KeyValueProps): TemplateResult => (\n <div data-ui=\"key-value\">\n <dt data-ui=\"key-value-label\">{props.label}</dt>\n <dd data-ui=\"key-value-value\">{props.value ?? props.emptyLabel ?? '\u2014'}</dd>\n </div>\n)\n\nexport const DescriptionList = (props: {\n label?: string\n items: readonly (KeyValueProps & { id: string })[]\n columns?: 1 | 2 | 3\n}): TemplateResult => (\n <dl data-ui=\"description-list\" aria-label={props.label} data-columns={String(props.columns ?? 2)}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <KeyValue {...item} />\n ),\n )}\n </dl>\n)\n\nexport type PersonProps = {\n name: string\n detail?: JSXChild\n href?: string\n size?: 'small' | 'default' | 'large'\n}\nexport const Person = (props: PersonProps): TemplateResult => (\n <span data-ui=\"person\" data-pattern=\"person\">\n <Avatar name={props.name} size={props.size} />\n <span data-ui=\"person-body\">\n {props.href ? <a href={props.href}>{props.name}</a> : <strong>{props.name}</strong>}\n {props.detail !== undefined && <small>{props.detail}</small>}\n </span>\n </span>\n)\n\nexport const AvatarGroup = (props: {\n label: string\n people: readonly { id: string; name: string }[]\n max?: number\n}): TemplateResult => {\n const visible = props.people.slice(0, props.max ?? 4)\n const remaining = props.people.length - visible.length\n return (\n <span data-ui=\"avatar-group\" role=\"group\" aria-label={props.label}>\n {each(\n visible,\n (person) => person.id,\n (person) => (\n <Avatar name={person.name} />\n ),\n )}\n {remaining > 0 && (\n <span role=\"img\" aria-label={`${remaining} more people`} title={`${remaining} more people`}>\n +{String(remaining)}\n </span>\n )}\n </span>\n )\n}\n\nexport const Status = (props: { label: string; tone?: Tone; detail?: string }): TemplateResult => (\n <span data-ui=\"status\" title={props.detail}>\n <Badge label={props.label} tone={props.tone} />\n </span>\n)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['formatted-date', 'formatted-number', 'formatted-money'] as const\n\nexport const FormattedDate = (props: {\n value: string\n locale?: string\n emptyLabel?: string\n}): TemplateResult => {\n const match = /^(\\d{4})-(\\d{2})-(\\d{2})$/u.exec(props.value)\n const date = match ? new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]))) : null\n const valid =\n !!match &&\n !!date &&\n date.getUTCFullYear() === Number(match[1]) &&\n date.getUTCMonth() === Number(match[2]) - 1 &&\n date.getUTCDate() === Number(match[3])\n const formatted = valid\n ? new Intl.DateTimeFormat(props.locale ?? 'vi-VN', { dateStyle: 'medium', timeZone: 'UTC' }).format(date)\n : (props.emptyLabel ?? '\u2014')\n return (\n <time data-ui=\"formatted-date\" datetime={valid ? props.value : null}>\n {formatted}\n </time>\n )\n}\n\nexport const FormattedNumber = (props: {\n value: number\n locale?: string\n minimumFractionDigits?: number\n maximumFractionDigits?: number\n}): TemplateResult => (\n <data data-ui=\"formatted-number\" value={String(props.value)}>\n {new Intl.NumberFormat(props.locale ?? 'vi-VN', {\n minimumFractionDigits: props.minimumFractionDigits,\n maximumFractionDigits: props.maximumFractionDigits,\n }).format(props.value)}\n </data>\n)\n\nexport const FormattedMoney = (props: {\n value: number\n currency: string\n locale?: string\n}): TemplateResult => (\n <data data-ui=\"formatted-money\" value={String(props.value)} data-currency={props.currency}>\n {new Intl.NumberFormat(props.locale ?? 'vi-VN', { style: 'currency', currency: props.currency }).format(\n props.value,\n )}\n </data>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup } from '../../primitives/actions/index.tsx'\nimport { Section } from '../../layouts/layout/index.tsx'\nimport { DescriptionList, Person, Status } from '../display/index.tsx'\nimport type { KeyValueProps, PersonProps } from '../display/index.tsx'\nimport type { Tone } from '../../primitives/status/index.tsx'\n\nexport const HOOKS = ['record-summary', 'record-summary-main', 'record-actions', 'record-rail'] as const\n\nexport type RecordSummaryProps = {\n title: JSXChild\n subtitle?: JSXChild\n person?: PersonProps\n status?: { label: string; tone?: Tone; detail?: string }\n facts?: readonly (KeyValueProps & { id: string })[]\n}\n\nexport const RecordSummary = (props: RecordSummaryProps): TemplateResult => (\n <header data-ui=\"record-summary\">\n <div data-ui=\"record-summary-main\">\n <h2>{props.title}</h2>\n {props.subtitle !== undefined && <p>{props.subtitle}</p>}\n {props.person && <Person {...props.person} />}\n </div>\n {props.status && <Status {...props.status} />}\n {props.facts && <DescriptionList items={props.facts} columns={3} />}\n </header>\n)\n\nexport const RecordActions = (props: { label?: string; actions: readonly JSXChild[] }): TemplateResult => (\n <nav data-ui=\"record-actions\" aria-label={props.label ?? 'Record actions'}>\n <ActionGroup actions={props.actions} />\n </nav>\n)\n\nexport type RecordRailSection = { id: string; title: string; body: JSXChild; actions?: JSXChild }\nexport const RecordRail = (props: {\n label?: string\n sections: readonly RecordRailSection[]\n}): TemplateResult => (\n <aside data-ui=\"record-rail\" aria-label={props.label ?? 'Record context'}>\n {each(\n props.sections,\n (section) => section.id,\n (section) => (\n <Section title={section.title} body={section.body} actions={section.actions} />\n ),\n )}\n </aside>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState, Notice } from '../../primitives/feedback/index.tsx'\nimport { Person } from '../display/index.tsx'\n\nexport const HOOKS = [\n 'activity-timeline',\n 'activity-item',\n 'activity-marker',\n 'audit-log',\n 'audit-item',\n] as const\n\nexport type ActivityItem = {\n id: string\n actor: string\n actorDetail?: string\n action: JSXChild\n datetime: string\n timeLabel: string\n detail?: JSXChild\n redacted?: boolean\n}\n\ntype FeedProps = {\n label: string\n items: readonly ActivityItem[]\n loading?: boolean\n loadingLabel?: string\n emptyTitle?: string\n emptyMessage?: string\n error?: string | null\n redactedLabel?: string\n}\n\nconst state = (props: FeedProps): TemplateResult | null => {\n if (props.loading) return <LoadingState label={props.loadingLabel ?? 'Loading activity'} />\n if (props.error) return <Notice title=\"Activity unavailable\" message={props.error} tone=\"danger\" />\n if (!props.items.length)\n return (\n <EmptyState\n title={props.emptyTitle ?? 'No activity'}\n message={props.emptyMessage ?? 'Nothing has happened yet.'}\n />\n )\n return null\n}\n\nexport const ActivityTimeline = (props: FeedProps): TemplateResult => {\n const placeholder = state(props)\n if (placeholder) return placeholder\n return (\n <ol data-ui=\"activity-timeline\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"activity-item\">\n <span data-ui=\"activity-marker\" aria-hidden=\"true\" />\n <div>\n <Person name={item.actor} detail={item.actorDetail} size=\"small\" />\n <p>{item.redacted ? (props.redactedLabel ?? 'Details redacted') : item.action}</p>\n {item.detail !== undefined && !item.redacted && <div>{item.detail}</div>}\n <time datetime={item.datetime}>{item.timeLabel}</time>\n </div>\n </li>\n ),\n )}\n </ol>\n )\n}\n\nexport const AuditLog = (props: FeedProps): TemplateResult => {\n const placeholder = state(props)\n if (placeholder) return placeholder\n return (\n <ol data-ui=\"audit-log\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"audit-item\">\n <time datetime={item.datetime}>{item.timeLabel}</time>\n <strong>{item.actor}</strong>\n <span>{item.redacted ? (props.redactedLabel ?? 'Change redacted') : item.action}</span>\n </li>\n ),\n )}\n </ol>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState, LoadingState, Notice } from '../../primitives/feedback/index.tsx'\n\nexport const HOOKS = ['attachments', 'attachment', 'media-gallery', 'media-item'] as const\n\nexport type AttachmentItem = {\n id: string\n name: string\n href?: string\n meta?: JSXChild\n redacted?: boolean\n}\n\nexport const Attachments = (props: {\n label: string\n items: readonly AttachmentItem[]\n loading?: boolean\n error?: string | null\n redactedLabel?: string\n}): TemplateResult => {\n if (props.loading) return <LoadingState label=\"Loading attachments\" />\n if (props.error) return <Notice title=\"Attachments unavailable\" message={props.error} tone=\"danger\" />\n if (!props.items.length) return <EmptyState title=\"No attachments\" message=\"No files have been attached.\" />\n return (\n <ul data-ui=\"attachments\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <li data-ui=\"attachment\">\n {item.redacted || !item.href ? (\n <span>{props.redactedLabel ?? item.name}</span>\n ) : (\n <a href={item.href}>{item.name}</a>\n )}\n {item.meta !== undefined && <small>{item.meta}</small>}\n </li>\n ),\n )}\n </ul>\n )\n}\n\nexport type MediaItem = {\n id: string\n src?: string\n alt: string\n caption?: JSXChild\n href?: string\n redacted?: boolean\n}\nexport const MediaGallery = (props: {\n label: string\n items: readonly MediaItem[]\n emptyTitle?: string\n redactedLabel?: string\n}): TemplateResult => {\n if (!props.items.length)\n return <EmptyState title={props.emptyTitle ?? 'No media'} message=\"No media is available.\" />\n return (\n <div data-ui=\"media-gallery\" role=\"group\" aria-label={props.label}>\n {each(\n props.items,\n (item) => item.id,\n (item) => (\n <figure data-ui=\"media-item\">\n {item.redacted || !item.src ? (\n <div role=\"img\" aria-label={props.redactedLabel ?? 'Media redacted'}>\n Restricted\n </div>\n ) : item.href ? (\n <a href={item.href}>\n <img src={item.src} alt={item.alt} loading=\"lazy\" />\n </a>\n ) : (\n <img src={item.src} alt={item.alt} loading=\"lazy\" />\n )}\n {item.caption !== undefined && <figcaption>{item.caption}</figcaption>}\n </figure>\n ),\n )}\n </div>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = [\n 'app-navigation',\n 'navigation-trigger',\n 'navigation-trigger-icon',\n 'navigation-trigger-identity',\n 'navigation-trigger-label',\n 'navigation-layer',\n 'navigation-backdrop',\n 'navigation-drawer',\n 'navigation-header',\n 'navigation-identity',\n 'navigation-context',\n 'navigation-close',\n 'navigation-groups',\n 'navigation-group',\n 'navigation-group-label',\n 'navigation-items',\n 'navigation-supplementary',\n 'navigation-item',\n 'navigation-branch',\n 'navigation-branch-trigger',\n 'navigation-children',\n 'navigation-item-leading',\n 'navigation-item-copy',\n 'navigation-item-label',\n 'navigation-item-description',\n 'navigation-item-count',\n 'navigation-footer',\n] as const\n\ntype NavigationItemBase = {\n id: string\n label: string\n leading?: JSXChild\n description?: string\n count?: number\n}\n\nexport type NavigationItemData = NavigationItemBase &\n (\n | { href: string; active?: boolean; children?: never; expanded?: never }\n | { href?: never; active?: never; children: readonly NavigationItemData[]; expanded?: boolean }\n )\n\nexport type NavigationGroupData = {\n id: string\n label?: string\n items: readonly NavigationItemData[]\n}\n\nexport type AppNavigationProps = {\n id: string\n label: string\n identity: JSXChild\n groups: readonly NavigationGroupData[]\n context?: JSXChild\n supplementary?: JSXChild\n footer?: JSXChild\n navigationSlot?: string\n menuLabel?: string\n closeLabel?: string\n open?: boolean\n}\n\nexport const NavigationTrigger = (props: {\n controls: string\n label: string\n identity: JSXChild\n open?: boolean\n}): TemplateResult => (\n <summary\n data-ui=\"navigation-trigger\"\n aria-controls={props.controls}\n data-open={props.open === true ? 'true' : 'false'}\n >\n <span data-ui=\"navigation-trigger-icon\" aria-hidden=\"true\">\n <span />\n <span />\n <span />\n </span>\n <span data-ui=\"navigation-trigger-identity\">{props.identity}</span>\n <span data-ui=\"navigation-trigger-label\">{props.label}</span>\n </summary>\n)\n\nexport const NavigationHeader = (props: {\n identity: JSXChild\n context?: JSXChild\n closeLabel: string\n}): TemplateResult => (\n <header data-ui=\"navigation-header\">\n <div>\n <div data-ui=\"navigation-identity\">{props.identity}</div>\n {props.context !== undefined && <div data-ui=\"navigation-context\">{props.context}</div>}\n </div>\n <button data-ui=\"navigation-close\" type=\"button\" aria-label={props.closeLabel}>\n <span aria-hidden=\"true\" />\n </button>\n </header>\n)\n\nconst NavigationItemContent = (props: { item: NavigationItemData }): TemplateResult => (\n <>\n {props.item.leading !== undefined && (\n <span data-ui=\"navigation-item-leading\" aria-hidden=\"true\">\n {props.item.leading}\n </span>\n )}\n <span data-ui=\"navigation-item-copy\">\n <span data-ui=\"navigation-item-label\">{props.item.label}</span>\n {props.item.description !== undefined && (\n <span data-ui=\"navigation-item-description\">{props.item.description}</span>\n )}\n </span>\n {props.item.count !== undefined && (\n <span data-ui=\"navigation-item-count\">{String(props.item.count)}</span>\n )}\n </>\n)\n\nconst hasActiveItem = (item: NavigationItemData): boolean =>\n item.active === true || item.children?.some(hasActiveItem) === true\n\nconst renderNavigationItem = (\n props: NavigationItemData,\n branchGroup: string,\n level: number,\n): TemplateResult => {\n if (props.children !== undefined) {\n const open = props.expanded === true || hasActiveItem(props)\n return (\n <details data-ui=\"navigation-branch\" data-level={String(level)} name={branchGroup} open={open}>\n <summary data-ui=\"navigation-branch-trigger\">\n <NavigationItemContent item={props} />\n </summary>\n <div data-ui=\"navigation-children\" data-level={String(level + 1)}>\n {each(\n props.children,\n (item) => item.id,\n (item) => renderNavigationItem(item, `${props.id}-branches`, level + 1),\n )}\n </div>\n </details>\n )\n }\n return (\n <a\n data-ui=\"navigation-item\"\n data-active={props.active === true ? 'true' : null}\n data-level={String(level)}\n href={props.href}\n aria-current={props.active === true ? 'page' : null}\n >\n <NavigationItemContent item={props} />\n </a>\n )\n}\n\nexport const NavigationItem = (props: NavigationItemData): TemplateResult => (\n <>{renderNavigationItem(props, `${props.id}-root-branches`, 1)}</>\n)\n\nexport const NavigationGroup = (props: NavigationGroupData & { branchGroup?: string }): TemplateResult => (\n <section data-ui=\"navigation-group\" aria-labelledby={props.label ? `${props.id}-label` : null}>\n {props.label !== undefined && (\n <h2 data-ui=\"navigation-group-label\" id={`${props.id}-label`}>\n {props.label}\n </h2>\n )}\n <div data-ui=\"navigation-items\">\n {each(\n props.items,\n (item) => item.id,\n (item) => renderNavigationItem(item, props.branchGroup ?? `${props.id}-branches`, 1),\n )}\n </div>\n </section>\n)\n\nexport const NavigationDrawer = (props: {\n id: string\n label: string\n identity: JSXChild\n groups: readonly NavigationGroupData[]\n context?: JSXChild\n supplementary?: JSXChild\n footer?: JSXChild\n navigationSlot?: string\n closeLabel: string\n}): TemplateResult => (\n <div data-ui=\"navigation-layer\">\n <button data-ui=\"navigation-backdrop\" type=\"button\" aria-label={props.closeLabel} tabIndex={-1} />\n <div data-ui=\"navigation-drawer\" id={props.id} data-navigation-label={props.label} tabIndex={-1}>\n <NavigationHeader identity={props.identity} context={props.context} closeLabel={props.closeLabel} />\n <nav data-ui=\"navigation-groups\" aria-label={props.label} data-ket-slot={props.navigationSlot}>\n {each(\n props.groups,\n (group) => group.id,\n (group) => (\n <NavigationGroup {...group} id={`${props.id}-${group.id}`} branchGroup={`${props.id}-branches`} />\n ),\n )}\n {props.supplementary !== undefined && (\n <div data-ui=\"navigation-supplementary\">{props.supplementary}</div>\n )}\n </nav>\n {props.footer !== undefined && <footer data-ui=\"navigation-footer\">{props.footer}</footer>}\n </div>\n </div>\n)\n\nexport const AppNavigation = (props: AppNavigationProps): TemplateResult => {\n const drawerId = `${props.id}-drawer`\n const closeLabel = props.closeLabel ?? 'Close navigation'\n return (\n <details data-ui=\"app-navigation\" data-navigation-id={props.id} open={props.open === true}>\n <NavigationTrigger\n controls={drawerId}\n label={props.menuLabel ?? 'Menu'}\n identity={props.identity}\n open={props.open}\n />\n <NavigationDrawer\n id={drawerId}\n label={props.label}\n identity={props.identity}\n groups={props.groups}\n context={props.context}\n supplementary={props.supplementary}\n footer={props.footer}\n navigationSlot={props.navigationSlot}\n closeLabel={closeLabel}\n />\n </details>\n )\n}\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { EmptyState } from '../../primitives/feedback.tsx'\nimport { Surface } from '../../layouts/index.tsx'\n\nexport type Cell = JSXChild\nexport type SortDirection = 'ascending' | 'descending'\nexport type TablePager = {\n label: string\n previousHref?: string | null\n nextHref?: string | null\n}\n\nexport type Column<Row> = {\n key: string\n label: string\n cell: (row: Row) => Cell\n align?: 'start' | 'end'\n kind?: 'text' | 'number' | 'currency' | 'date' | 'status' | 'identifier' | 'person' | 'media'\n priority?: 'primary' | 'secondary' | 'tertiary'\n width?: 'narrow' | 'medium' | 'wide'\n hidden?: boolean\n optional?: boolean\n sort?: {\n href: string\n direction?: SortDirection | null\n label?: string\n } | null\n}\n\nexport type TableSelection = {\n /** Associates row checkboxes with an external bulk-action form. */\n form?: string\n name?: string\n selectAllName?: string\n selectedIds?: readonly string[]\n disabled?: boolean\n}\n\nexport type TableGroup<Row> = {\n id: string\n label: JSXChild\n count?: number\n href?: string | null\n rows?: readonly Row[]\n children?: readonly TableGroup<Row>[]\n pager?: TablePager | null\n}\n\nexport type DataTableLabels<Row> = {\n selectAll?: string\n selectRow?: (row: Row, id: string) => string\n sortedAscending?: string\n sortedDescending?: string\n previousPage?: string\n nextPage?: string\n}\n\nexport type DataTableProps<Row> = {\n /** A shared surface contains the heading and an unframed table viewport. */\n title?: string\n actions?: JSXChild\n columns: readonly Column<Row>[]\n rows: readonly Row[]\n id: (row: Row) => string\n caption?: string | null\n rowHref?: (row: Row) => string\n selected?: (row: Row) => boolean\n emptyTitle?: string\n emptyMessage?: string\n emptyActions?: JSXChild\n /** Stack labelled cells on narrow screens, or retain horizontal scrolling. */\n responsive?: 'scroll' | 'stack'\n /** Use a denser gutter for high-volume operational lists. */\n gutter?: 'default' | 'compact'\n selection?: TableSelection | null\n groups?: readonly TableGroup<Row>[]\n shownColumns?: readonly string[]\n labels?: DataTableLabels<Row>\n}\n\nexport const HOOKS = [\n 'table-scroll',\n 'table',\n 'table-caption',\n 'select-col',\n 'select-all',\n 'col',\n 'sort-link',\n 'sort-icon',\n 'row',\n 'group-row',\n 'group-cell',\n 'group-link',\n 'group-indent',\n 'group-count',\n 'group-pager',\n 'group-pager-actions',\n 'cell',\n 'select-cell',\n 'row-select',\n 'row-link',\n] as const\n\nconst selectedColumnKeys = <Row,>(props: DataTableProps<Row>): readonly string[] =>\n props.shownColumns ?? props.columns.filter((column) => column.hidden !== true).map((column) => column.key)\n\nconst visibleColumns = <Row,>(props: DataTableProps<Row>): readonly Column<Row>[] => {\n const selected = new Set(selectedColumnKeys(props))\n return props.columns.filter((column) => selected.has(column.key))\n}\n\nconst colSpan = <Row,>(props: DataTableProps<Row>, columns: readonly Column<Row>[]): number =>\n columns.length + (props.selection ? 1 : 0)\n\nconst sortLabel = <Row,>(column: Column<Row>, labels: DataTableLabels<Row> | undefined): string | null => {\n if (!column.sort?.direction) return null\n return column.sort.direction === 'ascending'\n ? (labels?.sortedAscending ?? 'Sorted ascending')\n : (labels?.sortedDescending ?? 'Sorted descending')\n}\n\nconst columnHeader = <Row,>(\n column: Column<Row>,\n labels: DataTableLabels<Row> | undefined,\n): TemplateResult => (\n <th\n data-ui=\"col\"\n data-col={column.key}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? 'text'}\n data-width={column.width ?? null}\n data-optional={column.optional === true ? 'true' : null}\n data-sort={column.sort?.direction ?? null}\n aria-sort={column.sort?.direction ?? null}\n scope=\"col\"\n >\n {column.sort ? (\n <a data-ui=\"sort-link\" href={column.sort.href} aria-label={column.sort.label ?? column.label}>\n <span>{column.label}</span>\n {!!column.sort.direction && (\n <span data-ui=\"sort-icon\" aria-hidden=\"true\" title={sortLabel(column, labels) ?? undefined}>\n {column.sort.direction === 'ascending' ? '\u2191' : '\u2193'}\n </span>\n )}\n </a>\n ) : (\n column.label\n )}\n </th>\n)\n\nconst selectionHeader = <Row,>(props: DataTableProps<Row>): TemplateResult | null =>\n props.selection ? (\n <th data-ui=\"select-col\" scope=\"col\">\n <input\n data-ui=\"select-all\"\n type=\"checkbox\"\n name={props.selection.selectAllName ?? 'select-all'}\n aria-label={props.labels?.selectAll ?? 'Select all rows'}\n disabled={props.selection.disabled === true}\n />\n </th>\n ) : null\n\nconst isSelected = <Row,>(props: DataTableProps<Row>, row: Row): boolean => {\n if (props.selected?.(row) === true) return true\n return props.selection?.selectedIds?.includes(props.id(row)) === true\n}\n\nconst rowCells = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n row: Row,\n): TemplateResult => (\n <>\n {props.selection && (\n <td data-ui=\"select-cell\">\n <input\n data-ui=\"row-select\"\n type=\"checkbox\"\n name={props.selection.name ?? 'ids'}\n form={props.selection.form}\n value={props.id(row)}\n checked={isSelected(props, row)}\n aria-label={props.labels?.selectRow?.(row, props.id(row)) ?? `Select row ${props.id(row)}`}\n disabled={props.selection.disabled === true}\n />\n </td>\n )}\n {each(\n columns,\n (column) => column.key,\n (column) => (\n <td\n data-ui=\"cell\"\n data-col={column.key}\n data-linked={props.rowHref && column === columns[0] ? 'true' : null}\n data-align={column.align ?? 'start'}\n data-kind={column.kind ?? 'text'}\n data-priority={column.priority ?? 'secondary'}\n data-label={props.responsive === 'stack' ? column.label : null}\n >\n {props.rowHref && column === columns[0] ? (\n <a data-ui=\"row-link\" href={props.rowHref(row)}>\n {column.cell(row)}\n </a>\n ) : (\n column.cell(row)\n )}\n </td>\n ),\n )}\n </>\n)\n\nconst tableRow = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n row: Row,\n): TemplateResult => (\n <tr data-ui=\"row\" data-row={props.id(row)} data-selected={isSelected(props, row) ? 'true' : null}>\n {rowCells(props, columns, row)}\n </tr>\n)\n\nconst groupPager = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n pager: TablePager | null | undefined,\n): TemplateResult | null =>\n pager ? (\n <tr data-ui=\"group-pager\">\n <td colSpan={String(colSpan(props, columns))}>\n <span>{pager.label}</span>\n <span data-ui=\"group-pager-actions\">\n {pager.previousHref && (\n <a href={pager.previousHref} aria-label={props.labels?.previousPage ?? 'Previous page'}>\n \u2039\n </a>\n )}\n {pager.nextHref && (\n <a href={pager.nextHref} aria-label={props.labels?.nextPage ?? 'Next page'}>\n \u203A\n </a>\n )}\n </span>\n </td>\n </tr>\n ) : null\n\nconst groupRows = <Row,>(\n props: DataTableProps<Row>,\n columns: readonly Column<Row>[],\n group: TableGroup<Row>,\n depth: number,\n): TemplateResult => (\n <>\n <tr data-ui=\"group-row\" data-group={group.id} data-depth={String(depth)}>\n <th data-ui=\"group-cell\" colSpan={String(colSpan(props, columns))} scope=\"rowgroup\">\n <span data-ui=\"group-indent\" style={`--kv-group-depth: ${depth}`} aria-hidden=\"true\" />\n {group.href ? (\n <a data-ui=\"group-link\" href={group.href}>\n {group.label}\n </a>\n ) : (\n <span data-ui=\"group-link\">{group.label}</span>\n )}\n {group.count !== undefined && <span data-ui=\"group-count\">{String(group.count)}</span>}\n </th>\n </tr>\n {each(group.rows ?? [], props.id, (row) => tableRow(props, columns, row))}\n {each(\n group.children ?? [],\n (child) => child.id,\n (child) => groupRows(props, columns, child, depth + 1),\n )}\n {groupPager(props, columns, group.pager)}\n </>\n)\n\nexport const DataTable = <Row,>(props: DataTableProps<Row>): TemplateResult => {\n const columns = visibleColumns(props)\n const groups = props.groups ?? []\n const hasRows = props.rows.length > 0 || groups.length > 0\n\n const content = hasRows ? (\n <div\n data-ui=\"table-scroll\"\n data-pattern=\"data-table\"\n data-framed={props.title ? 'false' : null}\n data-responsive={props.responsive ?? 'scroll'}\n data-gutter={props.gutter === 'compact' ? 'compact' : null}\n >\n <table data-ui=\"table\" aria-label={props.caption ? null : (props.title ?? null)}>\n {!!props.caption && <caption data-ui=\"table-caption\">{props.caption}</caption>}\n <thead>\n <tr>\n {selectionHeader(props)}\n {each(\n columns,\n (column) => column.key,\n (column) => columnHeader(column, props.labels),\n )}\n </tr>\n </thead>\n <tbody>\n {groups.length > 0\n ? each(\n groups,\n (group) => group.id,\n (group) => groupRows(props, columns, group, 0),\n )\n : each(props.rows, props.id, (row) => tableRow(props, columns, row))}\n </tbody>\n </table>\n </div>\n ) : (\n <EmptyState\n title={props.emptyTitle ?? 'No records'}\n message={props.emptyMessage ?? 'There is nothing to show yet.'}\n actions={props.emptyActions}\n />\n )\n return props.title ? (\n <Surface title={props.title} actions={props.actions} padding=\"none\" body={content} />\n ) : (\n content\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'list-page',\n 'list-page-context',\n 'list-page-header',\n 'list-page-heading',\n 'list-page-title-row',\n 'list-page-eyebrow',\n 'list-page-title',\n 'list-page-description',\n 'list-page-actions',\n 'list-page-toolbar',\n 'list-page-controls',\n 'list-page-status',\n 'list-page-body',\n 'list-page-footer',\n] as const\n\nexport type ListPageProps = {\n title: string\n body: JSXChild\n /**\n * A narrow application context strip above the page identity. The application\n * owns its breadcrumbs and organisation switcher; the pattern owns the slot.\n */\n context?: JSXChild\n /**\n * `operational` is the full workspace composition used by application collection\n * screens. The unadorned variant remains available for embedded specimens.\n */\n variant?: 'operational'\n eyebrow?: string | null\n description?: string | null\n actions?: JSXChild\n controls?: JSXChild\n status?: JSXChild\n footer?: JSXChild\n}\n\n/**\n * The baseline for an operational collection screen.\n *\n * Applications own the translated copy, URL-driven controls and records. This\n * pattern owns their order and rhythm: identity and primary action first, query\n * controls second, then the result status and the collection itself.\n */\nexport const ListPage = (props: ListPageProps): TemplateResult => {\n const operational = props.variant === 'operational'\n // Result summaries close the collection in an operational workspace. Keeping\n // the legacy `status` prop as the input lets existing screens migrate without\n // duplicating their translated count as a separate footer value.\n const toolbarStatus = operational ? undefined : props.status\n const footer = props.footer ?? (operational ? props.status : undefined)\n\n return (\n <section data-ui=\"list-page\" data-variant={props.variant ?? null} data-pattern=\"list\">\n {pageIdentity('list-page', {\n context: props.context,\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n actions: props.actions,\n })}\n {(props.controls !== undefined || toolbarStatus !== undefined) && (\n <div data-ui=\"list-page-toolbar\">\n {toolbarStatus !== undefined && <div data-ui=\"list-page-status\">{toolbarStatus}</div>}\n {props.controls !== undefined && <div data-ui=\"list-page-controls\">{props.controls}</div>}\n </div>\n )}\n <div data-ui=\"list-page-body\">{props.body}</div>\n {footer !== undefined && <footer data-ui=\"list-page-footer\">{footer}</footer>}\n </section>\n )\n}\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentityContent, type PageIdentityKind } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'record-page',\n 'record-page-context',\n 'record-page-header',\n 'record-page-title-row',\n 'record-page-heading',\n 'record-page-title',\n 'record-page-subline',\n 'record-page-status',\n 'record-page-description',\n 'record-page-actions',\n 'record-page-meta',\n 'record-page-controller',\n 'record-page-navigation',\n 'record-page-layout',\n 'record-page-body',\n 'record-page-aside',\n] as const\n\nexport type RecordPageSlots = {\n header: string\n body: string\n fragmentTitle?: string\n}\n\nexport type RecordPageProps = {\n title: string\n body: JSXChild\n context?: JSXChild\n variant?: 'operational'\n width?: 'default' | 'wide'\n scope?: string | null\n description?: string | null\n status?: JSXChild\n actions?: JSXChild\n meta?: JSXChild\n controller?: JSXChild\n navigation?: JSXChild\n aside?: JSXChild\n asideLabel?: string | null\n slots?: RecordPageSlots\n}\n\ntype RecordPageKind = Extract<PageIdentityKind, 'record-page' | 'form-page'>\n\nexport const recordPage = (props: RecordPageProps, compatibilityKind?: RecordPageKind): TemplateResult => {\n const kind = compatibilityKind ?? 'record-page'\n if (props.slots?.fragmentTitle !== undefined)\n return (\n <ket-fragments data-title={props.slots.fragmentTitle}>\n <template data-ket-slot={props.slots.header}>\n {pageIdentityContent(kind, { ...props, groupDescription: true })}\n </template>\n <template data-ket-slot={props.slots.body}>{props.body}</template>\n </ket-fragments>\n )\n return (\n <section\n data-ui={kind}\n data-scope={props.scope ?? null}\n data-has-aside={String(props.aside !== undefined)}\n data-variant={props.variant ?? null}\n data-width={props.width ?? 'default'}\n data-pattern=\"record\"\n >\n {props.context !== undefined && <div data-ui={`${kind}-context`}>{props.context}</div>}\n <header data-ui={`${kind}-header`} data-ket-slot={props.slots?.header}>\n {pageIdentityContent(kind, { ...props, groupDescription: true })}\n </header>\n {props.controller !== undefined && <div data-ui={`${kind}-controller`}>{props.controller}</div>}\n {props.navigation !== undefined && <div data-ui={`${kind}-navigation`}>{props.navigation}</div>}\n <div data-ui={`${kind}-layout`}>\n <div data-ui={`${kind}-body`} data-ket-slot={props.slots?.body}>\n {props.body}\n </div>\n {props.aside !== undefined && (\n <aside data-ui={`${kind}-aside`} aria-label={props.asideLabel ?? null}>\n {props.aside}\n </aside>\n )}\n </div>\n </section>\n )\n}\n\n/** One durable subject: create, edit, inspect or advance its workflow. */\nexport const RecordPage = (props: RecordPageProps): TemplateResult => recordPage(props)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { recordPage, type RecordPageProps, type RecordPageSlots } from '../record-page.tsx'\n\nexport const HOOKS = [\n 'form-page',\n 'form-page-context',\n 'form-page-header',\n 'form-page-title-row',\n 'form-page-heading',\n 'form-page-title',\n 'form-page-subline',\n 'form-page-status',\n 'form-page-description',\n 'form-page-actions',\n 'form-page-meta',\n 'form-page-controller',\n 'form-page-navigation',\n 'form-page-layout',\n 'form-page-body',\n 'form-page-aside',\n] as const\n\nexport type FormPageSlots = RecordPageSlots\nexport type FormPageProps = RecordPageProps\n\n/** @deprecated Use RecordPage. Retained while application call sites migrate. */\nexport const FormPage = (props: FormPageProps): TemplateResult => recordPage(props, 'form-page')\n", "import type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { pageIdentity, type PageIdentityKind } from '../page-shell.tsx'\n\nexport const HOOKS = [\n 'workspace-page',\n 'workspace-page-context',\n 'workspace-page-header',\n 'workspace-page-heading',\n 'workspace-page-eyebrow',\n 'workspace-page-title-row',\n 'workspace-page-title',\n 'workspace-page-subline',\n 'workspace-page-status',\n 'workspace-page-description',\n 'workspace-page-actions',\n 'workspace-page-meta',\n 'workspace-page-toolbar',\n 'workspace-page-controls',\n 'workspace-page-body',\n] as const\n\nexport type WorkspacePageProps = {\n title: string\n body: JSXChild\n context?: JSXChild\n variant?: 'operational'\n /** Vertical work surfaces use flow; boards, timelines and maps use canvas. */\n layout?: 'flow' | 'canvas'\n eyebrow?: string | null\n description?: string | null\n status?: JSXChild\n meta?: JSXChild\n actions?: JSXChild\n controls?: JSXChild\n}\n\ntype CompatibilityKind = Extract<PageIdentityKind, 'dashboard-page' | 'board-page'>\n\nexport const workspacePage = (\n props: WorkspacePageProps,\n compatibilityKind?: CompatibilityKind,\n): TemplateResult => {\n const kind = compatibilityKind ?? (props.layout === 'canvas' ? 'board-page' : 'dashboard-page')\n return (\n <section\n data-ui={kind}\n data-variant={props.variant ?? null}\n data-layout={props.layout ?? 'flow'}\n data-pattern=\"workspace\"\n >\n {pageIdentity(kind, props)}\n {props.controls !== undefined && (\n <div data-ui={`${kind}-toolbar`}>\n <div data-ui={`${kind}-controls`}>{props.controls}</div>\n </div>\n )}\n <div data-ui={`${kind}-body`}>{props.body}</div>\n </section>\n )\n}\n\n/** One operational canvas, with flow and horizontal-canvas layout modes. */\nexport const WorkspacePage = (props: WorkspacePageProps): TemplateResult => workspacePage(props)\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { workspacePage, type WorkspacePageProps } from '../workspace-page.tsx'\n\nexport const HOOKS = [\n 'dashboard-page',\n 'dashboard-page-context',\n 'dashboard-page-header',\n 'dashboard-page-heading',\n 'dashboard-page-eyebrow',\n 'dashboard-page-title-row',\n 'dashboard-page-title',\n 'dashboard-page-subline',\n 'dashboard-page-status',\n 'dashboard-page-description',\n 'dashboard-page-actions',\n 'dashboard-page-meta',\n 'dashboard-page-body',\n] as const\n\nexport type DashboardPageProps = Omit<WorkspacePageProps, 'layout' | 'controls'>\n\n/**\n * The baseline for KPI, workflow and analytical overview screens.\n *\n * Applications own the translated copy, business actions and dashboard blocks.\n * This pattern keeps location, identity and the overview canvas in a stable,\n * compact hierarchy without pretending that the dashboard is a business record.\n *\n * @deprecated Compose the overview recipe with WorkspacePage layout=\"flow\".\n */\nexport const DashboardPage = (props: DashboardPageProps): TemplateResult =>\n workspacePage({ ...props, layout: 'flow' }, 'dashboard-page')\n", "import type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { workspacePage, type WorkspacePageProps } from '../workspace-page.tsx'\n\nexport const HOOKS = [\n 'board-page',\n 'board-page-context',\n 'board-page-header',\n 'board-page-heading',\n 'board-page-eyebrow',\n 'board-page-title-row',\n 'board-page-title',\n 'board-page-subline',\n 'board-page-status',\n 'board-page-description',\n 'board-page-actions',\n 'board-page-meta',\n 'board-page-toolbar',\n 'board-page-controls',\n 'board-page-body',\n] as const\n\nexport type BoardPageProps = Omit<WorkspacePageProps, 'layout'>\n\n/**\n * The baseline for kanban, schedule and timeline workspaces.\n *\n * A board needs more horizontal room than a collection or form, but it still\n * belongs to the same application hierarchy. The pattern keeps location,\n * identity and global controls in stable compact bands while leaving scrolling,\n * columns and interaction to the specialised board supplied by the application.\n *\n * @deprecated Compose canvas recipes with WorkspacePage layout=\"canvas\".\n */\nexport const BoardPage = (props: BoardPageProps): TemplateResult =>\n workspacePage({ ...props, layout: 'canvas' }, 'board-page')\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport type { Tone } from '../../primitives/status.tsx'\n\nexport const HOOKS = [\n 'pipeline',\n 'pipeline-step',\n 'pipeline-arrow',\n 'pipeline-step-label',\n 'pipeline-step-value',\n 'pipeline-step-node',\n] as const\n\nexport type PipelineStep = {\n id: string\n label: string\n value: string | number\n href?: string | null\n tone?: Tone\n}\n\n/**\n * A process read left to right: the stages of a thing, and how many are sitting\n * in each one.\n *\n * Not a `Progress`, which answers \"how far along is this one record\". This\n * answers \"where is the work piling up\", so every stage carries its own count\n * and none of them is a percentage of the others \u2014 a stage can hold more than\n * the one before it, and a bar that normalised them would say the opposite.\n *\n * An ordered list, because the order is the meaning. The connecting rail and the\n * arrows between stages are drawn by the stylesheet from `pipeline-step-node`\n * and `pipeline-arrow`; both are decoration for an order the markup already\n * states, which is why the arrow carries no text to announce.\n */\nexport const Pipeline = (props: { label: string; steps: readonly PipelineStep[] }): TemplateResult => (\n <ol data-ui=\"pipeline\" aria-label={props.label}>\n {each(\n props.steps,\n (step) => step.id,\n (step) => (\n <li data-ui=\"pipeline-step\" data-tone={step.tone ?? 'neutral'}>\n <span data-ui=\"pipeline-arrow\" aria-hidden=\"true\" />\n {step.href ? (\n <a data-ui=\"pipeline-step-label\" href={step.href}>\n {step.label}\n </a>\n ) : (\n <span data-ui=\"pipeline-step-label\">{step.label}</span>\n )}\n <span data-ui=\"pipeline-step-value\">{String(step.value)}</span>\n <span data-ui=\"pipeline-step-node\" aria-hidden=\"true\" />\n </li>\n ),\n )}\n </ol>\n)\n", "import { each } from '@ketvietlab/ketjs-view'\nimport type { TemplateResult } from '@ketvietlab/ketjs-view'\nimport { ActionGroup, Button, LinkButton } from '../../primitives/actions.tsx'\nimport type { ActionVariant } from '../../primitives/actions.tsx'\nimport { Field } from '../../primitives/field.tsx'\nimport type { FieldProps } from '../../primitives/field.tsx'\n\nexport const HOOKS = ['record-form', 'form-errors', 'form-grid', 'form-actions'] as const\n\nexport type RecordFormProps = {\n id?: string | null\n action: string\n method?: 'get' | 'post'\n fields: readonly FieldProps[]\n submitLabel: string\n submitVariant?: ActionVariant\n cancelHref?: string | null\n cancelLabel?: string | null\n errors?: readonly string[]\n hidden?: Record<string, string>\n}\n\nexport const RecordForm = (props: RecordFormProps): TemplateResult => (\n <form\n data-ui=\"record-form\"\n id={props.id ?? undefined}\n action={props.action}\n method={props.method ?? 'post'}\n >\n {Object.entries(props.hidden ?? {}).map(([name, value]) => (\n <input type=\"hidden\" name={name} value={value} />\n ))}\n {!!props.errors?.length && (\n <ul data-ui=\"form-errors\" role=\"alert\">\n {each(\n props.errors,\n (error, index) => `${index}:${error}`,\n (error) => (\n <li>{error}</li>\n ),\n )}\n </ul>\n )}\n <div data-ui=\"form-grid\">\n {each(\n props.fields,\n (field) => field.id,\n (field) => (\n <Field {...field} />\n ),\n )}\n </div>\n <div data-ui=\"form-actions\">\n <ActionGroup\n actions={[\n <Button label={props.submitLabel} variant={props.submitVariant ?? 'primary'} type=\"submit\" />,\n ...(props.cancelHref\n ? [\n <LinkButton\n label={props.cancelLabel ?? 'Cancel'}\n href={props.cancelHref}\n variant=\"tertiary\"\n />,\n ]\n : []),\n ]}\n />\n </div>\n </form>\n)\n", "import { HOOKS as actionHooks } from '../primitives/actions/index.tsx'\nimport { HOOKS as feedbackHooks } from '../primitives/feedback/index.tsx'\nimport { HOOKS as fieldHooks } from '../primitives/field/index.tsx'\nimport { HOOKS as statusHooks } from '../primitives/status/index.tsx'\nimport { HOOKS as navigationHooks } from '../primitives/navigation/index.tsx'\nimport { HOOKS as progressHooks } from '../primitives/progress/index.tsx'\nimport { HOOKS as menuHooks } from '../interactions/menu/index.tsx'\nimport { HOOKS as popoverHooks } from '../interactions/popover/index.tsx'\nimport { HOOKS as tooltipHooks } from '../interactions/tooltip/index.tsx'\nimport { HOOKS as dialogHooks } from '../interactions/dialog/index.tsx'\nimport { HOOKS as toastHooks } from '../interactions/toast/index.tsx'\nimport { HOOKS as spinnerHooks } from '../interactions/spinner/index.tsx'\nimport { HOOKS as skeletonHooks } from '../interactions/skeleton/index.tsx'\nimport { HOOKS as scalarFieldHooks } from '../forms/scalar-fields/index.tsx'\nimport { HOOKS as comboboxHooks } from '../forms/combobox/index.tsx'\nimport { HOOKS as dateTimeHooks } from '../forms/date-time/index.tsx'\nimport { HOOKS as uploadHooks } from '../forms/upload/index.tsx'\nimport { HOOKS as relationPickerHooks } from '../forms/relation-picker/index.tsx'\nimport { HOOKS as listControlHooks } from '../data-operations/list-controls/index.tsx'\nimport { HOOKS as inlineEditHooks } from '../data-operations/inline-edit/index.tsx'\nimport { HOOKS as resourceListHooks } from '../data-display/resource-list/index.tsx'\nimport { HOOKS as dataGridHooks } from '../data-display/data-grid/index.tsx'\nimport { HOOKS as treeHooks } from '../data-display/tree/index.tsx'\nimport { HOOKS as matrixHooks } from '../data-display/matrix/index.tsx'\nimport { HOOKS as barChartHooks } from '../data-display/bar-chart/index.tsx'\nimport { HOOKS as timeframeHooks } from '../data-operations/timeframe-filter/index.tsx'\nimport { HOOKS as recordDisplayHooks } from '../record/display/index.tsx'\nimport { HOOKS as formattedValueHooks } from '../record/formatted-values/index.tsx'\nimport { HOOKS as recordCompositionHooks } from '../record/composition/index.tsx'\nimport { HOOKS as activityHooks } from '../record/activity/index.tsx'\nimport { HOOKS as mediaHooks } from '../record/media/index.tsx'\nimport { HOOKS as layoutHooks } from '../layouts/layout/index.tsx'\nimport { HOOKS as shellHooks } from '../layouts/shell/index.tsx'\nimport { HOOKS as appNavigationHooks } from '../layouts/app-navigation/index.tsx'\nimport { HOOKS as tableHooks } from '../patterns/data-table/index.tsx'\nimport { HOOKS as listChromeHooks } from '../patterns/list-chrome/index.tsx'\nimport { HOOKS as listPageHooks } from '../patterns/list-page/index.tsx'\nimport { HOOKS as formPageHooks } from '../patterns/form-page/index.tsx'\nimport { HOOKS as recordPageHooks } from '../patterns/record-page/index.tsx'\nimport { HOOKS as dashboardPageHooks } from '../patterns/dashboard-page/index.tsx'\nimport { HOOKS as boardPageHooks } from '../patterns/board-page/index.tsx'\nimport { HOOKS as modalHooks } from '../patterns/modal-sheet/index.tsx'\nimport { HOOKS as pipelineHooks } from '../patterns/pipeline/index.tsx'\nimport { HOOKS as formHooks } from '../patterns/record-form/index.tsx'\n\nconst GROUPS = {\n actions: actionHooks,\n feedback: feedbackHooks,\n fields: fieldHooks,\n status: statusHooks,\n navigation: navigationHooks,\n progress: progressHooks,\n menu: menuHooks,\n popover: popoverHooks,\n tooltip: tooltipHooks,\n dialog: dialogHooks,\n toast: toastHooks,\n spinner: spinnerHooks,\n skeleton: skeletonHooks,\n scalarFields: scalarFieldHooks,\n combobox: comboboxHooks,\n dateTime: dateTimeHooks,\n upload: uploadHooks,\n relationPicker: relationPickerHooks,\n listControls: listControlHooks,\n inlineEdit: inlineEditHooks,\n resourceList: resourceListHooks,\n dataGrid: dataGridHooks,\n tree: treeHooks,\n matrix: matrixHooks,\n barChart: barChartHooks,\n timeframe: timeframeHooks,\n recordDisplay: recordDisplayHooks,\n formattedValues: formattedValueHooks,\n recordComposition: recordCompositionHooks,\n activity: activityHooks,\n media: mediaHooks,\n layouts: layoutHooks,\n shell: shellHooks,\n appNavigation: appNavigationHooks,\n table: tableHooks,\n listChrome: listChromeHooks,\n listPage: listPageHooks,\n formPage: formPageHooks,\n recordPage: recordPageHooks,\n dashboardPage: dashboardPageHooks,\n boardPage: boardPageHooks,\n modal: modalHooks,\n pipeline: pipelineHooks,\n form: formHooks,\n} as const\n\nexport const HOOKS: readonly string[] = [...new Set(Object.values(GROUPS).flat())].sort()\n\nexport const OWNERS: Readonly<Record<string, string[]>> = Object.freeze(\n Object.fromEntries(Object.entries(GROUPS).map(([owner, hooks]) => [owner, [...hooks]])),\n)\n", "// The record-modal contract, browser half.\n//\n// A module describes a record \u2014 how to read it, its tabs, dialogs and commands \u2014\n// and this runtime owns everything that must behave the same in every module:\n// opening from a link or deep link, history, focus, Escape, the inert background,\n// loading and failure states, submitting through `/_ket/fn`, showing a refusal on\n// the field that caused it without losing what was typed, the unsaved-input guard,\n// and telling the collection behind the modal that its records changed.\n//\n// Views are render-pure: they read the context and return design-system markup.\n// They never fetch, never touch history and never query the document.\n\nimport { signal } from '@ketvietlab/ketjs-view'\nimport type { IslandController, JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { Button, LoadingState, ModalSheet, Notice, TabbedView } from '@ketvietlab/design-system'\nimport {\n RECORD_NEW_ID,\n RECORD_PARAM,\n RECORD_TAB_PARAM,\n readRecordModalTarget,\n recordModalClosedHref,\n recordModalCreateHref,\n recordModalHost,\n recordModalHref,\n} from '../record-modal.tsx'\n\n// \u2500\u2500 Function calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type RecordIssue = {\n field: string | null\n code: string\n message: string | null\n params: Record<string, unknown>\n}\n\nexport type RecordCallResult<Value = unknown> =\n | { ok: true; value: Value }\n | { ok: false; issues: RecordIssue[]; message: string | null; status: number }\n\nconst issuesOf = (source: unknown): RecordIssue[] => {\n const holder = (source ?? {}) as { errors?: unknown; issues?: unknown }\n const list = Array.isArray(holder.errors)\n ? holder.errors\n : Array.isArray(holder.issues)\n ? holder.issues\n : []\n return list.map((raw) => {\n const item = (raw ?? {}) as Record<string, unknown>\n const path = Array.isArray(item.path) ? item.path.join('.') : null\n return {\n field: typeof item.field === 'string' ? item.field : path,\n code: String(item.code ?? 'invalid'),\n message: typeof item.message === 'string' ? item.message : null,\n params: (item.params && typeof item.params === 'object' ? item.params : {}) as Record<string, unknown>,\n }\n })\n}\n\n/**\n * Call a server function as the signed-in viewer.\n *\n * A domain refusal (`{ ok: false, errors }` inside the value) and a transport or\n * permission refusal (`ok: false` on the envelope) come back in one shape, so a\n * view never has to know which layer said no.\n */\nexport const callRecordFunction = async <Value = unknown>(\n name: string,\n input: Record<string, unknown>,\n options: { signal?: AbortSignal; idempotencyKey?: string } = {},\n): Promise<RecordCallResult<Value>> => {\n const response = await fetch(`/_ket/fn/${encodeURIComponent(name)}`, {\n method: 'POST',\n credentials: 'same-origin',\n headers: {\n 'content-type': 'application/json',\n ...(options.idempotencyKey ? { 'idempotency-key': options.idempotencyKey } : {}),\n },\n body: JSON.stringify(input),\n signal: options.signal,\n })\n const payload = (await response.json().catch(() => ({}))) as {\n ok?: boolean\n value?: unknown\n error?: { message?: string; issues?: unknown }\n message?: string\n }\n if (!response.ok || payload.ok === false)\n return {\n ok: false,\n issues: issuesOf(payload.error ?? payload),\n message: payload.error?.message ?? payload.message ?? null,\n status: response.status,\n }\n const value = payload.value as { ok?: boolean } | undefined\n if (value && typeof value === 'object' && value.ok === false)\n return { ok: false, issues: issuesOf(value), message: null, status: response.status }\n return { ok: true, value: payload.value as Value }\n}\n\n// \u2500\u2500 Definition \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n/** What every record context read must return next to its data. */\nexport type RecordContextEnvelope<Data> = {\n data: Data\n /** Translated strings this record's views use, including `recordModal.*` runtime labels. */\n messages: Record<string, string>\n}\n\nexport type RecordModalContext<Data> = {\n kind: string\n id: string\n /** The modal was opened by a create action (`record=<kind>:new`); there is no record yet. */\n creating: boolean\n tab: string\n data: Data\n /** Translate a key the context shipped, interpolating `{name}` params. */\n t: (key: string, params?: Record<string, unknown>) => string\n /** The refusal for one field of the last submit, translated. */\n fieldError: (name: string) => string | null\n /** What was typed into a field before a refused submit, or the fallback. */\n draft: (name: string, fallback?: string) => string\n /** Whether a checkbox/radio value was selected before the view re-rendered. */\n draftChecked: (name: string, value?: string, fallback?: boolean) => boolean\n /**\n * A submit has been in flight long enough to be worth saying so. A command the\n * server answers at once never sets it, so a button bound to it does not flash\n * its spinner; the runtime stops a second submit either way.\n */\n busy: boolean\n dialog: { name: string; params: Record<string, string> } | null\n href: (tab: string) => string\n /** View-local state, set by `data-record-state` controls (click, or change for inputs). */\n state: (key: string, fallback?: string) => string\n}\n\n/** Attachments a command uploaded before its function ran, by form field. */\nexport type RecordUploads = Record<string, { id: string; name: string | null }>\n\n/** One extra call of a multi-step command \u2014 see `RecordModalCommand.also`. */\nexport type RecordModalCommandStep<Data> = {\n fn: string\n input: (\n form: FormData,\n context: RecordModalContext<Data>,\n uploads: RecordUploads,\n ) => Record<string, unknown>\n /** Skip this step \u2014 a module not installed, a permission the viewer lacks. */\n when?: (context: RecordModalContext<Data>) => boolean\n}\n\nexport type RecordModalCommand<Data> = {\n fn: string\n /** Map the submitted form to the function's input. */\n input: (\n form: FormData,\n context: RecordModalContext<Data>,\n uploads: RecordUploads,\n ) => Record<string, unknown>\n /**\n * Further calls made right after `fn`/`input` succeeds, in order, as part of\n * the same user-facing action \u2014 one busy state, one success notice, one set\n * of field refusals \u2014 stopping at the first one that fails. For an edit that\n * spans functions in different modules that cannot call one another (a\n * single `product.saveTemplate` cannot also configure stock tracking or\n * tax), this is what lets one \"Save\" button still read as one save.\n */\n also?: readonly RecordModalCommandStep<Data>[]\n /**\n * File fields stored through `/files` before the function runs, each mapped to\n * the attachment's metadata (`resModel`, `resId`, `resField`, `public`). The\n * stored attachment ids reach `input` as its third argument.\n */\n upload?: Record<string, (form: FormData, context: RecordModalContext<Data>) => Record<string, string>>\n /** A destructive command asks first; declining leaves the record untouched. */\n confirm?: (context: RecordModalContext<Data>) => string | null\n /**\n * What happens after success. Defaults to `close`. `reload` reads the record\n * again behind a loading state and closes a dialog; `refresh` reads it again in\n * place, keeping the tab, the open dialog and any islands mounted in them.\n * `open` is for a create command: the modal switches to the record it created\n * (the id from `created`, else the function value's `id`) on the tab named by\n * `openTab`, replacing the `:new` history entry.\n */\n after?: 'close' | 'reload' | 'refresh' | 'stay' | 'open' | { tab: string } | { dialog: string | null }\n /** The id of the record a create command made, read from the function's value. */\n created?: (value: unknown) => string | null\n /** Tab to open on the created record when `after` is `open`. */\n openTab?: string\n}\n\nexport type RecordModalTab<Data> = {\n id: string\n label: (context: RecordModalContext<Data>) => string\n visible?: (context: RecordModalContext<Data>) => boolean\n view: (context: RecordModalContext<Data>) => JSXChild\n}\n\nexport type RecordModalDialog<Data> = {\n title: (context: RecordModalContext<Data>) => string\n size?: 'default' | 'large'\n view: (context: RecordModalContext<Data>) => JSXChild\n}\n\nexport type RecordModalDefinition<Data> = {\n kind: string\n size?: 'default' | 'large'\n /**\n * Caps a tabbed record's fixed height (a CSS length or `min()`/`calc()`\n * expression) below the viewport-filling default, for a record whose own\n * content is shorter than that \u2014 see `ModalSheet.fixedHeight`. Ignored for a\n * record with one tab or none, since those never turn on `height: 'fixed'`.\n */\n fixedHeight?: string\n /**\n * A permission-checked read returning `{ data, messages }`. For a create action\n * the default input is `{}` (no id): the read returns the empty record's defaults,\n * the choices its form needs and the viewer's permissions.\n */\n context: { fn: string; input?: (id: string, creating: boolean) => Record<string, unknown> }\n title: (context: RecordModalContext<Data>) => string\n description?: (context: RecordModalContext<Data>) => string | null\n /**\n * What state this record is in, beside the modal's title: a badge, not a\n * strip. It stays put while the body scrolls, and a reader looking at the\n * title learns the state without reading down into the form.\n */\n status?: (context: RecordModalContext<Data>) => JSXChild\n /** A strip above the body \u2014 a customer, a summary \u2014 for what a badge cannot hold. */\n header?: (context: RecordModalContext<Data>) => JSXChild\n /**\n * A footer strip below the body, outside the scrolling area and the same on\n * every tab \u2014 the natural home for a record's primary actions (save, close,\n * more) so they stay reachable without hunting through the tab that happens\n * to hold the save button. Return `undefined` (not `''`) to render no footer\n * at all, e.g. while creating, when the create form owns its own submit.\n */\n actions?: (context: RecordModalContext<Data>) => JSXChild | undefined\n tabs?: readonly RecordModalTab<Data>[]\n /**\n * Tabs other modules add to this record, read from its context: they follow the\n * declared tabs, in composition order, and render through the same TabbedView.\n * A module that owns the record cannot know them when it is built.\n */\n extensionTabs?: (context: RecordModalContext<Data>) => readonly RecordModalTab<Data>[]\n /** The body of a record without tabs. */\n body?: (context: RecordModalContext<Data>) => JSXChild\n dialogs?: Record<string, RecordModalDialog<Data>>\n commands?: Record<string, RecordModalCommand<Data>>\n /**\n * Runtime labels (`recordModal.*`) in the page's language, available before the\n * record's context has loaded. Without them the loading state has only the\n * built-in English defaults, because `messages` arrive with the context.\n */\n labels?: Record<string, string> | (() => Record<string, string>)\n /**\n * Keep read contexts in memory so reopening a record shows at once and is read\n * again quietly (stale-while-revalidate). On by default; set `false` for a kind\n * whose context must never be shown before a fresh read.\n */\n cache?: boolean\n}\n\n/** How many record contexts one island keeps for instant reopening. */\nexport const RECORD_MODAL_CACHE_SIZE = 30\n\n/**\n * English defaults for every label the runtime itself shows. A module passes its\n * own language through `labels`; these only guarantee a reader never sees a key.\n */\nexport const RECORD_MODAL_LABELS: Readonly<Record<string, string>> = Object.freeze({\n 'recordModal.close': 'Close',\n 'recordModal.loading': 'Loading\u2026',\n 'recordModal.loadFailed': 'The record could not be read.',\n 'recordModal.notFound': 'The record is gone or you cannot see it.',\n 'recordModal.retry': 'Retry',\n 'recordModal.errorTitle': 'Not saved',\n 'recordModal.saveFailed': 'That did not work. Try again.',\n 'recordModal.savedTitle': 'Saved',\n 'recordModal.saved': 'The change is in.',\n 'recordModal.unsaved': 'Discard what you typed?',\n 'recordModal.uploadFailed': 'The file could not be uploaded. Try again.',\n})\n\n/**\n * The text for a key: the loaded context's messages, then the messages of the\n * last record this modal opened, then the module's labels, then the defaults.\n * Only an unknown key falls through to itself.\n */\nexport const resolveRecordModalLabel = (\n key: string,\n sources: {\n messages?: Record<string, string> | null\n previous?: Record<string, string> | null\n labels?: Record<string, string> | null\n },\n): string =>\n sources.messages?.[key] ??\n sources.previous?.[key] ??\n sources.labels?.[key] ??\n RECORD_MODAL_LABELS[key] ??\n key\n\n/** Field a form (or its submitter) uses to name the command it runs. */\nexport const RECORD_COMMAND_FIELD = '__command'\n/** Field a link or button uses to open a dialog of the same record. */\nexport const RECORD_DIALOG_ATTRIBUTE = 'data-record-dialog'\n\n// \u2500\u2500 Runtime helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst interpolate = (text: string, params: Record<string, unknown> = {}): string =>\n text.replace(/\\{([a-zA-Z0-9_]+)\\}/gu, (whole, name: string) =>\n name in params ? String(params[name] ?? '') : whole,\n )\n\nconst focusable = [\n 'a[href]',\n 'button:not(:disabled)',\n 'input:not(:disabled):not([type=\"hidden\"])',\n 'select:not(:disabled)',\n 'textarea:not(:disabled)',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(', ')\n\nconst focusablesIn = (element: HTMLElement): HTMLElement[] =>\n [...element.querySelectorAll<HTMLElement>(focusable)].filter(\n (item) =>\n !item.hidden &&\n item.getAttribute('aria-hidden') !== 'true' &&\n !item.matches('[data-ui=\"modal-backdrop\"]'),\n )\n\n/** Whether anything in a layer was typed into since it rendered. Same rule as route modals. */\nexport const recordLayerHasDraft = (layer: HTMLElement): boolean => {\n for (const field of layer.querySelectorAll<HTMLInputElement>('input:not([type=\"hidden\"])')) {\n if (field.disabled) continue\n if (field.type === 'checkbox' || field.type === 'radio') {\n if (field.checked !== field.defaultChecked) return true\n } else if (field.value !== field.defaultValue) return true\n }\n for (const area of layer.querySelectorAll<HTMLTextAreaElement>('textarea'))\n if (!area.disabled && area.value !== area.defaultValue) return true\n for (const select of layer.querySelectorAll<HTMLSelectElement>('select')) {\n if (select.disabled) continue\n for (const option of select.options) if (option.selected !== option.defaultSelected) return true\n }\n return false\n}\n\n/** Make everything outside `keep` inert, walking up to the body; returns the undo. */\nconst inertOutside = (keep: HTMLElement): (() => void) => {\n const changed: Array<{ element: HTMLElement; inert: boolean }> = []\n let node: HTMLElement | null = keep\n while (node && node !== document.body) {\n const parent: HTMLElement | null = node.parentElement\n if (!parent) break\n for (const sibling of parent.children)\n if (sibling instanceof HTMLElement && sibling !== node && !sibling.matches('script, style, template')) {\n changed.push({ element: sibling, inert: sibling.inert })\n sibling.inert = true\n }\n node = parent\n }\n return () => {\n for (const { element, inert } of changed) element.inert = inert\n }\n}\n\nconst uuid = (): string =>\n typeof crypto !== 'undefined' && 'randomUUID' in crypto\n ? crypto.randomUUID()\n : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n\ntype Status = 'idle' | 'loading' | 'ready' | 'error'\n\ntype DraftState = {\n values: Record<string, string>\n checks: Record<string, boolean>\n}\n\ntype DraftScope = 'record' | 'dialog'\n\nconst emptyDraftState = (): DraftState => ({ values: {}, checks: {} })\nconst draftCheckKey = (name: string, value: string): string => `${name}\\u0000${value}`\n\nconst after_ = <Data,>(command: RecordModalCommand<Data>) => command.after ?? 'close'\n\n/**\n * How long a command may run before its button says so. Below this the answer\n * arrives while the reader is still lifting their finger, and a spinner shown\n * and withdrawn inside that window is noise; above it, silence would read as a\n * click that did nothing.\n */\nexport const BUSY_AFTER_MS = 400\n\n/**\n * A flag that turns on late and off at once. Work that finishes inside the\n * window never raises it, so a progress indicator bound to it appears only when\n * there is progress to report, and never flashes on its way back out.\n */\nexport const delayedFlag = (\n show: (value: boolean) => void,\n after: number = BUSY_AFTER_MS,\n): { set: (running: boolean) => void; stop: () => void } => {\n let timer: ReturnType<typeof setTimeout> | undefined\n const stop = (): void => {\n clearTimeout(timer)\n timer = undefined\n }\n return {\n set: (running) => {\n stop()\n if (!running) {\n show(false)\n return\n }\n timer = setTimeout(() => show(true), after)\n },\n stop,\n }\n}\n\n/** Controls inside a row do their own thing; the row's destination is for the rest of it. */\nconst rowControl = 'a, button, input, select, textarea, label, summary, details, [data-ui=\"select-cell\"]'\n\n/**\n * Where a click is asking to go: the link it landed on, or the row it landed\n * in. A table row carries its destination on the row itself (`rowLink: false`),\n * so the whole row is one target rather than a link around the first cell. The\n * shell would navigate such a row through the navigation layer, which fetches\n * the collection again and leaves the modal unopened, so this reads it first.\n */\nexport const openerHref = (element: Element | null): string | null => {\n const anchor = element?.closest<HTMLAnchorElement>('a[href]')\n if (anchor) return anchor.target && anchor.target !== '_self' ? null : anchor.href\n if (element?.closest(rowControl)) return null\n return element?.closest<HTMLElement>('[data-row-href]')?.getAttribute('data-row-href') ?? null\n}\n\n/**\n * Create the island controller for one record kind.\n *\n * The returned factory is what a module exports as its island client:\n * `export const followupModal = createRecordModal(definition)`.\n */\nexport const createRecordModal =\n <Data,>(definition: RecordModalDefinition<Data>) =>\n (): IslandController => {\n const open = signal<{ id: string; tab: string } | null>(null)\n const status = signal<Status>('idle')\n const envelope = signal<RecordContextEnvelope<Data> | null>(null)\n const failure = signal<string | null>(null)\n const issues = signal<RecordIssue[]>([])\n const recordDrafts = signal<DraftState>(emptyDraftState())\n const dialogDrafts = signal<DraftState>(emptyDraftState())\n // `running` is the guard \u2014 one command at a time \u2014 and `busy` is what the\n // views show. They are not the same thing: a save the server answers in\n // twenty milliseconds would otherwise flash the button through its spinner\n // and back, which reads as a glitch rather than as progress. The spinner\n // waits; the guard does not.\n const running = signal(false)\n const busy = signal(false)\n const showBusy = delayedFlag((value) => busy.set(value))\n // A command that succeeded while the modal stayed open. Saving is usually\n // answered before the button could say anything, and a record that looks\n // the same afterwards leaves the reader unsure anything happened, so the\n // form says so where it would have said the opposite.\n const saved = signal(false)\n const setRunning = (value: boolean): void => {\n running.set(value)\n showBusy.set(value)\n if (value) saved.set(false)\n }\n const dialog = signal<{ name: string; params: Record<string, string> } | null>(null)\n const version = signal(0)\n const viewState = signal<Record<string, string>>({})\n\n // Contexts this island has read, newest last: reopening a record (or the create\n // form) renders immediately and revalidates in place. Bounded and page-scoped;\n // a successful command or `ket:records-changed` for this kind drops the entries.\n const cache = new Map<string, RecordContextEnvelope<Data>>()\n const remember = (id: string, value: RecordContextEnvelope<Data>): void => {\n cache.delete(id)\n cache.set(id, value)\n while (cache.size > RECORD_MODAL_CACHE_SIZE) cache.delete(cache.keys().next().value as string)\n }\n\n let root: HTMLElement | null = null\n let request: AbortController | null = null\n let pushed = false\n let returnFocus: HTMLElement | null = null\n let dialogReturnFocus: HTMLElement | null = null\n let releaseInert: (() => void) | null = null\n\n // Messages of the last record this modal loaded: opening the next record clears\n // the envelope, and its loading state still needs words rather than keys.\n let previousMessages: Record<string, string> | null = null\n const labels = (): Record<string, string> =>\n typeof definition.labels === 'function' ? definition.labels() : (definition.labels ?? {})\n const t = (key: string, params?: Record<string, unknown>): string =>\n interpolate(\n resolveRecordModalLabel(key, {\n messages: envelope()?.messages,\n previous: previousMessages,\n labels: labels(),\n }),\n params,\n )\n const visibleTabs = (context: RecordModalContext<Data>) =>\n [...(definition.tabs ?? []), ...(definition.extensionTabs?.(context) ?? [])].filter(\n (tab) => tab.visible?.(context) ?? true,\n )\n\n const contextFor = (\n current: { id: string; tab: string },\n data: Data,\n scope: DraftScope = 'record',\n ): RecordModalContext<Data> => {\n const draftState = (): DraftState => (scope === 'dialog' ? dialogDrafts() : recordDrafts())\n const base: RecordModalContext<Data> = {\n kind: definition.kind,\n id: current.id,\n creating: current.id === RECORD_NEW_ID,\n tab: current.tab,\n data,\n t,\n fieldError: (name) => {\n const hit = issues().find((issue) => issue.field === name)\n return hit ? (hit.message ?? t(hit.code, hit.params)) : null\n },\n draft: (name, fallback = '') => draftState().values[name] ?? fallback,\n draftChecked: (name, value = '1', fallback = false) =>\n draftState().checks[draftCheckKey(name, value)] ?? fallback,\n busy: busy(),\n dialog: dialog(),\n href: (tab) => recordModalHref(location.href, { kind: definition.kind, id: current.id, tab }),\n state: (key, fallback = '') => viewState()[key] ?? fallback,\n }\n const tabs = visibleTabs(base)\n if (tabs.length && !tabs.some((tab) => tab.id === current.tab)) base.tab = tabs[0]!.id\n return base\n }\n\n const load = async (id: string, quiet = false): Promise<void> => {\n request?.abort()\n const controller = new AbortController()\n request = controller\n // A record opened before shows at once from the in-memory cache and is read\n // again quietly behind it; the reader never waits on a frame they have seen.\n const cached = definition.cache === false ? undefined : cache.get(id)\n if (cached) {\n previousMessages = cached.messages ?? previousMessages\n envelope.set(cached)\n status.set('ready')\n quiet = true\n }\n if (!quiet) status.set('loading')\n failure.set(null)\n try {\n const creating = id === RECORD_NEW_ID\n const input = definition.context.input\n ? definition.context.input(id, creating)\n : creating\n ? {}\n : { id }\n const result = await callRecordFunction<RecordContextEnvelope<Data> | null>(\n definition.context.fn,\n input,\n {\n signal: controller.signal,\n },\n )\n if (controller.signal.aborted) return\n if (!result.ok || !result.value) {\n failure.set(result.ok ? 'recordModal.notFound' : (result.message ?? 'recordModal.loadFailed'))\n status.set('error')\n return\n }\n previousMessages = result.value.messages ?? previousMessages\n if (definition.cache !== false) remember(id, result.value)\n envelope.set(result.value)\n status.set('ready')\n } catch (caught) {\n if (controller.signal.aborted || (caught as Error)?.name === 'AbortError') return\n failure.set('recordModal.loadFailed')\n status.set('error')\n }\n }\n\n const layers = (): HTMLElement[] =>\n root ? [...root.querySelectorAll<HTMLElement>('[data-ui=\"modal-layer\"][data-client-modal=\"true\"]')] : []\n const recordLayer = (): HTMLElement | null => layers()[0] ?? null\n const topLayer = (): HTMLElement | null => layers().at(-1) ?? null\n\n const keepDrafts = (current: HTMLElement | null, scope: DraftScope): void => {\n if (!current) return\n const previous = scope === 'dialog' ? dialogDrafts() : recordDrafts()\n const kept: DraftState = {\n values: { ...previous.values },\n checks: { ...previous.checks },\n }\n for (const control of current.querySelectorAll<\n HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement\n >('input[name], select[name], textarea[name]')) {\n if (control.disabled || control.name === RECORD_COMMAND_FIELD) continue\n if (control instanceof HTMLInputElement) {\n if (control.type === 'file' || control.type === 'hidden') continue\n if (control.type === 'checkbox' || control.type === 'radio') {\n kept.checks[draftCheckKey(control.name, control.value)] = control.checked\n if (control.type === 'checkbox') kept.values[control.name] = control.checked ? control.value : ''\n else if (control.checked) kept.values[control.name] = control.value\n continue\n }\n }\n kept.values[control.name] = control.value\n }\n if (scope === 'dialog') dialogDrafts.set(kept)\n else recordDrafts.set(kept)\n }\n\n const keepAllDrafts = (): void => {\n const currentLayers = layers()\n keepDrafts(currentLayers[0] ?? null, 'record')\n if (currentLayers.length > 1) keepDrafts(currentLayers.at(-1) ?? null, 'dialog')\n }\n\n const mayDiscard = (current: HTMLElement | null): boolean => {\n if (!current || !recordLayerHasDraft(current)) return true\n return globalThis.confirm(t('recordModal.unsaved'))\n }\n\n const afterRender = (preferred?: () => HTMLElement | null): void => {\n requestAnimationFrame(() => {\n const currentLayers = layers()\n const record = currentLayers[0]\n const top = currentLayers.at(-1)\n if (!top) return\n if (record) record.inert = currentLayers.length > 1\n const target = preferred?.()\n if (target?.isConnected && top.contains(target)) {\n target.focus()\n return\n }\n if (top.contains(document.activeElement)) return\n const sheet = top.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')\n const first = focusablesIn(top).find((item) => !item.matches('[data-ui=\"modal-close\"]'))\n ;(first ?? sheet)?.focus()\n })\n }\n\n const show = (\n id: string,\n tab: string | null,\n how: 'push' | 'replace' | 'none',\n preferredFocus?: () => HTMLElement | null,\n ): void => {\n const current = open()\n const sameRecord = current?.id === id\n const nextTab = tab ?? (sameRecord ? current.tab : '')\n if (!sameRecord) {\n returnFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null\n issues.set([])\n saved.set(false)\n recordDrafts.set(emptyDraftState())\n dialogDrafts.set(emptyDraftState())\n viewState.set({})\n dialog.set(null)\n dialogReturnFocus = null\n envelope.set(null)\n void load(id)\n }\n open.set({ id, tab: nextTab })\n const href = recordModalHref(location.href, { kind: definition.kind, id, tab: nextTab || null })\n if (how === 'push') {\n // The shell's own navigation snapshots scroll onto the entry it leaves\n // before pushing (`saveScroll` in packages/ketjs/src/server/http.ts) so\n // going back restores it; this push must do the same, or closing the\n // modal later restores no scroll and the page it sits over jumps to top.\n history.replaceState(\n { ...(history.state ?? {}), __ketScroll: [window.scrollX, window.scrollY] },\n '',\n location.href,\n )\n history.pushState({ ...(history.state ?? {}), __ketRecordModal: definition.kind }, '', href)\n pushed = true\n } else if (how === 'replace') history.replaceState(history.state ?? {}, '', href)\n if (root && !releaseInert) releaseInert = inertOutside(root)\n afterRender(preferredFocus)\n }\n\n const hide = (how: 'history' | 'replace' | 'none'): void => {\n request?.abort()\n open.set(null)\n dialog.set(null)\n issues.set([])\n saved.set(false)\n recordDrafts.set(emptyDraftState())\n dialogDrafts.set(emptyDraftState())\n viewState.set({})\n status.set('idle')\n envelope.set(null)\n releaseInert?.()\n releaseInert = null\n if (how === 'history' && pushed) {\n pushed = false\n history.back()\n } else if (how !== 'none') {\n pushed = false\n history.replaceState(history.state ?? {}, '', recordModalClosedHref(location.href))\n }\n const target = returnFocus\n returnFocus = null\n dialogReturnFocus = null\n if (target?.isConnected) requestAnimationFrame(() => target.focus())\n }\n\n const close = (): void => {\n if (dialog()) {\n if (!mayDiscard(topLayer())) return\n const target = dialogReturnFocus\n dialogReturnFocus = null\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n issues.set([])\n afterRender(() => (target?.isConnected ? target : null))\n return\n }\n if (!mayDiscard(recordLayer())) return\n hide('history')\n }\n\n const run = async (name: string, form: HTMLFormElement, submitter: HTMLElement | null): Promise<void> => {\n const command = definition.commands?.[name]\n const current = open()\n const data = envelope()?.data\n if (!command || !current || data === undefined || running()) return\n const formData = new FormData(form, submitter instanceof HTMLButtonElement ? submitter : null)\n const currentLayer = form.closest<HTMLElement>('[data-ui=\"modal-layer\"][data-client-modal=\"true\"]')\n const scope: DraftScope = dialog() && currentLayer === topLayer() ? 'dialog' : 'record'\n keepDrafts(currentLayer, scope)\n const context = contextFor(current, data, scope)\n if (command.confirm) {\n const message = command.confirm(context)\n if (message && !globalThis.confirm(message)) return\n }\n setRunning(true)\n issues.set([])\n try {\n const uploads: RecordUploads = {}\n for (const [field, target] of Object.entries(command.upload ?? {})) {\n const file = formData.get(field)\n if (!(file instanceof File) || file.size === 0) continue\n const body = new FormData()\n for (const [key, value] of Object.entries(target(formData, context))) body.append(key, value)\n body.append('file', file, file.name)\n const response = await fetch('/files', { method: 'POST', credentials: 'same-origin', body })\n const stored = (await response.json().catch(() => null)) as {\n id?: unknown\n name?: unknown\n message?: unknown\n } | null\n if (!response.ok || !stored?.id) {\n issues.set([\n {\n field: null,\n code: 'recordModal.uploadFailed',\n message: typeof stored?.message === 'string' ? stored.message : null,\n params: {},\n },\n ])\n return\n }\n uploads[field] = {\n id: String(stored.id),\n name: typeof stored.name === 'string' ? stored.name : null,\n }\n }\n const invoke = (fn: string, input: Record<string, unknown>) =>\n callRecordFunction(fn, input, { idempotencyKey: uuid() })\n let result = await invoke(command.fn, command.input(formData, context, uploads))\n // Further calls run only once the first succeeds, and stop at the first\n // one that does not \u2014 a later step's success never papers over an\n // earlier failure.\n for (const step of result.ok ? (command.also ?? []) : []) {\n if (step.when && !step.when(context)) continue\n result = await invoke(step.fn, step.input(formData, context, uploads))\n if (!result.ok) break\n }\n if (!result.ok) {\n // The layer was snapshotted before the busy state rendered, including unchecked controls.\n issues.set(\n result.issues.length\n ? result.issues\n : [\n {\n field: null,\n code: result.message ?? 'recordModal.saveFailed',\n message: result.message,\n params: {},\n },\n ],\n )\n return\n }\n if (scope === 'dialog') dialogDrafts.set(emptyDraftState())\n else recordDrafts.set(emptyDraftState())\n form.reset()\n const value = result.value as { id?: unknown } | null | undefined\n const createdId =\n after_(command) === 'open'\n ? (command.created?.(result.value) ??\n (value && typeof value === 'object' && value.id != null ? String(value.id) : null))\n : null\n // What the command changed is no longer what the cache holds.\n cache.delete(current.id)\n if (createdId) cache.delete(createdId)\n const announce = (): void => {\n document.dispatchEvent(\n new CustomEvent('ket:records-changed', {\n detail: { kind: definition.kind, ids: [createdId ?? current.id] },\n }),\n )\n }\n const after = after_(command)\n // A modal that closes says so by closing; one that stays owes an answer.\n if (after !== 'close') saved.set(true)\n if (after === 'open') {\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n // Replace `:new` in the address bar before the collection refreshes: the\n // shell re-fetches `location.href`, which must already name the new record.\n if (createdId) show(createdId, command.openTab ?? null, 'replace')\n else hide('history')\n announce()\n return\n }\n announce()\n if (after === 'close') hide('history')\n else if (after === 'reload') {\n const target = dialogReturnFocus\n dialogReturnFocus = null\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n await load(current.id)\n afterRender(() => (target?.isConnected ? target : null))\n } else if (after === 'refresh') await load(current.id, true)\n else if (after === 'stay') version.set(version() + 1)\n else if ('tab' in after) {\n dialogReturnFocus = null\n dialog.set(null)\n dialogDrafts.set(emptyDraftState())\n show(current.id, after.tab, 'replace')\n await load(current.id)\n } else {\n const target = after.dialog ? null : dialogReturnFocus\n dialogReturnFocus = after.dialog && submitter instanceof HTMLElement ? submitter : null\n dialog.set(after.dialog ? { name: after.dialog, params: {} } : null)\n dialogDrafts.set(emptyDraftState())\n await load(current.id, true)\n afterRender(() => (target?.isConnected ? target : null))\n }\n } catch {\n issues.set([{ field: null, code: 'recordModal.saveFailed', message: null, params: {} }])\n } finally {\n setRunning(false)\n }\n }\n\n const formIssues = (context: RecordModalContext<Data>): JSXChild => {\n const general = issues().filter((issue) => !issue.field)\n const fieldless = issues().filter(\n (issue) => issue.field && !root?.querySelector(`[name=\"${CSS.escape(issue.field)}\"]`),\n )\n const all = [...general, ...fieldless]\n if (!all.length)\n return saved()\n ? Notice({\n title: context.t('recordModal.savedTitle'),\n message: context.t('recordModal.saved'),\n tone: 'positive',\n })\n : ''\n return Notice({\n title: context.t('recordModal.errorTitle'),\n message: all.map((issue) => issue.message ?? context.t(issue.code, issue.params)).join(' \u00B7 '),\n tone: 'danger',\n })\n }\n\n const recordBody = (): TemplateResult | string => {\n const current = open()\n if (!current) return ''\n if (status() === 'error')\n return (\n <>\n {Notice({\n title: t('recordModal.errorTitle'),\n message: t(failure() ?? 'recordModal.loadFailed'),\n tone: 'danger',\n })}\n {Button({ label: t('recordModal.retry'), variant: 'secondary', name: 'intent', value: 'retry' })}\n </>\n )\n const data = envelope()?.data\n if (status() !== 'ready' || data === undefined) return LoadingState({ label: t('recordModal.loading') })\n const context = contextFor(current, data)\n const tabs = visibleTabs(context)\n const active = tabs.find((tab) => tab.id === context.tab)\n const header = definition.header?.(context) ?? ''\n const issueNotice = formIssues(context)\n const body = active ? active.view(context) : (definition.body?.(context) ?? '')\n if (tabs.length)\n return TabbedView({\n id: `record-view-${definition.kind.replaceAll('.', '-')}`,\n label: definition.title(context),\n items: tabs.map((tab) => ({\n id: tab.id,\n label: tab.label(context),\n href: context.href(tab.id),\n active: tab.id === context.tab,\n })),\n context:\n header !== '' || issueNotice !== '' ? (\n <>\n {header}\n {issueNotice}\n </>\n ) : undefined,\n body,\n })\n return (\n <>\n {header}\n {issueNotice}\n {body}\n </>\n )\n }\n\n const dialogLayer = (): TemplateResult | string => {\n const current = open()\n const opened = dialog()\n const data = envelope()?.data\n if (!current || !opened || data === undefined) return ''\n const spec = definition.dialogs?.[opened.name]\n if (!spec) return ''\n const context = contextFor(current, data, 'dialog')\n return ModalSheet({\n id: `record-dialog-${definition.kind.replaceAll('.', '-')}-${opened.name}`,\n mode: 'client',\n presentation: 'dialog',\n size: spec.size ?? 'default',\n title: spec.title(context),\n closeLabel: t('recordModal.close'),\n body: (\n <>\n {formIssues(context)}\n {spec.view(context)}\n </>\n ),\n })\n }\n\n return {\n view: () => {\n version()\n const current = open()\n if (!current) return recordModalHost(definition.kind)\n const data = envelope()?.data\n const context = data === undefined ? null : contextFor(current, data)\n return (\n <>\n {recordModalHost(definition.kind)}\n {ModalSheet({\n id: `record-modal-${definition.kind.replaceAll('.', '-')}`,\n mode: 'client',\n presentation: 'dialog',\n size: definition.size ?? 'default',\n // Tabs have different heights; a fixed dialog does not jump when the reader switches\n // tabs, nor when the loading state gives way to the record.\n // A definition that takes extension tabs may gain one at any time, so it\n // keeps the fixed height its declared tabs would otherwise earn.\n height:\n (definition.tabs?.length ?? 0) + (definition.extensionTabs ? 1 : 0) > 1 ? 'fixed' : 'content',\n fixedHeight: definition.fixedHeight,\n title: context ? definition.title(context) : t('recordModal.loading'),\n description: context ? (definition.description?.(context) ?? null) : null,\n status: context ? definition.status?.(context) : undefined,\n actions: context ? definition.actions?.(context) : undefined,\n closeLabel: t('recordModal.close'),\n body: recordBody(),\n })}\n {dialogLayer()}\n </>\n )\n },\n mount: ({ root: host, lifetime }) => {\n root = host as unknown as HTMLElement\n\n // Links anywhere on the page that name this kind open it; a link naming the\n // record already open switches its tab. Runs before the navigation layer.\n document.addEventListener(\n 'click',\n (event) => {\n if (\n event.defaultPrevented ||\n event.button !== 0 ||\n event.metaKey ||\n event.ctrlKey ||\n event.shiftKey ||\n event.altKey\n )\n return\n const element = event.target instanceof Element ? event.target : null\n const inModal = element?.closest('[data-ui=\"modal-layer\"][data-client-modal=\"true\"]')\n if (inModal && root?.contains(inModal)) {\n // `data-ui=\"modal-close\"` is the chrome's own \u00D7 control; `data-record-close`\n // marks a module's own labeled close button placed elsewhere in the body\n // (the two can't share one attribute \u2014 a labeled button needs `data-ui=\"action\"`\n // for its styling, not the icon-only close control's).\n if (\n element?.closest('[data-ui=\"modal-close\"], [data-ui=\"modal-backdrop\"], [data-record-close]')\n ) {\n event.preventDefault()\n close()\n return\n }\n const stateControl = element?.closest<HTMLElement>(\n 'button[data-record-state], a[data-record-state]',\n )\n if (stateControl) {\n event.preventDefault()\n keepAllDrafts()\n viewState.set({\n ...viewState(),\n [stateControl.getAttribute('data-record-state') ?? '']:\n stateControl.getAttribute('data-record-value') ?? '',\n })\n return\n }\n const opener = element?.closest<HTMLElement>(`[${RECORD_DIALOG_ATTRIBUTE}]`)\n if (opener) {\n event.preventDefault()\n const focused = document.activeElement\n dialogReturnFocus =\n focused instanceof HTMLElement && opener.contains(focused)\n ? focused\n : opener.querySelector<HTMLElement>(focusable)\n dialogDrafts.set(emptyDraftState())\n const params: Record<string, string> = {}\n for (const [key, value] of Object.entries(opener.dataset))\n if (key.startsWith('recordParam') && value !== undefined)\n params[key.slice('recordParam'.length).replace(/^./u, (c) => c.toLowerCase())] = value\n issues.set([])\n dialog.set({ name: opener.getAttribute(RECORD_DIALOG_ATTRIBUTE) ?? '', params })\n afterRender()\n return\n }\n if (element?.closest('button[name=\"intent\"][value=\"retry\"]')) {\n event.preventDefault()\n const current = open()\n if (current) void load(current.id)\n return\n }\n }\n const href = openerHref(element)\n if (!href) return\n const url = new URL(href, location.href)\n if (url.origin !== location.origin || url.pathname !== location.pathname) return\n const target = readRecordModalTarget(url)\n if (!target || target.kind !== definition.kind) return\n event.preventDefault()\n const current = open()\n if (current?.id === target.id) {\n if ((target.tab ?? '') === current.tab) return\n // Switching tab is not discarding: what was typed rides along as drafts,\n // which the views read back, so no prompt stands between two tabs.\n keepDrafts(recordLayer(), 'record')\n issues.set([])\n show(\n target.id,\n target.tab ?? null,\n 'replace',\n () =>\n recordLayer()?.querySelector<HTMLElement>('[data-ui=\"tab\"][data-active=\"true\"]') ?? null,\n )\n return\n }\n show(target.id, target.tab ?? null, 'push')\n },\n // Capture: the navigation layer listens on the document too, and was\n // installed first, so a bubbling listener would see the page fetched.\n { signal: lifetime, capture: true },\n )\n\n // A row is reached by keyboard, not by pointer alone: Enter and Space on\n // the focused row open what clicking it opens.\n document.addEventListener(\n 'keydown',\n (event) => {\n if (event.defaultPrevented || (event.key !== 'Enter' && event.key !== ' ')) return\n const element = event.target instanceof Element ? event.target : null\n if (!element?.matches('[data-row-href][tabindex=\"0\"]')) return\n const href = element.getAttribute('data-row-href')\n const url = href ? new URL(href, location.href) : null\n if (!url || url.origin !== location.origin || url.pathname !== location.pathname) return\n const target = readRecordModalTarget(url)\n if (!target || target.kind !== definition.kind) return\n event.preventDefault()\n show(target.id, target.tab ?? null, 'push')\n },\n { signal: lifetime, capture: true },\n )\n\n document.addEventListener(\n 'submit',\n (event) => {\n const form = event.target instanceof HTMLFormElement ? event.target : null\n if (!form || !root?.contains(form)) return\n event.preventDefault()\n const submitter = (event as SubmitEvent).submitter ?? null\n const name =\n (submitter instanceof HTMLButtonElement && submitter.name === RECORD_COMMAND_FIELD\n ? submitter.value\n : null) ?? String(new FormData(form).get(RECORD_COMMAND_FIELD) ?? '')\n void run(name, form, submitter)\n },\n { signal: lifetime },\n )\n\n document.addEventListener(\n 'keydown',\n (event) => {\n if (!open()) return\n const layers = root?.querySelectorAll<HTMLElement>(\n '[data-ui=\"modal-layer\"][data-client-modal=\"true\"]',\n )\n const top = layers?.[layers.length - 1]\n if (!top) return\n if (event.key === 'Escape') {\n event.preventDefault()\n close()\n return\n }\n if (event.key !== 'Tab') return\n const items = focusablesIn(top)\n if (!items.length) {\n event.preventDefault()\n top.querySelector<HTMLElement>('[data-ui=\"modal-sheet\"]')?.focus()\n return\n }\n const first = items[0]!\n const last = items.at(-1)!\n if (\n event.shiftKey &&\n (document.activeElement === first || !top.contains(document.activeElement))\n ) {\n event.preventDefault()\n last.focus()\n } else if (\n !event.shiftKey &&\n (document.activeElement === last || !top.contains(document.activeElement))\n ) {\n event.preventDefault()\n first.focus()\n }\n },\n { signal: lifetime },\n )\n\n // An input or select carrying `data-record-state` feeds view state as it\n // changes; a file chosen in a `data-record-submit` input submits its form.\n document.addEventListener(\n 'change',\n (event) => {\n const control = event.target instanceof Element ? event.target : null\n if (!control || !root?.contains(control)) return\n if (\n (control instanceof HTMLSelectElement || control instanceof HTMLInputElement) &&\n control.hasAttribute('data-record-state')\n ) {\n keepAllDrafts()\n viewState.set({\n ...viewState(),\n [control.getAttribute('data-record-state') ?? '']: control.value,\n })\n return\n }\n if (\n control instanceof HTMLInputElement &&\n control.type === 'file' &&\n control.hasAttribute('data-record-submit') &&\n control.files?.length\n )\n control.form?.requestSubmit()\n },\n { signal: lifetime },\n )\n\n // A form marked `data-record-dropzone` takes a dropped file into its file\n // input and submits, so dragging a photo in is the same command as choosing it.\n const dropzoneOf = (event: Event): HTMLFormElement | null => {\n const element = event.target instanceof Element ? event.target : null\n const zone = element?.closest<HTMLFormElement>('form[data-record-dropzone]') ?? null\n return zone && root?.contains(zone) ? zone : null\n }\n document.addEventListener(\n 'dragover',\n (event) => {\n const zone = dropzoneOf(event)\n if (!zone) return\n event.preventDefault()\n zone.dataset.drag = '1'\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'dragleave',\n (event) => {\n const zone = dropzoneOf(event)\n if (!zone) return\n const related = (event as DragEvent).relatedTarget\n if (related instanceof Node && zone.contains(related)) return\n delete zone.dataset.drag\n },\n { signal: lifetime },\n )\n document.addEventListener(\n 'drop',\n (event) => {\n const zone = dropzoneOf(event)\n if (!zone) return\n event.preventDefault()\n delete zone.dataset.drag\n const files = (event as DragEvent).dataTransfer?.files\n const input = zone.querySelector<HTMLInputElement>('input[type=\"file\"]')\n if (!input || !files?.length) return\n const transfer = new DataTransfer()\n transfer.items.add(files[0]!)\n input.files = transfer.files\n zone.requestSubmit()\n },\n { signal: lifetime },\n )\n\n // Islands a view places with `recordIsland` start when their host appears\n // and stop when it leaves, through the page's own island manager.\n const islandSelector = 'ket-island, div[data-ket-island]'\n const holdsIsland = (node: Node): node is Element =>\n node instanceof Element &&\n (node.matches(islandSelector) || node.querySelector(islandSelector) !== null)\n const observer = new MutationObserver((records) => {\n for (const record of records) {\n for (const node of record.removedNodes)\n if (holdsIsland(node))\n document.dispatchEvent(new CustomEvent('ket:islands-detach', { detail: { root: node } }))\n for (const node of record.addedNodes)\n if (holdsIsland(node) && node.isConnected)\n document.dispatchEvent(new CustomEvent('ket:islands-attach', { detail: { root: node } }))\n }\n })\n observer.observe(root, { childList: true, subtree: true })\n lifetime.addEventListener('abort', () => observer.disconnect())\n\n // Another island (or this one) changed records of this kind: drop them so the\n // next opening reads fresh data first.\n document.addEventListener(\n 'ket:records-changed',\n (event) => {\n const detail = (event as CustomEvent<{ kind?: string; ids?: unknown[] }>).detail\n if (detail?.kind !== definition.kind) return\n if (!Array.isArray(detail.ids) || !detail.ids.length) cache.clear()\n else for (const id of detail.ids) cache.delete(String(id))\n },\n { signal: lifetime },\n )\n\n // Back and forward over entries this modal created change only the modal.\n document.addEventListener(\n 'ket:popstate',\n (event) => {\n const target = readRecordModalTarget(location.href)\n const mine = target?.kind === definition.kind\n if (!mine && !open()) return\n event.preventDefault()\n if (mine && target) show(target.id, target.tab ?? null, 'none')\n else hide('none')\n },\n { signal: lifetime },\n )\n\n const initial = readRecordModalTarget(location.href)\n if (initial?.kind === definition.kind) show(initial.id, initial.tab ?? null, 'none')\n\n lifetime.addEventListener('abort', () => {\n request?.abort()\n showBusy.stop()\n releaseInert?.()\n releaseInert = null\n root = null\n })\n },\n }\n }\n\n/**\n * Place an island inside a record view. The runtime starts it when the host\n * appears and disposes it when the host leaves; props must be JSON.\n */\nexport const recordIsland = (name: string, props: Record<string, unknown>): TemplateResult => (\n <ket-island data-island={name} data-props={JSON.stringify(props)} />\n)\n\nexport {\n RECORD_NEW_ID,\n RECORD_PARAM,\n RECORD_TAB_PARAM,\n recordModalHref,\n recordModalCreateHref,\n recordModalClosedHref,\n readRecordModalTarget,\n}\n", "// The record-modal contract, server half.\n//\n// A collection row opens its record in a modal island (design-system contract\n// \"Collections open records in a modal\"). The server's part is small on purpose:\n// it writes links that name a record, and it renders a closed host. Everything a\n// reader does inside the modal \u2014 opening, switching tabs, submitting, closing \u2014\n// happens in the browser, through the runtime in `client/record-modal.tsx`.\n//\n// One URL shape for every module keeps deep links, the back button and the list's\n// own query state working the same way everywhere:\n//\n// /admin/<collection>?<list state>&record=<kind>:<id>&tab=<tab>\n\nimport type { IslandDefinition, TemplateResult } from '@ketvietlab/ketjs-view'\n\nexport const HOOKS = ['record-modal-host'] as const\n\n/** Query parameter naming the open record. Shared by every record modal. */\nexport const RECORD_PARAM = 'record'\n/** Query parameter naming the open tab of the record modal. */\nexport const RECORD_TAB_PARAM = 'tab'\n/**\n * The id a create action names: `record=<kind>:new` opens the same modal with an\n * empty record. A kind whose ids could literally be `new` must not use this.\n */\nexport const RECORD_NEW_ID = 'new'\n\nexport type RecordModalTarget = {\n /** Stable record kind, `<module>.<name>`; never contains a colon. */\n kind: string\n id: string\n tab?: string | null\n}\n\nconst kindPattern = /^[a-z][a-z0-9_]*(?:\\.[a-zA-Z][a-zA-Z0-9_]*)+$/u\n\nexport const isRecordKind = (kind: string): boolean => kindPattern.test(kind)\n\n/**\n * The link a row, card or create action uses to open a record.\n *\n * It keeps every other query parameter, so the collection behind the modal keeps\n * its filters and page. Without scripting it is still a real URL; the server\n * renders the collection and the island opens the record once it hydrates.\n */\nexport const recordModalHref = (url: URL | string, target: RecordModalTarget): string => {\n if (!isRecordKind(target.kind)) throw new TypeError(`invalid record kind \"${target.kind}\"`)\n const next = new URL(String(url), 'http://ket.local')\n next.searchParams.set(RECORD_PARAM, `${target.kind}:${target.id}`)\n if (target.tab) next.searchParams.set(RECORD_TAB_PARAM, target.tab)\n else next.searchParams.delete(RECORD_TAB_PARAM)\n return `${next.pathname}${next.search}`\n}\n\n/**\n * The link a collection's create action uses. It opens the record modal of that\n * kind with no record yet; the create command then switches it to the new record.\n */\nexport const recordModalCreateHref = (\n url: URL | string,\n target: { kind: string; tab?: string | null },\n): string => recordModalHref(url, { kind: target.kind, id: RECORD_NEW_ID, tab: target.tab ?? null })\n\n/** Whether a target asks for the create form rather than an existing record. */\nexport const isRecordModalCreate = (target: Pick<RecordModalTarget, 'id'> | null | undefined): boolean =>\n target?.id === RECORD_NEW_ID\n\n/** The URL with no record open, for closing and for rendering the collection. */\nexport const recordModalClosedHref = (url: URL | string): string => {\n const next = new URL(String(url), 'http://ket.local')\n next.searchParams.delete(RECORD_PARAM)\n next.searchParams.delete(RECORD_TAB_PARAM)\n return `${next.pathname}${next.search}`\n}\n\n/** Which record a URL asks for, or null. The id may itself contain colons. */\nexport const readRecordModalTarget = (url: URL | string): RecordModalTarget | null => {\n const parsed = new URL(String(url), 'http://ket.local')\n const raw = parsed.searchParams.get(RECORD_PARAM) ?? ''\n const split = raw.indexOf(':')\n if (split <= 0 || split === raw.length - 1) return null\n const kind = raw.slice(0, split)\n if (!isRecordKind(kind)) return null\n return { kind, id: raw.slice(split + 1), tab: parsed.searchParams.get(RECORD_TAB_PARAM) }\n}\n\n/** The closed host. Identical on the server and in the first client render. */\nexport const recordModalHost = (kind: string): TemplateResult => (\n <span data-ui=\"record-modal-host\" data-record-kind={kind} hidden />\n)\n\n/**\n * The island declaration a module places through `backend:runtime`.\n *\n * Props are empty: the host must not carry record data into a page that does not\n * show a record, and the record it opens is read through a permission-checked\n * function once a reader asks for it.\n */\nexport const defineRecordModalIsland = (options: {\n kind: string\n /** Browser module, relative to the declaring module's assets directory. */\n client: string\n export: string\n}): IslandDefinition => {\n if (!isRecordKind(options.kind)) throw new TypeError(`invalid record kind \"${options.kind}\"`)\n return {\n props: {},\n client: options.client,\n export: options.export,\n view: () => ({ view: () => recordModalHost(options.kind) }),\n }\n}\n", "// Record-modal form markup, owned by the kit.\n//\n// A record modal's views are module code; markup belongs to `ketsuite/ui`. These\n// are the pieces a view needs beyond the design system's fields and buttons: the\n// form the runtime submits (optionally naming its command), a select whose choice\n// feeds view state before anything is submitted, and a trigger that opens a dialog\n// of the same record.\n\nimport { ActionGroup, Field } from '@ketvietlab/design-system'\nimport type { FieldOption, FieldProps } from '@ketvietlab/design-system'\nimport type { JSXChild, TemplateResult } from '@ketvietlab/ketjs-view'\nimport { RECORD_COMMAND_FIELD } from './record-modal.tsx'\n\n/**\n * The form a record modal's runtime submits. A `command` becomes the hidden\n * `__command` field; without one, the submitting button names it.\n */\nexport const RecordModalForm = (props: {\n kind: string\n fields: readonly FieldProps[]\n actions?: readonly JSXChild[]\n command?: string | null\n /** Lets a button outside this form submit it via the HTML `form` attribute. */\n id?: string\n}): TemplateResult => (\n <form data-ui=\"record-form\" method=\"post\" action=\"\" data-record-kind={props.kind} id={props.id}>\n {props.command ? (\n // autocomplete=\"off\" like every other input the kit writes: the ui contract\n // holds for a hidden field too, and a browser restoring one would submit a\n // command the reader never chose.\n <input type=\"hidden\" name={RECORD_COMMAND_FIELD} value={props.command} autocomplete=\"off\" />\n ) : (\n ''\n )}\n <div data-ui=\"form-grid\">{props.fields.map((item) => Field(item))}</div>\n {props.actions?.length ? <div data-ui=\"form-actions\">{ActionGroup({ actions: props.actions })}</div> : ''}\n </form>\n)\n\n/**\n * The control of a select whose choice changes what the rest of the form offers.\n * Use it as a design-system field's `control`; `data-record-state` makes the\n * runtime re-render the view with the new choice.\n */\nexport const recordStateSelectControl = (props: {\n id: string\n name: string\n state: string\n value: string\n options: readonly FieldOption[]\n required?: boolean\n disabled?: boolean\n invalid?: boolean\n}): TemplateResult => (\n <select\n data-ui=\"field-control\"\n data-record-state={props.state}\n id={props.id}\n name={props.name}\n required={props.required === true}\n disabled={props.disabled === true}\n aria-invalid={props.invalid ? 'true' : null}\n >\n {props.options.map((option) => (\n <option value={option.value} selected={option.value === props.value}>\n {option.label}\n </option>\n ))}\n </select>\n)\n\n/**\n * Opens a dialog of the open record. `id` reaches the dialog as `params.id`;\n * without it the dialog opens for a new child (for example a new team member).\n */\nexport const RecordDialogTrigger = (props: {\n dialog: string\n id?: string | null\n children: JSXChild\n}): TemplateResult => (\n <span data-record-dialog={props.dialog} data-record-param-id={props.id ?? null}>\n {props.children}\n </span>\n)\n\n/**\n * A bare form for an action that names no field of its own \u2014 generate, remove.\n * `data-layout=\"actions\"` opts the form out of the record-form grid (built for a\n * field/label pair), which would otherwise stretch a lone submit button to fill it.\n */\nexport const RecordActionForm = (props: {\n kind: string\n command: string\n hidden?: Record<string, string>\n children: JSXChild\n}): TemplateResult => (\n <form data-ui=\"record-form\" data-layout=\"actions\" method=\"post\" action=\"\" data-record-kind={props.kind}>\n <input type=\"hidden\" name={RECORD_COMMAND_FIELD} value={props.command} autocomplete=\"off\" />\n {Object.entries(props.hidden ?? {}).map(([name, value]) => (\n <input type=\"hidden\" name={name} value={value} autocomplete=\"off\" />\n ))}\n {props.children}\n </form>\n)\n\n/**\n * A form with no field or button of its own, submitted only through the HTML\n * `form` attribute on buttons placed elsewhere \u2014 a menu whose every item names\n * its own command on itself, so one empty form serves the whole menu.\n */\nexport const RecordCommandForm = (props: { kind: string; id: string }): TemplateResult => (\n <form id={props.id} data-record-kind={props.kind} method=\"post\" action=\"\" hidden />\n)\n\n/**\n * Marks its child as a labeled close control the runtime's own click handler\n * recognizes from anywhere in the record's body \u2014 distinct from\n * `data-ui=\"modal-close\"`, the icon-only corner control's own styling hook.\n */\nexport const RecordCloseTrigger = (props: { children: JSXChild }): TemplateResult => (\n <span data-record-close=\"true\">{props.children}</span>\n)\n", "// The user record modal, client side (KetSuite record-modal contract).\n//\n// The users collection opens a person here, and its create action opens the same\n// modal with an empty record. The view is render-pure: it reads what\n// `user.userModalContext` returned and writes design-system markup. The runtime\n// owns reading, submitting, history and focus; the commands call\n// `user.provisionUser` and `user.saveUser`, so the server stays the only place\n// that decides whether a login may exist, where it works and what it may do.\n//\n// Bundled by tools/build-backend-client.mjs into user_backend/client/.\n\nimport {\n Badge,\n Button,\n DataTable,\n DescriptionList,\n Notice,\n RecordSummary,\n Section,\n Stack,\n} from '@ketvietlab/design-system'\nimport type { FieldOption, FieldProps } from '@ketvietlab/design-system'\nimport type { JSXChild } from '@ketvietlab/ketjs-view'\nimport { createRecordModal } from '../../../ui/client/record-modal.tsx'\nimport type { RecordModalContext, RecordModalDefinition } from '../../../ui/client/record-modal.tsx'\nimport {\n RecordDialogTrigger,\n RecordModalForm,\n recordStateSelectControl,\n} from '../../../ui/client/record-modal-form.tsx'\n\n// biome-ignore lint/suspicious/noExplicitAny: rows are JSON shaped by user.userModalContext\ntype AnyRow = Record<string, any>\n\nexport type UserRecord = {\n id: string\n name: string\n login: string\n email: string\n accessKind: string\n active: boolean\n superuser: boolean\n}\n\nexport type UserModalData = {\n record: UserRecord\n companies: AnyRow[]\n branches: AnyRow[]\n assignments: AnyRow[]\n roles: AnyRow[]\n scopeKinds: string[]\n revision: number\n permissions: Record<string, boolean>\n lang: 'vi' | 'en'\n}\n\ntype Context = RecordModalContext<UserModalData>\n\nconst ACCESS_KINDS = ['internal', 'portal', 'public'] as const\n\nconst uuid = (): string =>\n typeof crypto !== 'undefined' && 'randomUUID' in crypto\n ? crypto.randomUUID()\n : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`\n\nconst pageLang = (): 'vi' | 'en' =>\n typeof document !== 'undefined' && document.documentElement.lang === 'en' ? 'en' : 'vi'\n\nconst t = (c: Context, key: string): string => c.t(`user_backend.${key}`)\n\nconst text = (form: FormData, name: string): string => String(form.get(name) ?? '').trim()\n\nconst checked = (form: FormData, name: string): boolean =>\n ['1', 'on', 'true'].includes(String(form.get(name) ?? ''))\n\n/** Whether this reader may write what the open form offers: create one, or save this one. */\nconst canWrite = (c: Context): boolean =>\n c.creating ? c.data.permissions.create === true : c.data.permissions.save === true\n\nconst fieldId = (name: string): string => `user-user-${name}`\n\n/** A field of the record form: typed input survives a refusal, the refusal shows on it. */\nconst field = (c: Context, props: Omit<FieldProps, 'id'>): FieldProps => ({\n ...props,\n id: fieldId(props.name),\n value:\n props.type === 'checkbox'\n ? c.draftChecked(props.name, '1', props.value === true || props.value === '1')\n : props.type === 'checkbox-group'\n ? props.value\n : c.draft(props.name, String(props.value ?? '')),\n options:\n props.type === 'checkbox-group'\n ? props.options?.map((option) => ({\n ...option,\n checked: c.draftChecked(option.name ?? `${props.name}[]`, option.value, option.checked === true),\n }))\n : props.options,\n error: c.fieldError(props.name),\n disabled: props.disabled === true || !canWrite(c),\n})\n\n/** A select whose choice changes what the rest of the form offers, before anything is submitted. */\nconst stateSelect = (\n c: Context,\n props: { name: string; label: string; value: string; options: FieldOption[]; required?: boolean },\n): FieldProps => {\n const base = field(c, {\n name: props.name,\n label: props.label,\n type: 'select',\n required: props.required,\n })\n return {\n ...base,\n options: props.options,\n control: recordStateSelectControl({\n id: base.id,\n name: props.name,\n state: props.name,\n value: props.value,\n options: props.options,\n required: props.required,\n disabled: base.disabled === true,\n invalid: !!base.error,\n }),\n }\n}\n\nconst roleFieldName = (roleId: string): string => `role_${roleId}`\n\nconst selectedRoles = (form: FormData, roles: AnyRow[]): string[] =>\n roles.map((role) => String(role.id)).filter((id) => checked(form, roleFieldName(id)))\n\n/**\n * The form that hires someone.\n *\n * It asks for the whole decision at once \u2014 who they are, where they work, what\n * they do and why \u2014 because that is the request being made, and because the\n * reason is what the audit of their new authority will carry.\n */\nconst createFields = (c: Context): FieldProps[] => {\n const scopeKind = c.state('scopeKind', 'branch')\n const companyId = c.state('companyId', String(c.data.companies[0]?.id ?? ''))\n return [\n field(c, { name: 'name', label: t(c, 'field.name'), required: true }),\n field(c, { name: 'login', label: t(c, 'field.login'), required: true }),\n field(c, { name: 'email', label: t(c, 'field.email'), type: 'email', required: true, span: 'full' }),\n stateSelect(c, {\n name: 'scopeKind',\n label: t(c, 'field.scope'),\n value: scopeKind,\n required: true,\n options: c.data.scopeKinds.map((kind) => ({\n value: kind,\n label: t(c, `scope.choice.${kind}`),\n })),\n }),\n ...(scopeKind === 'tenant'\n ? []\n : [\n stateSelect(c, {\n name: 'companyId',\n label: t(c, 'field.company'),\n value: companyId,\n required: true,\n options: c.data.companies.map((company) => ({\n value: String(company.id),\n label: String(company.name),\n })),\n }),\n ]),\n ...(scopeKind === 'branch'\n ? [\n field(c, {\n name: 'branchId',\n label: t(c, 'field.branch'),\n type: 'select',\n required: true,\n options: c.data.branches\n .filter((branch) => String(branch.companyId) === companyId)\n .map((branch) => ({ value: String(branch.id), label: String(branch.name) })),\n }),\n ]\n : []),\n field(c, {\n name: 'roleIds',\n label: t(c, 'field.jobRoles'),\n type: 'checkbox-group',\n required: true,\n span: 'full',\n // One role per line: a person can hold several, and a row of boxes hides that.\n optionsOrientation: 'vertical',\n options: c.data.roles.map((role) => ({\n name: roleFieldName(String(role.id)),\n value: '1',\n label: String(role.name),\n // What was ticked before a refusal comes back ticked.\n checked: c.draftChecked(roleFieldName(String(role.id))),\n })),\n }),\n field(c, {\n name: 'reason',\n label: t(c, 'field.reason'),\n type: 'textarea',\n required: true,\n span: 'full',\n }),\n ]\n}\n\nconst createView = (c: Context): JSXChild =>\n Section({\n title: t(c, 'users.newTitle'),\n body: RecordModalForm({\n kind: c.kind,\n fields: createFields(c),\n command: 'create',\n actions: [Button({ label: t(c, 'action.createUser'), variant: 'primary', type: 'submit' })],\n }),\n })\n\nconst profileFields = (c: Context): FieldProps[] => [\n field(c, { name: 'name', label: t(c, 'field.name'), value: c.data.record.name, required: true }),\n field(c, { name: 'login', label: t(c, 'field.login'), value: c.data.record.login, required: true }),\n field(c, { name: 'email', label: t(c, 'field.email'), type: 'email', value: c.data.record.email }),\n field(c, {\n name: 'accessKind',\n label: t(c, 'field.accessKind'),\n type: 'select',\n value: c.data.record.accessKind,\n options: ACCESS_KINDS.map((kind) => ({ value: kind, label: t(c, `access.${kind}`) })),\n }),\n field(c, { name: 'active', label: t(c, 'state.active'), type: 'checkbox', value: c.data.record.active }),\n]\n\n/** The person, named once at the top of every tab, with the state that decides access. */\nconst header = (c: Context): JSXChild =>\n c.creating\n ? ''\n : Stack({\n gap: 'compact',\n items: [\n RecordSummary({\n title: c.data.record.name || c.data.record.login,\n subtitle: c.data.record.login,\n status: {\n label: c.data.record.active ? t(c, 'state.active') : t(c, 'state.archived'),\n tone: c.data.record.active ? 'positive' : 'neutral',\n },\n }),\n ...(c.data.permissions.save\n ? [\n RecordDialogTrigger({\n dialog: 'edit',\n children: Button({ label: t(c, 'action.editProfile'), variant: 'secondary' }),\n }),\n ]\n : [\n Notice({\n tone: 'info',\n title: t(c, 'users.readOnlyTitle'),\n message: t(c, 'users.readOnlyHint'),\n }),\n ]),\n ],\n })\n\nconst overviewTab = (c: Context): JSXChild => {\n const company = c.data.assignments.find((assignment) => assignment.company)?.company\n return Section({\n title: t(c, 'users.infoTitle'),\n body: DescriptionList({\n columns: 2,\n items: [\n { id: 'email', label: t(c, 'field.email'), value: c.data.record.email || '\u2014' },\n {\n id: 'state',\n label: t(c, 'field.state'),\n value: Badge({\n label: c.data.record.active ? t(c, 'state.active') : t(c, 'state.archived'),\n tone: c.data.record.active ? 'positive' : 'neutral',\n }),\n },\n { id: 'roles', label: t(c, 'field.rolesHeld'), value: String(c.data.assignments.length) },\n { id: 'company', label: t(c, 'field.company'), value: company ? String(company) : '\u2014' },\n ],\n }),\n })\n}\n\n/** What this person may do, as rows of role and place \u2014 the authority they actually hold. */\nconst accessTab = (c: Context): JSXChild =>\n c.data.assignments.length\n ? DataTable<AnyRow>({\n rows: c.data.assignments,\n id: (row) => String(row.id),\n columns: [\n {\n key: 'role',\n label: t(c, 'field.assignment'),\n priority: 'primary',\n cell: (row) => String(row.roleName),\n },\n {\n key: 'scope',\n label: t(c, 'field.scope'),\n cell: (row) =>\n row.branch\n ? `${String(row.company)} \u00B7 ${String(row.branch)}`\n : row.company\n ? String(row.company)\n : t(c, 'scope.choice.tenant'),\n },\n ],\n })\n : Notice({ tone: 'info', title: t(c, 'users.noAssignments'), message: t(c, 'users.readOnlyHint') })\n\nexport const userModalDefinition: RecordModalDefinition<UserModalData> = {\n kind: 'user.user',\n context: {\n fn: 'user.userModalContext',\n input: (id, creating) => (creating ? { locale: pageLang() } : { id, locale: pageLang() }),\n },\n title: (c) => (c.creating ? t(c, 'users.create') : c.data.record.name || c.data.record.login),\n description: (c) => (c.creating ? t(c, 'users.createSubtitle') : c.data.record.login),\n header,\n body: (c) => (c.creating ? createView(c) : ''),\n tabs: [\n {\n id: 'overview',\n label: (c) => t(c, 'tab.overview'),\n visible: (c) => !c.creating,\n view: overviewTab,\n },\n {\n id: 'access',\n label: (c) => `${t(c, 'tab.access')} ${String(c.data.assignments.length)}`,\n visible: (c) => !c.creating,\n view: accessTab,\n },\n ],\n dialogs: {\n edit: {\n title: (c) => t(c, 'action.editProfile'),\n view: (c) =>\n RecordModalForm({\n kind: c.kind,\n fields: profileFields(c),\n command: 'save',\n actions: [Button({ label: t(c, 'action.save'), variant: 'primary', type: 'submit' })],\n }),\n },\n },\n commands: {\n create: {\n fn: 'user.provisionUser',\n input: (form, c) => ({\n id: uuid(),\n name: text(form, 'name'),\n login: text(form, 'login'),\n email: text(form, 'email') || null,\n accessKind: 'internal',\n roleIds: selectedRoles(form, c.data.roles),\n scopeKind: text(form, 'scopeKind') || 'branch',\n companyId: text(form, 'companyId') || null,\n branchId: text(form, 'branchId') || null,\n reason: text(form, 'reason'),\n expectedAuthorizationRevision: c.data.revision,\n idempotencyKey: uuid(),\n }),\n // The modal switches to the person it created, on their overview.\n after: 'open',\n openTab: 'overview',\n created: (value) => {\n const row = (value ?? {}) as { id?: unknown }\n return typeof row.id === 'string' ? row.id : null\n },\n },\n save: {\n fn: 'user.saveUser',\n input: (form, c) => ({\n id: c.id,\n name: text(form, 'name'),\n login: text(form, 'login'),\n email: text(form, 'email') || null,\n accessKind: text(form, 'accessKind') || 'internal',\n active: checked(form, 'active'),\n // Never offered by this form: saving a profile must not change who is a superuser.\n superuser: c.data.record.superuser,\n }),\n // Read the person again so every tab shows what the server kept, and close the dialog.\n after: 'reload',\n },\n },\n}\n\nexport const userModal = createRecordModal(userModalDefinition)\n"],
5
+ "mappings": "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QAAQ,OAAO,KAAMuB,EAAS,EAEnFA,CAEJ,ECvQQ,OACkC,OAAAG,GADlC,QAAAC,OAAA,4BA/DD,IAAMC,GAAQ,CACnB,YACA,oBACA,mBACA,oBACA,sBACA,oBACA,kBACA,wBACA,oBACA,oBACA,qBACA,mBACA,iBACA,kBACF,ECkCM,OACE,OAAAC,GADF,QAAAC,OAAA,4BAjDC,IAAMC,GAAQ,CACnB,cACA,sBACA,qBACA,wBACA,sBACA,oBACA,sBACA,qBACA,0BACA,sBACA,mBACA,yBACA,yBACA,qBACA,mBACA,mBACF,ECjBO,IAAMC,GAAQ,CACnB,YACA,oBACA,mBACA,sBACA,oBACA,kBACA,oBACA,mBACA,wBACA,oBACA,iBACA,uBACA,uBACA,mBACA,iBACA,iBACF,ECwBI,OASM,OAAAC,GATN,QAAAC,OAAA,4BCzCG,IAAMC,GAAQ,CACnB,iBACA,yBACA,wBACA,yBACA,yBACA,2BACA,uBACA,yBACA,wBACA,6BACA,yBACA,sBACA,qBACF,ECdO,IAAMC,GAAQ,CACnB,aACA,qBACA,oBACA,qBACA,qBACA,uBACA,mBACA,qBACA,oBACA,yBACA,qBACA,kBACA,qBACA,sBACA,iBACF,ECnBA,OAAS,QAAAC,OAAY,sBAyCb,OACE,OAAAC,GADF,QAAAC,OAAA,4BArCD,IAAMC,GAAQ,CACnB,WACA,gBACA,iBACA,sBACA,sBACA,oBACF,ECXA,OAAS,QAAAC,OAAY,sBAuBnB,OAOI,OAAAC,GAPJ,QAAAC,OAAA,4BAhBK,IAAMC,GAAQ,CAAC,cAAe,cAAe,YAAa,cAAc,ECsC/E,IAAMC,GAAS,CACb,QAASC,GACT,SAAUA,GACV,OAAQA,GACR,OAAQA,GACR,WAAYA,GACZ,SAAUA,GACV,KAAMA,GACN,QAASA,GACT,QAASA,GACT,OAAQA,GACR,MAAOA,GACP,QAASA,GACT,SAAUA,GACV,aAAcA,GACd,SAAUA,GACV,SAAUA,GACV,OAAQA,GACR,eAAgBA,GAChB,aAAcA,GACd,WAAYA,GACZ,aAAcA,GACd,SAAUA,GACV,KAAMA,GACN,OAAQA,GACR,SAAUA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",
6
+ "names": ["each", "Fragment", "jsx", "jsxs", "HOOKS", "ActionContent", "props", "Button", "ActionGroup", "props", "jsx", "each", "_", "index", "action", "Fragment", "jsx", "jsxs", "HOOKS", "Badge", "props", "initials", "name", "parts", "first", "Avatar", "props", "jsx", "each", "Fragment", "jsx", "jsxs", "HOOKS", "NOTICE_ICONS", "Notice", "props", "EmptyState", "LoadingState", "_", "index", "each", "each", "Fragment", "jsx", "jsxs", "jsx", "jsxs", "HOOKS", "Fragment", "jsx", "jsxs", "HOOKS", "Items", "props", "each", "_", "index", "item", "Stack", "Surface", "props", "jsxs", "jsx", "Disclosure", "Section", "each", "Fragment", "jsx", "NativeFieldControl", "props", "describedBy", "option", "jsx", "jsxs", "HOOKS", "hasError", "props", "Field", "helpId", "errorId", "describedBy", "label", "Disclosure", "each", "field", "option", "NativeFieldControl", "each", "jsx", "jsxs", "HOOKS", "Tab", "props", "jsxs", "jsx", "tabElementId", "tabsId", "itemId", "Tabs", "each", "item", "TabPanel", "TabbedView", "panelId", "active", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "ModalSheet", "props", "client", "embedded", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "HOOKS", "focusableSelector", "DISMISSIBLE_POPUPS", "DISMISSIBLE_POPUPS_OPEN", "selector", "jsx", "jsxs", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "each", "Fragment", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "KeyValue", "props", "DescriptionList", "each", "item", "Person", "Avatar", "Status", "props", "jsx", "Badge", "jsx", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "RecordSummary", "props", "Person", "Status", "DescriptionList", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "each", "Fragment", "jsx", "jsxs", "HOOKS", "selectedColumnKeys", "props", "column", "visibleColumns", "selected", "colSpan", "columns", "sortLabel", "labels", "columnHeader", "selectionHeader", "isSelected", "row", "rowCells", "each", "tableRow", "groupPager", "pager", "groupRows", "group", "depth", "child", "DataTable", "groups", "content", "EmptyState", "Surface", "jsx", "jsxs", "HOOKS", "jsx", "jsxs", "HOOKS", "HOOKS", "jsx", "jsxs", "HOOKS", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "each", "jsx", "jsxs", "HOOKS", "GROUPS", "HOOKS", "OWNERS", "owner", "hooks", "signal", "jsx", "RECORD_PARAM", "kindPattern", "isRecordKind", "kind", "recordModalHref", "url", "target", "next", "RECORD_PARAM", "recordModalClosedHref", "url", "next", "RECORD_PARAM", "readRecordModalTarget", "parsed", "raw", "split", "kind", "isRecordKind", "recordModalHost", "jsx", "Fragment", "jsx", "jsxs", "issuesOf", "source", "holder", "raw", "item", "path", "callRecordFunction", "name", "input", "options", "response", "payload", "value", "RECORD_MODAL_CACHE_SIZE", "RECORD_MODAL_LABELS", "resolveRecordModalLabel", "key", "sources", "RECORD_COMMAND_FIELD", "RECORD_DIALOG_ATTRIBUTE", "interpolate", "text", "params", "whole", "focusable", "focusablesIn", "element", "recordLayerHasDraft", "layer", "field", "area", "select", "option", "inertOutside", "keep", "changed", "node", "parent", "sibling", "inert", "uuid", "emptyDraftState", "draftCheckKey", "after_", "command", "BUSY_AFTER_MS", "delayedFlag", "show", "after", "timer", "stop", "running", "rowControl", "openerHref", "anchor", "createRecordModal", "definition", "open", "signal", "status", "envelope", "failure", "issues", "recordDrafts", "dialogDrafts", "busy", "showBusy", "saved", "setRunning", "dialog", "version", "viewState", "cache", "remember", "id", "root", "request", "pushed", "returnFocus", "dialogReturnFocus", "releaseInert", "previousMessages", "labels", "t", "visibleTabs", "context", "tab", "contextFor", "current", "data", "scope", "draftState", "base", "hit", "issue", "fallback", "recordModalHref", "tabs", "load", "quiet", "controller", "cached", "creating", "result", "caught", "layers", "recordLayer", "topLayer", "keepDrafts", "previous", "kept", "control", "keepAllDrafts", "currentLayers", "mayDiscard", "afterRender", "preferred", "record", "top", "target", "sheet", "how", "preferredFocus", "sameRecord", "nextTab", "href", "hide", "recordModalClosedHref", "close", "run", "form", "submitter", "formData", "currentLayer", "message", "uploads", "file", "body", "stored", "invoke", "fn", "step", "createdId", "announce", "formIssues", "general", "fieldless", "all", "Notice", "recordBody", "Button", "LoadingState", "active", "header", "issueNotice", "TabbedView", "dialogLayer", "opened", "spec", "ModalSheet", "recordModalHost", "host", "lifetime", "event", "inModal", "stateControl", "opener", "focused", "c", "url", "readRecordModalTarget", "items", "first", "last", "dropzoneOf", "zone", "related", "files", "transfer", "islandSelector", "holdsIsland", "observer", "records", "detail", "mine", "initial", "jsx", "jsxs", "RecordModalForm", "props", "RECORD_COMMAND_FIELD", "item", "Field", "ActionGroup", "recordStateSelectControl", "option", "RecordDialogTrigger", "ACCESS_KINDS", "uuid", "pageLang", "t", "c", "key", "text", "form", "name", "checked", "canWrite", "fieldId", "field", "props", "option", "stateSelect", "base", "recordStateSelectControl", "roleFieldName", "roleId", "selectedRoles", "roles", "role", "id", "createFields", "scopeKind", "companyId", "kind", "company", "branch", "createView", "Section", "RecordModalForm", "Button", "profileFields", "header", "Stack", "RecordSummary", "RecordDialogTrigger", "Notice", "overviewTab", "assignment", "DescriptionList", "Badge", "accessTab", "DataTable", "row", "userModalDefinition", "creating", "value", "userModal", "createRecordModal"]
7
+ }