@alfakit/react 0.1.1 → 0.1.6

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 (219) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/actions/action-button.d.ts.map +1 -1
  3. package/dist/components/actions/action-button.js +5 -4
  4. package/dist/components/actions/action-button.js.map +1 -1
  5. package/dist/components/actions/action-modals.d.ts.map +1 -1
  6. package/dist/components/actions/action-modals.js +6 -5
  7. package/dist/components/actions/action-modals.js.map +1 -1
  8. package/dist/components/ai/copilot.d.ts +3 -1
  9. package/dist/components/ai/copilot.d.ts.map +1 -1
  10. package/dist/components/ai/copilot.js +4 -1
  11. package/dist/components/ai/copilot.js.map +1 -1
  12. package/dist/components/cms/live-preview.js +3 -2
  13. package/dist/components/cms/live-preview.js.map +1 -1
  14. package/dist/components/cms/page.js +1 -1
  15. package/dist/components/fields/builder.js +15 -6
  16. package/dist/components/fields/builder.js.map +1 -1
  17. package/dist/components/fields/color-picker.js +1 -0
  18. package/dist/components/fields/color-picker.js.map +1 -1
  19. package/dist/components/fields/file-upload.js +11 -10
  20. package/dist/components/fields/file-upload.js.map +1 -1
  21. package/dist/components/fields/image-editor-dialog.js +4 -2
  22. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  23. package/dist/components/fields/item-actions.js +10 -10
  24. package/dist/components/fields/item-actions.js.map +1 -1
  25. package/dist/components/fields/key-value.d.ts.map +1 -1
  26. package/dist/components/fields/key-value.js +3 -3
  27. package/dist/components/fields/key-value.js.map +1 -1
  28. package/dist/components/fields/modal-table-select.js +6 -5
  29. package/dist/components/fields/modal-table-select.js.map +1 -1
  30. package/dist/components/fields/one-time-code-input.js +1 -1
  31. package/dist/components/fields/one-time-code-input.js.map +1 -1
  32. package/dist/components/fields/repeater-table.js +12 -1
  33. package/dist/components/fields/repeater-table.js.map +1 -1
  34. package/dist/components/fields/repeater.js +2 -2
  35. package/dist/components/fields/repeater.js.map +1 -1
  36. package/dist/components/fields/rich-editor.js +2 -1
  37. package/dist/components/fields/rich-editor.js.map +1 -1
  38. package/dist/components/fields/select.js +3 -3
  39. package/dist/components/fields/select.js.map +1 -1
  40. package/dist/components/file-manager/file-manager.js +1 -1
  41. package/dist/components/file-manager/file-manager.js.map +1 -1
  42. package/dist/components/form/form-block.d.ts.map +1 -1
  43. package/dist/components/form/form-block.js +14 -2
  44. package/dist/components/form/form-block.js.map +1 -1
  45. package/dist/components/form/form-context.js +7 -3
  46. package/dist/components/form/form-context.js.map +1 -1
  47. package/dist/components/kanban/kanban.js +4 -3
  48. package/dist/components/kanban/kanban.js.map +1 -1
  49. package/dist/components/layout/fused-group.js +1 -0
  50. package/dist/components/layout/fused-group.js.map +1 -1
  51. package/dist/components/layout/section.js +2 -2
  52. package/dist/components/layout/section.js.map +1 -1
  53. package/dist/components/layout/tabs.d.ts.map +1 -1
  54. package/dist/components/layout/tabs.js +54 -15
  55. package/dist/components/layout/tabs.js.map +1 -1
  56. package/dist/components/panel/app-sidebar.d.ts +2 -3
  57. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  58. package/dist/components/panel/app-sidebar.js +1 -24
  59. package/dist/components/panel/app-sidebar.js.map +1 -1
  60. package/dist/components/panel/brand.d.ts +10 -0
  61. package/dist/components/panel/brand.d.ts.map +1 -0
  62. package/dist/components/panel/brand.js +37 -0
  63. package/dist/components/panel/brand.js.map +1 -0
  64. package/dist/components/panel/global-search.js +1 -0
  65. package/dist/components/panel/global-search.js.map +1 -1
  66. package/dist/components/panel/index.d.ts +2 -1
  67. package/dist/components/panel/shortcut-keys.js +1 -0
  68. package/dist/components/panel/shortcut-keys.js.map +1 -1
  69. package/dist/components/panel/tenant-menu.js +2 -2
  70. package/dist/components/panel/tenant-menu.js.map +1 -1
  71. package/dist/components/panel/topbar.js +1 -1
  72. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  73. package/dist/components/realtime/live-records-pill.js +6 -3
  74. package/dist/components/realtime/live-records-pill.js.map +1 -1
  75. package/dist/components/table/cells.d.ts +6 -0
  76. package/dist/components/table/cells.d.ts.map +1 -1
  77. package/dist/components/table/cells.js +15 -3
  78. package/dist/components/table/cells.js.map +1 -1
  79. package/dist/components/table/column-manager.js +8 -7
  80. package/dist/components/table/column-manager.js.map +1 -1
  81. package/dist/components/table/content-layout.js +2 -1
  82. package/dist/components/table/content-layout.js.map +1 -1
  83. package/dist/components/table/grouping.js +11 -1
  84. package/dist/components/table/grouping.js.map +1 -1
  85. package/dist/components/table/pagination.js +16 -13
  86. package/dist/components/table/pagination.js.map +1 -1
  87. package/dist/components/table/table-block.js +14 -4
  88. package/dist/components/table/table-block.js.map +1 -1
  89. package/dist/components/ui/dialog.d.ts.map +1 -1
  90. package/dist/components/ui/dialog.js +3 -1
  91. package/dist/components/ui/dialog.js.map +1 -1
  92. package/dist/components/ui/popover.d.ts.map +1 -1
  93. package/dist/components/ui/popover.js +3 -1
  94. package/dist/components/ui/popover.js.map +1 -1
  95. package/dist/components/ui/sheet.d.ts.map +1 -1
  96. package/dist/components/ui/sheet.js +3 -1
  97. package/dist/components/ui/sheet.js.map +1 -1
  98. package/dist/components/ui/tooltip.js +3 -1
  99. package/dist/components/ui/tooltip.js.map +1 -1
  100. package/dist/components.css +347 -0
  101. package/dist/context.d.ts +5 -0
  102. package/dist/context.d.ts.map +1 -1
  103. package/dist/context.js +5 -1
  104. package/dist/context.js.map +1 -1
  105. package/dist/create-alfakit.d.ts.map +1 -1
  106. package/dist/create-alfakit.js +8 -5
  107. package/dist/create-alfakit.js.map +1 -1
  108. package/dist/eject-manifest.json +16 -3
  109. package/dist/index.d.ts +6 -3
  110. package/dist/index.js +24 -21
  111. package/dist/layouts/app-layout.d.ts.map +1 -1
  112. package/dist/layouts/app-layout.js +2 -29
  113. package/dist/layouts/app-layout.js.map +1 -1
  114. package/dist/layouts/auth-layout.js +2 -2
  115. package/dist/layouts/auth-layout.js.map +1 -1
  116. package/dist/layouts/fullscreen-layout.js +1 -1
  117. package/dist/layouts/fullscreen-layout.js.map +1 -1
  118. package/dist/layouts/lazy-layouts.js +15 -0
  119. package/dist/layouts/lazy-layouts.js.map +1 -0
  120. package/dist/layouts/max-widths.js +34 -0
  121. package/dist/layouts/max-widths.js.map +1 -0
  122. package/dist/layouts/panel-layouts.d.ts +10 -0
  123. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  124. package/dist/layouts/panel-layouts.js +20 -0
  125. package/dist/layouts/panel-layouts.js.map +1 -0
  126. package/dist/layouts/top-nav-layout.js +2 -2
  127. package/dist/layouts/top-nav-layout.js.map +1 -1
  128. package/dist/pages/page.d.ts.map +1 -1
  129. package/dist/pages/page.js +12 -7
  130. package/dist/pages/page.js.map +1 -1
  131. package/dist/standalone/alfakit-component.js +11 -6
  132. package/dist/standalone/alfakit-component.js.map +1 -1
  133. package/dist/standalone/alfakit-toaster.d.ts.map +1 -1
  134. package/dist/standalone/alfakit-toaster.js +8 -1
  135. package/dist/standalone/alfakit-toaster.js.map +1 -1
  136. package/dist/standalone.css +54 -0
  137. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  138. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  139. package/dist/starter-kit/dashboard-widgets.js +53 -0
  140. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  141. package/dist/starter-kit/impersonation-banner.js +34 -0
  142. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  143. package/dist/starter-kit/index.d.ts +3 -2
  144. package/dist/starter-kit/index.js +3 -2
  145. package/dist/starter-kit/starter-kit.d.ts +6 -0
  146. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  147. package/dist/starter-kit/starter-kit.js +18 -7
  148. package/dist/starter-kit/starter-kit.js.map +1 -1
  149. package/dist/starter-kit/widgets-loaded.js +21 -5
  150. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  151. package/dist/starter-kit/widgets-triggers.js +35 -3
  152. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  153. package/dist/starter-kit/widgets.d.ts +13 -5
  154. package/dist/starter-kit/widgets.d.ts.map +1 -1
  155. package/dist/starter-kit/widgets.js +56 -18
  156. package/dist/starter-kit/widgets.js.map +1 -1
  157. package/dist/styles.css +15 -2
  158. package/dist/themes.css +8 -335
  159. package/package.json +2 -2
  160. package/src/components/actions/action-button.tsx +7 -4
  161. package/src/components/actions/action-modals.tsx +13 -1
  162. package/src/components/ai/copilot.tsx +14 -1
  163. package/src/components/cms/live-preview.tsx +5 -2
  164. package/src/components/fields/builder.tsx +14 -5
  165. package/src/components/fields/color-picker.tsx +1 -0
  166. package/src/components/fields/file-upload.tsx +21 -10
  167. package/src/components/fields/image-editor-dialog.tsx +7 -2
  168. package/src/components/fields/item-actions.tsx +32 -9
  169. package/src/components/fields/key-value.tsx +3 -2
  170. package/src/components/fields/modal-table-select.tsx +8 -3
  171. package/src/components/fields/one-time-code-input.tsx +1 -1
  172. package/src/components/fields/repeater-table.tsx +12 -1
  173. package/src/components/fields/repeater.tsx +2 -3
  174. package/src/components/fields/rich-editor.tsx +3 -0
  175. package/src/components/fields/select.tsx +7 -3
  176. package/src/components/file-manager/file-manager.tsx +1 -1
  177. package/src/components/form/form-block.tsx +26 -1
  178. package/src/components/form/form-context.tsx +3 -6
  179. package/src/components/kanban/kanban.tsx +16 -3
  180. package/src/components/layout/fused-group.tsx +5 -1
  181. package/src/components/layout/section.tsx +2 -2
  182. package/src/components/layout/tabs.tsx +72 -17
  183. package/src/components/panel/app-sidebar.tsx +2 -38
  184. package/src/components/panel/brand.tsx +46 -0
  185. package/src/components/panel/global-search.tsx +1 -1
  186. package/src/components/panel/shortcut-keys.tsx +4 -1
  187. package/src/components/panel/tenant-menu.tsx +2 -2
  188. package/src/components/realtime/live-records-pill.tsx +4 -3
  189. package/src/components/table/cells.tsx +27 -5
  190. package/src/components/table/column-manager.tsx +12 -10
  191. package/src/components/table/content-layout.tsx +5 -1
  192. package/src/components/table/grouping.tsx +15 -1
  193. package/src/components/table/pagination.tsx +16 -9
  194. package/src/components/table/table-block.tsx +25 -12
  195. package/src/components/ui/dialog.tsx +5 -1
  196. package/src/components/ui/popover.tsx +5 -1
  197. package/src/components/ui/sheet.tsx +5 -1
  198. package/src/components/ui/tooltip.tsx +5 -1
  199. package/src/context.tsx +10 -0
  200. package/src/create-alfakit.tsx +11 -2
  201. package/src/index.ts +2 -0
  202. package/src/layouts/app-layout.tsx +4 -28
  203. package/src/layouts/auth-layout.tsx +2 -2
  204. package/src/layouts/fullscreen-layout.tsx +1 -1
  205. package/src/layouts/lazy-layouts.ts +14 -0
  206. package/src/layouts/max-widths.ts +29 -0
  207. package/src/layouts/panel-layouts.ts +17 -0
  208. package/src/layouts/top-nav-layout.tsx +2 -2
  209. package/src/pages/page.tsx +21 -12
  210. package/src/standalone/alfakit-component.tsx +18 -7
  211. package/src/standalone/alfakit-toaster.tsx +21 -2
  212. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  213. package/src/starter-kit/impersonation-banner.tsx +48 -0
  214. package/src/starter-kit/index.ts +2 -0
  215. package/src/starter-kit/starter-kit.tsx +31 -4
  216. package/src/starter-kit/widgets-loaded.tsx +23 -1
  217. package/src/starter-kit/widgets-triggers.tsx +37 -2
  218. package/src/starter-kit/widgets.tsx +95 -20
  219. package/src/styles.css +15 -2
@@ -1 +1 @@
1
- {"version":3,"file":"form-block.js","names":["FormBlock"],"sources":["../../../src/components/form/form-block.tsx"],"sourcesContent":["import type { ActionContext, ActionSchema, FormBlock as FormBlockSchema } from '@alfakit/core'\nimport {\n FormStore,\n applyFormRefresh,\n formActionsVisible,\n formIdentity,\n formRefreshFor,\n isImplicitSubmit,\n isLeavingVisit,\n pageFlash,\n pageFormName,\n registerConflictForm,\n registerFormSubmitter,\n registerPageForm,\n resolvePartialProps,\n wizardProgressStore,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useActionRunner, useAlfakitProps, usePageSchema, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { ActionList } from '../actions/action-list'\nimport { showToast } from '../notifications/flash-toasts'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { FormProvider } from './form-context'\n\n/**\n * A page form: its state lives in a FormStore, and its buttons are actions that post the\n * current values through the action endpoint.\n */\nexport function FormBlock({ block }: { block: FormBlockSchema }) {\n const panel = usePanel()\n const page = usePageSchema()\n const props = useAlfakitProps()\n const runner = useActionRunner()\n const t = useT()\n const token = page?.token ?? ''\n\n // The store survives reloads of the same form (table sorting/paging, polling,\n // relation-manager actions all deliver a new `block` object); only a different form\n // identity starts from server state again. The token is re-issued per response, so it\n // is swapped in place instead of being part of the identity.\n const identity = formIdentity(page, block)\n\n const store = useMemo(\n () =>\n new FormStore(block, {\n token,\n component: block.component,\n refreshUrl: panel.endpoints.schemaRefresh,\n optionsUrl: panel.endpoints.schemaOptions,\n validateUrl: panel.endpoints.schemaValidate,\n // A halted wizard step shows what its hook sent.\n onNotifications: (notifications) => notifications.forEach((n) => showToast(n)),\n // Live dashboard filters are applied by the refresh; the widgets reload to follow.\n onRefreshed: () => {\n if (latestBlock.current.liveFilters)\n router.reload({ only: resolvePartialProps(['page']) })\n },\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [identity],\n )\n\n useEffect(() => () => store.destroy(), [store])\n\n // Named forms of custom pages (`<PageForm name>`) publish their store for page method calls.\n useEffect(() => {\n const name = pageFormName(block.component)\n\n return name ? registerPageForm(name, store) : undefined\n }, [block.component, store])\n\n // Edit pages: the conflict banner diffs and merges the main form (`detectConflicts()`).\n useEffect(\n () => (block.component === 'form' ? registerConflictForm(store) : undefined),\n [block.component, store],\n )\n\n useEffect(() => {\n store.setToken(token)\n }, [store, token])\n\n useEffect(() => {\n store.setErrors((props.errors as Record<string, string>) ?? {})\n }, [props.errors, store])\n\n const state = useStore(store.store)\n const context = useMemo<ActionContext>(() => ({ token, component: 'page' }), [token])\n const submitting = useRef(false)\n // After this form's own successful submit, refill from the server's new state. The new\n // block may arrive before or after Inertia's `success` event, so handle both orders.\n const latestBlock = useRef(block)\n const submittedBlock = useRef<FormBlockSchema | null>(null)\n const refillPending = useRef(false)\n\n useEffect(() => {\n latestBlock.current = block\n\n if (refillPending.current) {\n refillPending.current = false\n store.fill(block)\n }\n }, [block, store])\n\n const actions: ActionSchema[] = useMemo(\n () => block.actions.map((action) => ({ ...action, hasModal: false })),\n [block.actions],\n )\n\n const trigger = useCallback(\n async (action: ActionSchema, ctx: ActionContext) => {\n if (action.url && !action.hasAction) {\n return runner.trigger(action, ctx)\n }\n\n submitting.current = true\n submittedBlock.current = latestBlock.current\n store.setProcessing(true)\n runner.execute(action, ctx, store.state.values)\n },\n [runner, store],\n )\n\n // Buttons elsewhere with `formId('form')` (a header \"Save\") submit through this form.\n useEffect(() => registerFormSubmitter(block.component, trigger), [block.component, trigger])\n\n // `refreshFormData([...])`: an action changed the record; copy those fields in.\n useEffect(\n () =>\n router.on('success', (event) => {\n const refresh = formRefreshFor(pageFlash(event.detail.page), block.component)\n\n if (refresh) applyFormRefresh(store, refresh)\n }),\n [block.component, store],\n )\n\n // Wizard forms (`HasWizard` pages) show their buttons on the last step only.\n const progressStore = useMemo(() => wizardProgressStore(store), [store])\n const progress = useStore(progressStore)\n const showActions = formActionsVisible(block, progress)\n\n // Track this form's own submit visit so unrelated reloads finishing meanwhile (polling,\n // relation-manager paging) neither end its processing state nor count as its success.\n const submitVisit = useRef<string | null>(null)\n\n useEffect(() => {\n const offStart = router.on('start', (event) => {\n const visit = event.detail.visit\n\n if (submitting.current && !submitVisit.current && visit.method !== 'get') {\n submitVisit.current = visit.id\n }\n })\n\n const offSuccess = router.on('success', (event) => {\n const id = submitVisit.current\n\n if (!id || (event.detail.visitId ?? id) !== id) return\n\n // A halted action (rate limit, `halt()`) saved nothing: keep what the user typed.\n if (pageFlash(event.detail.page).alfakitAction?.status === 'halted') return\n\n if (latestBlock.current !== submittedBlock.current) {\n store.fill(latestBlock.current)\n } else {\n refillPending.current = true\n }\n })\n\n const offFinish = router.on('finish', (event) => {\n if (submitVisit.current && event.detail.visit.id !== submitVisit.current) return\n\n store.setProcessing(false)\n submitting.current = false\n submitVisit.current = null\n })\n\n return () => {\n offStart()\n offSuccess()\n offFinish()\n }\n }, [store])\n\n useEffect(() => {\n if (!panel.unsavedChangesAlerts) {\n return\n }\n\n return router.on('before', (event) => {\n if (\n store.isDirty &&\n !submitting.current &&\n isLeavingVisit(event.detail.visit) &&\n !window.confirm(t('unsaved_changes', {}, 'You have unsaved changes. Leave anyway?'))\n ) {\n event.preventDefault()\n }\n })\n }, [panel.unsavedChangesAlerts, store, t])\n\n const primary = actions[0]\n\n return (\n <FormProvider store={store}>\n <form\n data-slot=\"form-block\"\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault()\n\n if (primary && showActions) {\n void trigger(primary, context)\n }\n }}\n onKeyDown={(event) => {\n // Enter in a text field runs the primary action (actions are type=\"button\").\n if (primary && showActions && isImplicitSubmit(event.nativeEvent, event.currentTarget)) {\n event.preventDefault()\n void trigger(primary, context)\n }\n }}\n >\n <fieldset disabled={state.processing || block.operation === 'view'} className=\"contents\">\n <SchemaRenderer components={state.components} columns={state.columns} />\n </fieldset>\n {actions.length > 0 && showActions && (\n <div data-slot=\"form-actions\" className=\"border-t pt-6\">\n <ActionList actions={actions} context={context} onTrigger={trigger} />\n </div>\n )}\n </form>\n </FormProvider>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"form-block.js","names":["FormBlock"],"sources":["../../../src/components/form/form-block.tsx"],"sourcesContent":["import type { ActionContext, ActionSchema, FormBlock as FormBlockSchema } from '@alfakit/core'\nimport {\n FormStore,\n applyFormRefresh,\n formActionsVisible,\n formIdentity,\n formRefreshFor,\n isImplicitSubmit,\n isLeavingVisit,\n pageFlash,\n pageFormName,\n registerConflictForm,\n registerFormSubmitter,\n registerPageForm,\n resolvePartialProps,\n shouldResyncForm,\n wizardProgressStore,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useCallback, useEffect, useMemo, useRef } from 'react'\nimport { useActionRunner, useAlfakitProps, usePageSchema, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { ActionList } from '../actions/action-list'\nimport { showToast } from '../notifications/flash-toasts'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { FormProvider } from './form-context'\n\n/**\n * A page form: its state lives in a FormStore, and its buttons are actions that post the\n * current values through the action endpoint.\n */\nexport function FormBlock({ block }: { block: FormBlockSchema }) {\n const panel = usePanel()\n const page = usePageSchema()\n const props = useAlfakitProps()\n const runner = useActionRunner()\n const t = useT()\n const token = page?.token ?? ''\n\n // The store survives reloads of the same form (table sorting/paging, polling,\n // relation-manager actions all deliver a new `block` object); only a different form\n // identity starts from server state again. The token is re-issued per response, so it\n // is swapped in place instead of being part of the identity.\n const identity = formIdentity(page, block)\n\n const store = useMemo(\n () =>\n new FormStore(block, {\n token,\n component: block.component,\n refreshUrl: panel.endpoints.schemaRefresh,\n optionsUrl: panel.endpoints.schemaOptions,\n validateUrl: panel.endpoints.schemaValidate,\n // A halted wizard step shows what its hook sent.\n onNotifications: (notifications) => notifications.forEach((n) => showToast(n)),\n // Live dashboard filters are applied by the refresh; the widgets reload to follow.\n onRefreshed: () => {\n if (latestBlock.current.liveFilters)\n router.reload({ only: resolvePartialProps(['page']) })\n },\n }),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [identity],\n )\n\n useEffect(() => () => store.destroy(), [store])\n\n // Named forms of custom pages (`<PageForm name>`) publish their store for page method calls.\n useEffect(() => {\n const name = pageFormName(block.component)\n\n return name ? registerPageForm(name, store) : undefined\n }, [block.component, store])\n\n // Edit pages: the conflict banner diffs and merges the main form (`detectConflicts()`).\n useEffect(\n () => (block.component === 'form' ? registerConflictForm(store) : undefined),\n [block.component, store],\n )\n\n useEffect(() => {\n store.setToken(token)\n }, [store, token])\n\n useEffect(() => {\n store.setErrors((props.errors as Record<string, string>) ?? {})\n }, [props.errors, store])\n\n const state = useStore(store.store)\n const context = useMemo<ActionContext>(() => ({ token, component: 'page' }), [token])\n const submitting = useRef(false)\n // After this form's own successful submit, refill from the server's new state. The new\n // block may arrive before or after Inertia's `success` event, so handle both orders.\n const latestBlock = useRef(block)\n const submittedBlock = useRef<FormBlockSchema | null>(null)\n const refillPending = useRef(false)\n // Another action succeeded and its new block has not arrived yet.\n const resyncPending = useRef(false)\n // The block when the latest non-GET visit started, to tell whether its response arrived.\n const visitBlock = useRef<FormBlockSchema | null>(null)\n\n useEffect(() => {\n latestBlock.current = block\n\n if (refillPending.current) {\n refillPending.current = false\n store.fill(block)\n }\n\n if (resyncPending.current) {\n resyncPending.current = false\n\n if (!store.isDirty) store.fill(block)\n }\n }, [block, store])\n\n const actions: ActionSchema[] = useMemo(\n () => block.actions.map((action) => ({ ...action, hasModal: false })),\n [block.actions],\n )\n\n const trigger = useCallback(\n async (action: ActionSchema, ctx: ActionContext) => {\n if (action.url && !action.hasAction) {\n return runner.trigger(action, ctx)\n }\n\n submitting.current = true\n submittedBlock.current = latestBlock.current\n store.setProcessing(true)\n // A click that blurred a live field started a refresh: send the state it returns.\n store.whenSettled(() => runner.execute(action, ctx, store.state.values))\n },\n [runner, store],\n )\n\n // Buttons elsewhere with `formId('form')` (a header \"Save\") submit through this form.\n useEffect(() => registerFormSubmitter(block.component, trigger), [block.component, trigger])\n\n // `refreshFormData([...])`: an action changed the record; copy those fields in.\n useEffect(\n () =>\n router.on('success', (event) => {\n const refresh = formRefreshFor(pageFlash(event.detail.page), block.component)\n\n if (refresh) applyFormRefresh(store, refresh)\n }),\n [block.component, store],\n )\n\n // Wizard forms (`HasWizard` pages) show their buttons on the last step only.\n const progressStore = useMemo(() => wizardProgressStore(store), [store])\n const progress = useStore(progressStore)\n const showActions = formActionsVisible(block, progress)\n\n // Track this form's own submit visit so unrelated reloads finishing meanwhile (polling,\n // relation-manager paging) neither end its processing state nor count as its success.\n const submitVisit = useRef<string | null>(null)\n\n useEffect(() => {\n const offStart = router.on('start', (event) => {\n const visit = event.detail.visit\n\n if (submitting.current && !submitVisit.current && visit.method !== 'get') {\n submitVisit.current = visit.id\n }\n\n if (visit.method !== 'get') visitBlock.current = latestBlock.current\n })\n\n const offSuccess = router.on('success', (event) => {\n const id = submitVisit.current\n\n // Another action changed the record: show the server's new form unless it holds edits.\n if (!id && shouldResyncForm(pageFlash(event.detail.page), store)) {\n if (latestBlock.current !== visitBlock.current) {\n store.fill(latestBlock.current)\n } else {\n resyncPending.current = true\n }\n\n return\n }\n\n if (!id || (event.detail.visitId ?? id) !== id) return\n\n // A halted action (rate limit, `halt()`) saved nothing: keep what the user typed.\n if (pageFlash(event.detail.page).alfakitAction?.status === 'halted') return\n\n if (latestBlock.current !== submittedBlock.current) {\n store.fill(latestBlock.current)\n } else {\n refillPending.current = true\n }\n })\n\n const offFinish = router.on('finish', (event) => {\n if (submitVisit.current && event.detail.visit.id !== submitVisit.current) return\n\n store.setProcessing(false)\n submitting.current = false\n submitVisit.current = null\n })\n\n return () => {\n offStart()\n offSuccess()\n offFinish()\n }\n }, [store])\n\n useEffect(() => {\n if (!panel.unsavedChangesAlerts) {\n return\n }\n\n return router.on('before', (event) => {\n if (\n store.isDirty &&\n !submitting.current &&\n isLeavingVisit(event.detail.visit) &&\n !window.confirm(t('unsaved_changes', {}, 'You have unsaved changes. Leave anyway?'))\n ) {\n event.preventDefault()\n }\n })\n }, [panel.unsavedChangesAlerts, store, t])\n\n const primary = actions[0]\n\n return (\n <FormProvider store={store}>\n <form\n data-slot=\"form-block\"\n className=\"flex flex-col gap-6\"\n onSubmit={(event) => {\n event.preventDefault()\n\n if (primary && showActions) {\n void trigger(primary, context)\n }\n }}\n onKeyDown={(event) => {\n // Enter in a text field runs the primary action (actions are type=\"button\").\n if (primary && showActions && isImplicitSubmit(event.nativeEvent, event.currentTarget)) {\n event.preventDefault()\n void trigger(primary, context)\n }\n }}\n >\n <fieldset disabled={state.processing || block.operation === 'view'} className=\"contents\">\n <SchemaRenderer components={state.components} columns={state.columns} />\n </fieldset>\n {actions.length > 0 && showActions && (\n <div data-slot=\"form-actions\" className=\"border-t pt-6\">\n <ActionList actions={actions} context={context} onTrigger={trigger} />\n </div>\n )}\n </form>\n </FormProvider>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;AA+BA,SAAgBA,YAAU,EAAE,SAAqC;CAC/D,MAAM,QAAQ,SAAS;CACvB,MAAM,OAAO,cAAc;CAC3B,MAAM,QAAQ,gBAAgB;CAC9B,MAAM,SAAS,gBAAgB;CAC/B,MAAM,IAAI,KAAK;CACf,MAAM,QAAQ,MAAM,SAAS;CAM7B,MAAM,WAAW,aAAa,MAAM,KAAK;CAEzC,MAAM,QAAQ,cAEV,IAAI,UAAU,OAAO;EACnB;EACA,WAAW,MAAM;EACjB,YAAY,MAAM,UAAU;EAC5B,YAAY,MAAM,UAAU;EAC5B,aAAa,MAAM,UAAU;EAE7B,kBAAkB,kBAAkB,cAAc,SAAS,MAAM,UAAU,CAAC,CAAC;EAE7E,mBAAmB;GACjB,IAAI,YAAY,QAAQ,aACtB,OAAO,OAAO,EAAE,MAAM,oBAAoB,CAAC,MAAM,CAAC,EAAE,CAAC;EACzD;CACF,CAAC,GAEH,CAAC,QAAQ,CACX;CAEA,sBAAsB,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC;CAG9C,gBAAgB;EACd,MAAM,OAAO,aAAa,MAAM,SAAS;EAEzC,OAAO,OAAO,iBAAiB,MAAM,KAAK,IAAI,KAAA;CAChD,GAAG,CAAC,MAAM,WAAW,KAAK,CAAC;CAG3B,gBACS,MAAM,cAAc,SAAS,qBAAqB,KAAK,IAAI,KAAA,GAClE,CAAC,MAAM,WAAW,KAAK,CACzB;CAEA,gBAAgB;EACd,MAAM,SAAS,KAAK;CACtB,GAAG,CAAC,OAAO,KAAK,CAAC;CAEjB,gBAAgB;EACd,MAAM,UAAW,MAAM,UAAqC,CAAC,CAAC;CAChE,GAAG,CAAC,MAAM,QAAQ,KAAK,CAAC;CAExB,MAAM,QAAQ,SAAS,MAAM,KAAK;CAClC,MAAM,UAAU,eAA8B;EAAE;EAAO,WAAW;CAAO,IAAI,CAAC,KAAK,CAAC;CACpF,MAAM,aAAa,OAAO,KAAK;CAG/B,MAAM,cAAc,OAAO,KAAK;CAChC,MAAM,iBAAiB,OAA+B,IAAI;CAC1D,MAAM,gBAAgB,OAAO,KAAK;CAElC,MAAM,gBAAgB,OAAO,KAAK;CAElC,MAAM,aAAa,OAA+B,IAAI;CAEtD,gBAAgB;EACd,YAAY,UAAU;EAEtB,IAAI,cAAc,SAAS;GACzB,cAAc,UAAU;GACxB,MAAM,KAAK,KAAK;EAClB;EAEA,IAAI,cAAc,SAAS;GACzB,cAAc,UAAU;GAExB,IAAI,CAAC,MAAM,SAAS,MAAM,KAAK,KAAK;EACtC;CACF,GAAG,CAAC,OAAO,KAAK,CAAC;CAEjB,MAAM,UAA0B,cACxB,MAAM,QAAQ,KAAK,YAAY;EAAE,GAAG;EAAQ,UAAU;CAAM,EAAE,GACpE,CAAC,MAAM,OAAO,CAChB;CAEA,MAAM,UAAU,YACd,OAAO,QAAsB,QAAuB;EAClD,IAAI,OAAO,OAAO,CAAC,OAAO,WACxB,OAAO,OAAO,QAAQ,QAAQ,GAAG;EAGnC,WAAW,UAAU;EACrB,eAAe,UAAU,YAAY;EACrC,MAAM,cAAc,IAAI;EAExB,MAAM,kBAAkB,OAAO,QAAQ,QAAQ,KAAK,MAAM,MAAM,MAAM,CAAC;CACzE,GACA,CAAC,QAAQ,KAAK,CAChB;CAGA,gBAAgB,sBAAsB,MAAM,WAAW,OAAO,GAAG,CAAC,MAAM,WAAW,OAAO,CAAC;CAG3F,gBAEI,OAAO,GAAG,YAAY,UAAU;EAC9B,MAAM,UAAU,eAAe,UAAU,MAAM,OAAO,IAAI,GAAG,MAAM,SAAS;EAE5E,IAAI,SAAS,iBAAiB,OAAO,OAAO;CAC9C,CAAC,GACH,CAAC,MAAM,WAAW,KAAK,CACzB;CAGA,MAAM,gBAAgB,cAAc,oBAAoB,KAAK,GAAG,CAAC,KAAK,CAAC;CACvE,MAAM,WAAW,SAAS,aAAa;CACvC,MAAM,cAAc,mBAAmB,OAAO,QAAQ;CAItD,MAAM,cAAc,OAAsB,IAAI;CAE9C,gBAAgB;EACd,MAAM,WAAW,OAAO,GAAG,UAAU,UAAU;GAC7C,MAAM,QAAQ,MAAM,OAAO;GAE3B,IAAI,WAAW,WAAW,CAAC,YAAY,WAAW,MAAM,WAAW,OACjE,YAAY,UAAU,MAAM;GAG9B,IAAI,MAAM,WAAW,OAAO,WAAW,UAAU,YAAY;EAC/D,CAAC;EAED,MAAM,aAAa,OAAO,GAAG,YAAY,UAAU;GACjD,MAAM,KAAK,YAAY;GAGvB,IAAI,CAAC,MAAM,iBAAiB,UAAU,MAAM,OAAO,IAAI,GAAG,KAAK,GAAG;IAChE,IAAI,YAAY,YAAY,WAAW,SACrC,MAAM,KAAK,YAAY,OAAO;SAE9B,cAAc,UAAU;IAG1B;GACF;GAEA,IAAI,CAAC,OAAO,MAAM,OAAO,WAAW,QAAQ,IAAI;GAGhD,IAAI,UAAU,MAAM,OAAO,IAAI,CAAC,CAAC,eAAe,WAAW,UAAU;GAErE,IAAI,YAAY,YAAY,eAAe,SACzC,MAAM,KAAK,YAAY,OAAO;QAE9B,cAAc,UAAU;EAE5B,CAAC;EAED,MAAM,YAAY,OAAO,GAAG,WAAW,UAAU;GAC/C,IAAI,YAAY,WAAW,MAAM,OAAO,MAAM,OAAO,YAAY,SAAS;GAE1E,MAAM,cAAc,KAAK;GACzB,WAAW,UAAU;GACrB,YAAY,UAAU;EACxB,CAAC;EAED,aAAa;GACX,SAAS;GACT,WAAW;GACX,UAAU;EACZ;CACF,GAAG,CAAC,KAAK,CAAC;CAEV,gBAAgB;EACd,IAAI,CAAC,MAAM,sBACT;EAGF,OAAO,OAAO,GAAG,WAAW,UAAU;GACpC,IACE,MAAM,WACN,CAAC,WAAW,WACZ,eAAe,MAAM,OAAO,KAAK,KACjC,CAAC,OAAO,QAAQ,EAAE,mBAAmB,CAAC,GAAG,yCAAyC,CAAC,GAEnF,MAAM,eAAe;EAEzB,CAAC;CACH,GAAG;EAAC,MAAM;EAAsB;EAAO;CAAC,CAAC;CAEzC,MAAM,UAAU,QAAQ;CAExB,OACE,oBAAC,cAAD;EAAqB;EACnB,UAAA,qBAAC,QAAD;GACE,aAAU;GACV,WAAU;GACV,WAAW,UAAU;IACnB,MAAM,eAAe;IAErB,IAAI,WAAW,aACb,QAAa,SAAS,OAAO;GAEjC;GACA,YAAY,UAAU;IAEpB,IAAI,WAAW,eAAe,iBAAiB,MAAM,aAAa,MAAM,aAAa,GAAG;KACtF,MAAM,eAAe;KACrB,QAAa,SAAS,OAAO;IAC/B;GACF;GAhBF,UAAA,CAkBE,oBAAC,YAAD;IAAU,UAAU,MAAM,cAAc,MAAM,cAAc;IAAQ,WAAU;IAC5E,UAAA,oBAAC,gBAAD;KAAgB,YAAY,MAAM;KAAY,SAAS,MAAM;IAAU,CAAA;GAC/D,CAAA,GACT,QAAQ,SAAS,KAAK,eACrB,oBAAC,OAAD;IAAK,aAAU;IAAe,WAAU;IACtC,UAAA,oBAAC,YAAD;KAAqB;KAAkB;KAAS,WAAW;IAAU,CAAA;GAClE,CAAA,CAEH;;CACM,CAAA;AAElB"}
@@ -1,5 +1,5 @@
1
1
  import { useStore, useStoreSelector } from "../../hooks/use-store.js";
2
- import { fieldAria } from "@alfakit/core";
2
+ import { fieldAria, fieldDomId, fieldError } from "@alfakit/core";
3
3
  import { createContext, useCallback, useContext, useMemo } from "react";
4
4
  import { jsx } from "react/jsx-runtime";
5
5
  //#region src/components/form/form-context.tsx
@@ -33,10 +33,14 @@ function useField(field) {
33
33
  }
34
34
  return current;
35
35
  });
36
- const error = useStoreSelector(store.store, (state) => state.errors[path]?.[0]);
36
+ const error = useStoreSelector(store.store, (state) => fieldError(state.errors, path));
37
37
  const setValue = useCallback((next) => store.setValue(path, next), [store, path]);
38
38
  const blur = useCallback(() => store.blur(path), [store, path]);
39
- const id = useMemo(() => field.id ?? `alfakit-${path.replace(/[^a-zA-Z0-9_-]/g, "-")}`, [field.id, path]);
39
+ const id = useMemo(() => field.id ?? fieldDomId(store.idPrefix, path), [
40
+ field.id,
41
+ store,
42
+ path
43
+ ]);
40
44
  const aria = useMemo(() => fieldAria(id, {
41
45
  hint: field.hint,
42
46
  helperText: field.helperText,
@@ -1 +1 @@
1
- {"version":3,"file":"form-context.js","names":[],"sources":["../../../src/components/form/form-context.tsx"],"sourcesContent":["import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'\nimport { fieldAria } from '@alfakit/core'\nimport { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'\nimport { useStore, useStoreSelector } from '../../hooks/use-store'\n\nconst FormStoreContext = createContext<FormStore | null>(null)\n\nexport function FormProvider({ store, children }: { store: FormStore; children: ReactNode }) {\n return <FormStoreContext.Provider value={store}>{children}</FormStoreContext.Provider>\n}\n\nexport function useFormStore(): FormStore {\n const store = useContext(FormStoreContext)\n\n if (!store) {\n throw new Error('Form fields must be rendered inside a FormProvider.')\n }\n\n return store\n}\n\nexport function useOptionalFormStore(): FormStore | null {\n return useContext(FormStoreContext)\n}\n\nexport function useFormState(): FormState {\n return useStore(useFormStore().store)\n}\n\nexport interface FieldBinding<T = unknown> {\n value: T\n setValue: (value: T) => void\n blur: () => void\n error: string | undefined\n id: string\n /** The field cannot be interacted with at all (`disabled()`). */\n disabled: boolean\n /** The field shows its value but the user cannot change it (`readOnly()`). */\n readOnly: boolean\n /** `aria-describedby` (hint, helper text, error), `aria-invalid` and `aria-required` for the control. */\n aria: FieldAriaAttributes\n}\n\n/** Bind a field schema (a built-in or custom field) to the form store. */\nexport function useField<T = unknown>(field: BaseField): FieldBinding<T> {\n const store = useFormStore()\n const path = field.statePath\n\n const value = useStoreSelector(store.store, (state) => {\n let current: unknown = state.values\n\n for (const segment of path.split('.')) {\n if (current === null || current === undefined || typeof current !== 'object') {\n return undefined as T\n }\n\n current = (current as Record<string, unknown>)[segment]\n }\n\n return current as T\n })\n\n const error = useStoreSelector(store.store, (state) => state.errors[path]?.[0])\n\n const setValue = useCallback((next: T) => store.setValue(path, next), [store, path])\n const blur = useCallback(() => store.blur(path), [store, path])\n\n const id = useMemo(\n () => field.id ?? `alfakit-${path.replace(/[^a-zA-Z0-9_-]/g, '-')}`,\n [field.id, path],\n )\n\n const aria = useMemo(\n () =>\n fieldAria(\n id,\n { hint: field.hint, helperText: field.helperText, required: field.required },\n error,\n ),\n [id, field.hint, field.helperText, field.required, error],\n )\n\n // Fields stay enabled during a live refresh: the store keeps edits made while the request\n // is in flight, so locking inputs would only swallow keystrokes.\n return {\n value,\n setValue,\n blur,\n error,\n id,\n disabled: Boolean(field.disabled),\n readOnly: Boolean(field.readOnly),\n aria,\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,mBAAmB,cAAgC,IAAI;AAE7D,SAAgB,aAAa,EAAE,OAAO,YAAuD;CAC3F,OAAO,oBAAC,iBAAiB,UAAlB;EAA2B,OAAO;EAAQ;CAAoC,CAAA;AACvF;AAEA,SAAgB,eAA0B;CACxC,MAAM,QAAQ,WAAW,gBAAgB;CAEzC,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,qDAAqD;CAGvE,OAAO;AACT;AAEA,SAAgB,uBAAyC;CACvD,OAAO,WAAW,gBAAgB;AACpC;AAEA,SAAgB,eAA0B;CACxC,OAAO,SAAS,aAAa,CAAC,CAAC,KAAK;AACtC;;AAiBA,SAAgB,SAAsB,OAAmC;CACvE,MAAM,QAAQ,aAAa;CAC3B,MAAM,OAAO,MAAM;CAEnB,MAAM,QAAQ,iBAAiB,MAAM,QAAQ,UAAU;EACrD,IAAI,UAAmB,MAAM;EAE7B,KAAK,MAAM,WAAW,KAAK,MAAM,GAAG,GAAG;GACrC,IAAI,YAAY,QAAQ,YAAY,KAAA,KAAa,OAAO,YAAY,UAClE;GAGF,UAAW,QAAoC;EACjD;EAEA,OAAO;CACT,CAAC;CAED,MAAM,QAAQ,iBAAiB,MAAM,QAAQ,UAAU,MAAM,OAAO,KAAK,GAAG,EAAE;CAE9E,MAAM,WAAW,aAAa,SAAY,MAAM,SAAS,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC;CACnF,MAAM,OAAO,kBAAkB,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC;CAE9D,MAAM,KAAK,cACH,MAAM,MAAM,WAAW,KAAK,QAAQ,mBAAmB,GAAG,KAChE,CAAC,MAAM,IAAI,IAAI,CACjB;CAEA,MAAM,OAAO,cAET,UACE,IACA;EAAE,MAAM,MAAM;EAAM,YAAY,MAAM;EAAY,UAAU,MAAM;CAAS,GAC3E,KACF,GACF;EAAC;EAAI,MAAM;EAAM,MAAM;EAAY,MAAM;EAAU;CAAK,CAC1D;CAIA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,UAAU,QAAQ,MAAM,QAAQ;EAChC,UAAU,QAAQ,MAAM,QAAQ;EAChC;CACF;AACF"}
1
+ {"version":3,"file":"form-context.js","names":[],"sources":["../../../src/components/form/form-context.tsx"],"sourcesContent":["import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'\nimport { fieldAria, fieldDomId, fieldError } from '@alfakit/core'\nimport { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'\nimport { useStore, useStoreSelector } from '../../hooks/use-store'\n\nconst FormStoreContext = createContext<FormStore | null>(null)\n\nexport function FormProvider({ store, children }: { store: FormStore; children: ReactNode }) {\n return <FormStoreContext.Provider value={store}>{children}</FormStoreContext.Provider>\n}\n\nexport function useFormStore(): FormStore {\n const store = useContext(FormStoreContext)\n\n if (!store) {\n throw new Error('Form fields must be rendered inside a FormProvider.')\n }\n\n return store\n}\n\nexport function useOptionalFormStore(): FormStore | null {\n return useContext(FormStoreContext)\n}\n\nexport function useFormState(): FormState {\n return useStore(useFormStore().store)\n}\n\nexport interface FieldBinding<T = unknown> {\n value: T\n setValue: (value: T) => void\n blur: () => void\n error: string | undefined\n id: string\n /** The field cannot be interacted with at all (`disabled()`). */\n disabled: boolean\n /** The field shows its value but the user cannot change it (`readOnly()`). */\n readOnly: boolean\n /** `aria-describedby` (hint, helper text, error), `aria-invalid` and `aria-required` for the control. */\n aria: FieldAriaAttributes\n}\n\n/** Bind a field schema (a built-in or custom field) to the form store. */\nexport function useField<T = unknown>(field: BaseField): FieldBinding<T> {\n const store = useFormStore()\n const path = field.statePath\n\n const value = useStoreSelector(store.store, (state) => {\n let current: unknown = state.values\n\n for (const segment of path.split('.')) {\n if (current === null || current === undefined || typeof current !== 'object') {\n return undefined as T\n }\n\n current = (current as Record<string, unknown>)[segment]\n }\n\n return current as T\n })\n\n const error = useStoreSelector(store.store, (state) => fieldError(state.errors, path))\n\n const setValue = useCallback((next: T) => store.setValue(path, next), [store, path])\n const blur = useCallback(() => store.blur(path), [store, path])\n\n const id = useMemo(() => field.id ?? fieldDomId(store.idPrefix, path), [field.id, store, path])\n\n const aria = useMemo(\n () =>\n fieldAria(\n id,\n { hint: field.hint, helperText: field.helperText, required: field.required },\n error,\n ),\n [id, field.hint, field.helperText, field.required, error],\n )\n\n // Fields stay enabled during a live refresh: the store keeps edits made while the request\n // is in flight, so locking inputs would only swallow keystrokes.\n return {\n value,\n setValue,\n blur,\n error,\n id,\n disabled: Boolean(field.disabled),\n readOnly: Boolean(field.readOnly),\n aria,\n }\n}\n"],"mappings":";;;;;AAKA,MAAM,mBAAmB,cAAgC,IAAI;AAE7D,SAAgB,aAAa,EAAE,OAAO,YAAuD;CAC3F,OAAO,oBAAC,iBAAiB,UAAlB;EAA2B,OAAO;EAAQ;CAAoC,CAAA;AACvF;AAEA,SAAgB,eAA0B;CACxC,MAAM,QAAQ,WAAW,gBAAgB;CAEzC,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,qDAAqD;CAGvE,OAAO;AACT;AAEA,SAAgB,uBAAyC;CACvD,OAAO,WAAW,gBAAgB;AACpC;AAEA,SAAgB,eAA0B;CACxC,OAAO,SAAS,aAAa,CAAC,CAAC,KAAK;AACtC;;AAiBA,SAAgB,SAAsB,OAAmC;CACvE,MAAM,QAAQ,aAAa;CAC3B,MAAM,OAAO,MAAM;CAEnB,MAAM,QAAQ,iBAAiB,MAAM,QAAQ,UAAU;EACrD,IAAI,UAAmB,MAAM;EAE7B,KAAK,MAAM,WAAW,KAAK,MAAM,GAAG,GAAG;GACrC,IAAI,YAAY,QAAQ,YAAY,KAAA,KAAa,OAAO,YAAY,UAClE;GAGF,UAAW,QAAoC;EACjD;EAEA,OAAO;CACT,CAAC;CAED,MAAM,QAAQ,iBAAiB,MAAM,QAAQ,UAAU,WAAW,MAAM,QAAQ,IAAI,CAAC;CAErF,MAAM,WAAW,aAAa,SAAY,MAAM,SAAS,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC;CACnF,MAAM,OAAO,kBAAkB,MAAM,KAAK,IAAI,GAAG,CAAC,OAAO,IAAI,CAAC;CAE9D,MAAM,KAAK,cAAc,MAAM,MAAM,WAAW,MAAM,UAAU,IAAI,GAAG;EAAC,MAAM;EAAI;EAAO;CAAI,CAAC;CAE9F,MAAM,OAAO,cAET,UACE,IACA;EAAE,MAAM,MAAM;EAAM,YAAY,MAAM;EAAY,UAAU,MAAM;CAAS,GAC3E,KACF,GACF;EAAC;EAAI,MAAM;EAAM,MAAM;EAAY,MAAM;EAAU;CAAK,CAC1D;CAIA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA,UAAU,QAAQ,MAAM,QAAQ;EAChC,UAAU,QAAQ,MAAM,QAAQ;EAChC;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { useActionRunner, useDirection, useFormatter, usePanel, useT } from "../../context.js";
1
+ import { useActionRunner, useChoice, useDirection, useFormatter, usePanel, useT } from "../../context.js";
2
2
  import { usePageActionContext } from "../actions/action-context.js";
3
3
  import { cn } from "../../lib/utils.js";
4
4
  import { badgeColorClasses, colorClass, cssColor } from "../../lib/colors.js";
@@ -303,6 +303,7 @@ function KanbanFilters({ controller, store, t }) {
303
303
  })] });
304
304
  }
305
305
  function ColumnHeader({ column, id, collapsed, creatable, filtered, onToggle, onCreate, t }) {
306
+ const choice = useChoice();
306
307
  const wip = kanbanWipState(column);
307
308
  const shown = column.wipLimit !== void 0 ? column.count : filtered ? column.total : column.count;
308
309
  const countLabel = column.wipLimit !== void 0 ? t("kanban.wip", {
@@ -311,7 +312,7 @@ function ColumnHeader({ column, id, collapsed, creatable, filtered, onToggle, on
311
312
  }, ":count of :limit") : filtered && column.total !== column.count ? t("kanban.count_filtered", {
312
313
  shown: column.total,
313
314
  count: column.count
314
- }, ":shown of :count cards") : t("kanban.count", { count: column.count }, ":count cards");
315
+ }, ":shown of :count cards") : choice("kanban.count", column.count, {}, "{1} :count card|[0,*] :count cards");
315
316
  const style = column.color ? { borderTopColor: cssColor(column.color) } : void 0;
316
317
  const toggleLabel = collapsed ? t("kanban.expand", { column: column.label }, "Expand :column") : t("kanban.collapse", { column: column.label }, "Collapse :column");
317
318
  if (collapsed) return /* @__PURE__ */ jsxs("div", {
@@ -403,7 +404,7 @@ function ColumnHeader({ column, id, collapsed, creatable, filtered, onToggle, on
403
404
  }),
404
405
  wip === "over" && /* @__PURE__ */ jsx("p", {
405
406
  className: "text-destructive text-xs",
406
- children: t("kanban.wip_over", { limit: column.wipLimit ?? 0 }, "Over the limit of :limit cards")
407
+ children: choice("kanban.wip_over", column.wipLimit ?? 0, { limit: column.wipLimit ?? 0 }, "{1} Over the limit of :limit card|[0,*] Over the limit of :limit cards")
407
408
  }),
408
409
  wip === "full" && column.wipStrict && /* @__PURE__ */ jsx("p", {
409
410
  className: "text-muted-foreground text-xs",
@@ -1 +1 @@
1
- {"version":3,"file":"kanban.js","names":[],"sources":["../../../src/components/kanban/kanban.tsx"],"sourcesContent":["import type {\n KanbanCardSchema,\n KanbanColumnSchema,\n KanbanEnvironment,\n KanbanLaneSchema,\n KanbanSchema,\n KanbanWidgetSchema,\n LocaleFormatter,\n} from '@alfakit/core'\nimport {\n KanbanController,\n attachKanbanPointer,\n clientErrorNotification,\n createKanbanFiltersStore,\n kanbanActiveFilters,\n kanbanCardsIn,\n kanbanInitials,\n kanbanWipState,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport {\n CalendarIcon,\n ChevronRightIcon,\n ChevronsLeftRightIcon,\n ChevronsRightLeftIcon,\n FilterIcon,\n PlusIcon,\n SearchIcon,\n} from 'lucide-react'\nimport { Fragment, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react'\nimport { useActionRunner, useDirection, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { badgeColorClasses, colorClass, cssColor } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { ActionList } from '../actions/action-list'\nimport { FormProvider } from '../form/form-context'\nimport { showToast } from '../notifications/flash-toasts'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Card, CardTitle } from '../ui/card'\nimport { Input } from '../ui/input'\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'\nimport { Spinner } from '../ui/spinner'\n\ntype Translate = ReturnType<typeof useT>\n\n/**\n * The board of a `KanbanPage` (schema component `kanban`) or a `KanbanWidget`\n * (alfasix/alfakit-kanban). Cards move by dragging (mouse, pen, touch) or with the\n * keyboard, and every move is saved in one request.\n */\nexport function Kanban({\n component,\n widget,\n}: {\n component?: KanbanSchema\n widget?: KanbanWidgetSchema\n}) {\n const board = component ?? widget?.kanban\n\n return board ? <KanbanBoard key={board.actionComponent} board={board} /> : null\n}\n\nfunction KanbanBoard({ board }: { board: KanbanSchema }) {\n const runner = useActionRunner()\n const page = usePageActionContext()\n const panel = usePanel()\n const t = useT()\n const direction = useDirection()\n const formatter = useFormatter()\n const labelId = useId()\n const instructionsId = useId()\n const environment = (): KanbanEnvironment => ({\n runner,\n token: page.token,\n t,\n notify: (notifications) => notifications.forEach((notification) => showToast(notification)),\n error: (message) => showToast(clientErrorNotification('kanban', message)),\n visit: (url) => router.visit(url),\n })\n // One controller per board (the parent keys this view by its action component).\n const [controller] = useState(() => new KanbanController(board, environment()))\n const [filters] = useState(() =>\n createKanbanFiltersStore(board, page.token, panel.endpoints.schemaOptions),\n )\n const root = useRef<HTMLDivElement>(null)\n const cards = useRef(new Map<string, HTMLElement>())\n const [focused, setFocused] = useState<string | null>(null)\n\n useEffect(() => {\n controller.setEnvironment(environment())\n })\n\n const first = useRef(true)\n\n useEffect(() => {\n if (first.current) {\n first.current = false\n\n return\n }\n\n controller.setBoard(board)\n }, [controller, board])\n\n useEffect(\n () => (root.current ? attachKanbanPointer(root.current, controller) : undefined),\n [controller],\n )\n useEffect(() => controller.startPolling(), [controller])\n useEffect(\n () => () => {\n controller.destroy()\n filters?.destroy()\n },\n [controller, filters],\n )\n\n const state = useStore(controller.store)\n const schema = controller.schema\n const lanes = controller.lanes()\n const keys = state.columns.flatMap((column) => column.cards.map((card) => card.key))\n const current = focused !== null && keys.includes(focused) ? focused : (keys[0] ?? null)\n\n const focus = (key: string | undefined) => {\n if (!key) return\n setFocused(key)\n requestAnimationFrame(() => cards.current.get(key)?.focus())\n }\n\n // A card picked up with the keyboard moves to another list: keep the focus on it.\n const picked = state.picked\n\n useEffect(() => {\n if (picked?.via === 'keyboard') {\n const element = cards.current.get(picked.key)\n\n if (element && document.activeElement !== element) element.focus()\n }\n }, [picked])\n\n const onKeyDown = (key: string) => (event: KeyboardEvent<HTMLElement>) => {\n if (event.target !== event.currentTarget) return\n\n const result = controller.handleKey(key, event, direction)\n\n if (!result.handled) return\n\n event.preventDefault()\n\n if (result.open) controller.open(key)\n else focus(result.focus)\n }\n\n const template = state.columns\n .map((column) => (state.collapsed.has(column.key) ? '3.25rem' : 'minmax(16rem, 20rem)'))\n .join(' ')\n\n return (\n <Card\n ref={root}\n role=\"region\"\n aria-labelledby={labelId}\n className=\"gap-3 py-4\"\n data-slot=\"kanban\"\n >\n <div className=\"flex flex-wrap items-center gap-2 px-4\" data-slot=\"kanban-toolbar\">\n {schema.heading ? (\n <CardTitle id={labelId} className=\"text-base\">\n {schema.heading}\n </CardTitle>\n ) : (\n <span id={labelId} className=\"sr-only\">\n {t('kanban.label', {}, 'Kanban board')}\n </span>\n )}\n <div className=\"ms-auto flex w-full flex-wrap items-center gap-2 sm:w-auto\">\n {state.loading && (\n <Spinner className=\"size-4\" aria-label={t('kanban.loading', {}, 'Loading board')} />\n )}\n {schema.searchable && (\n <KanbanSearch controller={controller} search={state.search} t={t} />\n )}\n {filters && <KanbanFilters controller={controller} store={filters} t={t} />}\n </div>\n </div>\n <p id={instructionsId} className=\"sr-only\">\n {t(\n 'kanban.instructions',\n {},\n 'Press Space to pick up a card. While it is picked up, use the arrow keys to move it, Space to drop it and Escape to cancel.',\n )}\n </p>\n <div\n className=\"relative overflow-x-auto overscroll-x-contain px-4 pb-2\"\n data-slot=\"kanban-scroller\"\n >\n <div\n className=\"grid w-max min-w-full snap-x gap-3\"\n style={{ gridTemplateColumns: template }}\n >\n {state.columns.map((column) => (\n <ColumnHeader\n key={column.key}\n column={column}\n id={`${labelId}-column-${column.key}`}\n collapsed={state.collapsed.has(column.key)}\n creatable={Boolean(column.creatable && schema.createAction)}\n filtered={state.search !== '' || kanbanActiveFilters(state.filters) > 0}\n onToggle={() => controller.toggleColumn(column.key)}\n onCreate={() => controller.create(column.key)}\n t={t}\n />\n ))}\n {lanes.map((lane) => {\n const laneId = lane ? `${labelId}-lane-${lane.key}` : undefined\n const collapsedLane = lane !== null && state.collapsedLanes.has(lane.key)\n\n return (\n <Fragment key={lane?.key ?? '__board'}>\n {lane && (\n <LaneHeader\n lane={lane}\n id={laneId!}\n count={state.columns.reduce(\n (sum, column) => sum + kanbanCardsIn(column, lane.key).length,\n 0,\n )}\n collapsed={collapsedLane}\n onToggle={() => controller.toggleLane(lane.key)}\n t={t}\n />\n )}\n {!collapsedLane &&\n state.columns.map((column) => {\n const collapsed = state.collapsed.has(column.key)\n const items = kanbanCardsIn(column, lane?.key ?? null)\n const hidden = lane === null ? column.total - column.cards.length : 0\n\n return (\n <ul\n key={column.key}\n role=\"list\"\n aria-labelledby={[`${labelId}-column-${column.key}`, laneId]\n .filter(Boolean)\n .join(' ')}\n data-slot=\"kanban-dropzone\"\n data-column={column.key}\n data-lane={lane?.key}\n data-refused={\n state.picked && state.refused?.column === column.key ? '' : undefined\n }\n className={cn(\n 'bg-muted/40 flex min-h-24 flex-col gap-2 rounded-lg p-2',\n lane === null && 'max-h-[70vh] overflow-y-auto',\n )}\n >\n {!collapsed &&\n items.map((card) => (\n <KanbanCard\n key={card.key}\n card={card}\n board={schema}\n token={page.token}\n current={current === card.key}\n picked={state.picked?.key === card.key ? state.picked.via : undefined}\n saving={state.saving.has(card.key)}\n describedBy={instructionsId}\n formatter={formatter}\n t={t}\n setRef={(element) => {\n if (element) cards.current.set(card.key, element)\n else cards.current.delete(card.key)\n }}\n onFocus={() => setFocused(card.key)}\n onKeyDown={onKeyDown(card.key)}\n onOpen={() => controller.open(card.key)}\n />\n ))}\n {!collapsed && items.length === 0 && (\n <li\n className=\"text-muted-foreground rounded-md border border-dashed px-2 py-6 text-center text-xs\"\n data-slot=\"kanban-empty\"\n >\n {state.search !== '' || kanbanActiveFilters(state.filters) > 0\n ? t('kanban.no_results', {}, 'No cards match')\n : (column.emptyState ?? t('kanban.empty', {}, 'No cards'))}\n </li>\n )}\n {!collapsed && hidden > 0 && (\n <li className=\"text-muted-foreground px-2 text-center text-xs\">\n {t('kanban.more_cards', { count: hidden }, '+:count more')}\n </li>\n )}\n </ul>\n )\n })}\n </Fragment>\n )\n })}\n </div>\n </div>\n <div className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {state.announcement}\n </div>\n </Card>\n )\n}\n\nfunction KanbanSearch({\n controller,\n search,\n t,\n}: {\n controller: KanbanController\n search: string\n t: Translate\n}) {\n const [value, setValue] = useState(search)\n const [synced, setSynced] = useState(search)\n\n if (synced !== search) {\n setSynced(search)\n setValue(search)\n }\n\n return (\n <div className=\"relative w-full sm:w-64\">\n <SearchIcon\n className=\"text-muted-foreground pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2\"\n aria-hidden=\"true\"\n />\n <Input\n type=\"search\"\n value={value}\n className=\"h-8 ps-8\"\n placeholder={t('kanban.search_placeholder', {}, 'Search…')}\n aria-label={t('kanban.search', {}, 'Search cards')}\n onChange={(event) => {\n setValue(event.target.value)\n controller.setSearch(event.target.value)\n }}\n />\n </div>\n )\n}\n\nfunction KanbanFilters({\n controller,\n store,\n t,\n}: {\n controller: KanbanController\n store: NonNullable<ReturnType<typeof createKanbanFiltersStore>>\n t: Translate\n}) {\n const form = useStore(store.store)\n const [initial] = useState(() => ({\n components: store.state.components,\n state: store.state.values,\n columns: store.state.columns,\n }))\n const active = kanbanActiveFilters(form.values)\n\n useEffect(() => {\n if (form.dirty.size > 0) controller.setFilters(form.values)\n }, [controller, form.values, form.dirty])\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" data-slot=\"kanban-filters\">\n <FilterIcon aria-hidden=\"true\" />\n {t('kanban.filters', {}, 'Filters')}\n {active > 0 && <Badge className=\"h-5 min-w-5 px-1\">{active}</Badge>}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-80 sm:w-96\">\n <FormProvider store={store}>\n <div className=\"flex flex-col gap-4\">\n <SchemaRenderer components={form.components} columns={form.columns} className=\"gap-4\" />\n {active > 0 && (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n onClick={() => {\n store.fill(initial)\n controller.setFilters(null, 0)\n }}\n >\n {t('kanban.reset_filters', {}, 'Reset')}\n </Button>\n </div>\n )}\n </div>\n </FormProvider>\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction ColumnHeader({\n column,\n id,\n collapsed,\n creatable,\n filtered,\n onToggle,\n onCreate,\n t,\n}: {\n column: KanbanColumnSchema\n id: string\n collapsed: boolean\n creatable: boolean\n filtered: boolean\n onToggle: () => void\n onCreate: () => void\n t: Translate\n}) {\n const wip = kanbanWipState(column)\n const shown =\n column.wipLimit !== undefined ? column.count : filtered ? column.total : column.count\n const countLabel =\n column.wipLimit !== undefined\n ? t('kanban.wip', { count: column.count, limit: column.wipLimit }, ':count of :limit')\n : filtered && column.total !== column.count\n ? t(\n 'kanban.count_filtered',\n { shown: column.total, count: column.count },\n ':shown of :count cards',\n )\n : t('kanban.count', { count: column.count }, ':count cards')\n const style = column.color ? { borderTopColor: cssColor(column.color) } : undefined\n const toggleLabel = collapsed\n ? t('kanban.expand', { column: column.label }, 'Expand :column')\n : t('kanban.collapse', { column: column.label }, 'Collapse :column')\n\n if (collapsed) {\n return (\n <div\n className=\"bg-muted/60 flex snap-start flex-col items-center gap-2 rounded-lg border border-t-4 px-1 py-2\"\n style={style}\n data-slot=\"kanban-column-header\"\n data-column={column.key}\n data-collapsed=\"\"\n data-wip={wip ?? undefined}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={toggleLabel}\n aria-expanded={false}\n onClick={onToggle}\n >\n <ChevronsLeftRightIcon aria-hidden=\"true\" />\n </Button>\n <span className=\"text-muted-foreground text-xs tabular-nums\" title={countLabel}>\n {shown}\n </span>\n <h3 id={id} className=\"text-sm font-medium [writing-mode:vertical-rl]\">\n {column.label}\n </h3>\n </div>\n )\n }\n\n return (\n <div\n className=\"bg-muted/60 flex snap-start flex-col gap-1 rounded-lg border border-t-4 px-3 py-2\"\n style={style}\n data-slot=\"kanban-column-header\"\n data-column={column.key}\n data-wip={wip ?? undefined}\n >\n <div className=\"flex items-center gap-2\">\n {column.icon && <Icon name={column.icon} className=\"text-muted-foreground size-4\" />}\n <h3 id={id} className=\"min-w-0 truncate text-sm font-semibold\">\n {column.label}\n </h3>\n <Badge\n variant=\"secondary\"\n className={cn(\n 'h-5 px-1.5 tabular-nums',\n wip === 'over' && 'bg-destructive/15 text-destructive',\n wip === 'full' && 'bg-warning/15 text-warning',\n )}\n title={countLabel}\n >\n <span aria-hidden=\"true\">\n {column.wipLimit !== undefined ? `${column.count}/${column.wipLimit}` : shown}\n </span>\n <span className=\"sr-only\">{countLabel}</span>\n </Badge>\n <div className=\"ms-auto flex items-center\">\n {creatable && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('kanban.add_card', { column: column.label }, 'Add a card to :column')}\n title={t('kanban.add_card', { column: column.label }, 'Add a card to :column')}\n onClick={onCreate}\n >\n <PlusIcon aria-hidden=\"true\" />\n </Button>\n )}\n {column.collapsible && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={toggleLabel}\n title={toggleLabel}\n aria-expanded={true}\n onClick={onToggle}\n >\n <ChevronsRightLeftIcon aria-hidden=\"true\" />\n </Button>\n )}\n </div>\n </div>\n {column.description && (\n <p className=\"text-muted-foreground line-clamp-2 text-xs\">{column.description}</p>\n )}\n {wip === 'over' && (\n <p className=\"text-destructive text-xs\">\n {t('kanban.wip_over', { limit: column.wipLimit ?? 0 }, 'Over the limit of :limit cards')}\n </p>\n )}\n {wip === 'full' && column.wipStrict && (\n <p className=\"text-muted-foreground text-xs\">\n {t('kanban.wip_full', {}, 'This column is full')}\n </p>\n )}\n </div>\n )\n}\n\nfunction LaneHeader({\n lane,\n id,\n count,\n collapsed,\n onToggle,\n t,\n}: {\n lane: KanbanLaneSchema\n id: string\n count: number\n collapsed: boolean\n onToggle: () => void\n t: Translate\n}) {\n return (\n <div\n className=\"flex items-center gap-2 pt-2\"\n style={{ gridColumn: '1 / -1' }}\n data-slot=\"kanban-lane\"\n data-lane={lane.key}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-expanded={!collapsed}\n aria-label={\n collapsed\n ? t('kanban.expand_lane', { lane: lane.label }, 'Expand row :lane')\n : t('kanban.collapse_lane', { lane: lane.label }, 'Collapse row :lane')\n }\n onClick={onToggle}\n >\n <ChevronRightIcon\n className={cn(\n 'transition-transform rtl:rotate-180',\n !collapsed && 'rotate-90 rtl:rotate-90',\n )}\n aria-hidden=\"true\"\n />\n </Button>\n {lane.avatar ? (\n <Avatar className=\"size-6\">\n <AvatarImage src={lane.avatar} alt=\"\" />\n <AvatarFallback>{kanbanInitials(lane.label)}</AvatarFallback>\n </Avatar>\n ) : (\n lane.icon && <Icon name={lane.icon} className=\"text-muted-foreground size-4\" />\n )}\n <span id={id} className=\"text-sm font-semibold\">\n {lane.label}\n </span>\n <Badge variant=\"secondary\" className=\"h-5 px-1.5 tabular-nums\">\n {count}\n </Badge>\n </div>\n )\n}\n\nfunction KanbanCard({\n card,\n board,\n token,\n current,\n picked,\n saving,\n describedBy,\n formatter,\n t,\n setRef,\n onFocus,\n onKeyDown,\n onOpen,\n}: {\n card: KanbanCardSchema\n board: KanbanSchema\n token: string\n current: boolean\n picked: 'keyboard' | 'pointer' | undefined\n saving: boolean\n describedBy: string\n formatter: LocaleFormatter\n t: Translate\n setRef: (element: HTMLElement | null) => void\n onFocus: () => void\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => void\n onOpen: () => void\n}) {\n const titleId = useId()\n const opens = card.clickable !== false && Boolean(card.url || board.cardAction)\n const assignees = card.assignees ?? []\n const shownAssignees = assignees.slice(0, 3)\n\n return (\n <li\n className=\"relative\"\n data-slot=\"kanban-card\"\n data-key={card.key}\n data-movable={board.movable && card.movable !== false ? undefined : 'false'}\n data-picked={picked}\n data-saving={saving ? '' : undefined}\n >\n <div\n ref={setRef}\n role=\"button\"\n tabIndex={current ? 0 : -1}\n aria-labelledby={titleId}\n aria-describedby={describedBy}\n aria-disabled={opens ? undefined : true}\n data-slot=\"kanban-card-body\"\n className={cn(\n 'bg-card text-card-foreground focus-visible:ring-ring/50 flex touch-manipulation flex-col gap-2 overflow-hidden rounded-lg border p-3 text-start shadow-xs outline-none select-none focus-visible:ring-[3px]',\n opens && 'hover:border-ring/60 cursor-pointer',\n card.color && 'border-s-4',\n )}\n style={card.color ? { borderInlineStartColor: cssColor(card.color) } : undefined}\n onFocus={(event) => event.target === event.currentTarget && onFocus()}\n onKeyDown={onKeyDown}\n onClick={() => opens && onOpen()}\n >\n {card.coverImage && (\n <img\n src={card.coverImage}\n alt={card.coverImageAlt ?? ''}\n loading=\"lazy\"\n draggable={false}\n className=\"-mx-3 -mt-3 h-32 w-[calc(100%+1.5rem)] max-w-none object-cover\"\n />\n )}\n <span\n id={titleId}\n className={cn('text-sm leading-snug font-medium', card.actions && 'pe-7')}\n >\n {card.title}\n </span>\n {card.description && (\n <p className=\"text-muted-foreground line-clamp-3 text-xs\">{card.description}</p>\n )}\n {card.badges && card.badges.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {card.badges.map((badge, index) => (\n <Badge\n key={index}\n variant=\"outline\"\n title={badge.tooltip}\n className={colorClass(badgeColorClasses, badge.color, 'gray')}\n >\n {badge.icon && <Icon name={badge.icon} />}\n {badge.label}\n </Badge>\n ))}\n </div>\n )}\n {card.schema && card.schema.length > 0 && (\n <SchemaRenderer components={card.schema} className=\"gap-2 text-xs\" />\n )}\n {card.progress !== undefined && (\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(card.progress)}\n aria-label={t(\n 'kanban.progress',\n { percent: formatter.percent(card.progress / 100) },\n ':percent complete',\n )}\n className=\"bg-muted h-1.5 overflow-hidden rounded-full\"\n >\n <div\n className=\"bg-primary h-full rounded-full\"\n style={{ width: `${card.progress}%` }}\n />\n </div>\n )}\n {(card.dueDate || assignees.length > 0) && (\n <div className=\"flex items-center gap-2 text-xs\">\n {card.dueDate && (\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n card.overdue ? 'text-destructive font-medium' : 'text-muted-foreground',\n )}\n >\n <CalendarIcon className=\"size-3.5\" aria-hidden=\"true\" />\n <span className=\"sr-only\">\n {t('kanban.due', { date: formatter.date(card.dueDate) }, 'Due :date')}\n </span>\n <time dateTime={card.dueDate} aria-hidden=\"true\">\n {formatter.date(card.dueDate)}\n </time>\n {card.overdue && (\n <span className=\"sr-only\">{t('kanban.overdue', {}, 'Overdue')}</span>\n )}\n </span>\n )}\n {assignees.length > 0 && (\n <div className=\"ms-auto flex items-center\">\n <span className=\"sr-only\">\n {t(\n 'kanban.assignees',\n { names: formatter.list(assignees.map((assignee) => assignee.name)) },\n 'Assigned to :names',\n )}\n </span>\n <div className=\"flex -space-x-1.5 rtl:space-x-reverse\" aria-hidden=\"true\">\n {shownAssignees.map((assignee, index) => (\n <Avatar key={index} className=\"ring-card size-6 ring-2\" title={assignee.name}>\n {assignee.avatar && <AvatarImage src={assignee.avatar} alt=\"\" />}\n <AvatarFallback className=\"text-[0.625rem]\">\n {kanbanInitials(assignee.name)}\n </AvatarFallback>\n </Avatar>\n ))}\n {assignees.length > shownAssignees.length && (\n <span className=\"bg-muted ring-card flex size-6 items-center justify-center rounded-full text-[0.625rem] font-medium ring-2\">\n {t(\n 'kanban.more_assignees',\n { count: assignees.length - shownAssignees.length },\n '+:count',\n )}\n </span>\n )}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n {card.actions && card.actions.length > 0 && (\n <div className=\"absolute end-1.5 top-1.5\" data-kanban-no-drag=\"\">\n <ActionList\n actions={card.actions}\n context={{ token, component: board.actionComponent, record: card.key }}\n size=\"xs\"\n />\n </div>\n )}\n </li>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"kanban.js","names":[],"sources":["../../../src/components/kanban/kanban.tsx"],"sourcesContent":["import type {\n KanbanCardSchema,\n KanbanColumnSchema,\n KanbanEnvironment,\n KanbanLaneSchema,\n KanbanSchema,\n KanbanWidgetSchema,\n LocaleFormatter,\n} from '@alfakit/core'\nimport {\n KanbanController,\n attachKanbanPointer,\n clientErrorNotification,\n createKanbanFiltersStore,\n kanbanActiveFilters,\n kanbanCardsIn,\n kanbanInitials,\n kanbanWipState,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport {\n CalendarIcon,\n ChevronRightIcon,\n ChevronsLeftRightIcon,\n ChevronsRightLeftIcon,\n FilterIcon,\n PlusIcon,\n SearchIcon,\n} from 'lucide-react'\nimport { Fragment, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react'\nimport {\n useActionRunner,\n useChoice,\n useDirection,\n useFormatter,\n usePanel,\n useT,\n} from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { badgeColorClasses, colorClass, cssColor } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { ActionList } from '../actions/action-list'\nimport { FormProvider } from '../form/form-context'\nimport { showToast } from '../notifications/flash-toasts'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Avatar, AvatarFallback, AvatarImage } from '../ui/avatar'\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Card, CardTitle } from '../ui/card'\nimport { Input } from '../ui/input'\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'\nimport { Spinner } from '../ui/spinner'\n\ntype Translate = ReturnType<typeof useT>\n\n/**\n * The board of a `KanbanPage` (schema component `kanban`) or a `KanbanWidget`\n * (alfasix/alfakit-kanban). Cards move by dragging (mouse, pen, touch) or with the\n * keyboard, and every move is saved in one request.\n */\nexport function Kanban({\n component,\n widget,\n}: {\n component?: KanbanSchema\n widget?: KanbanWidgetSchema\n}) {\n const board = component ?? widget?.kanban\n\n return board ? <KanbanBoard key={board.actionComponent} board={board} /> : null\n}\n\nfunction KanbanBoard({ board }: { board: KanbanSchema }) {\n const runner = useActionRunner()\n const page = usePageActionContext()\n const panel = usePanel()\n const t = useT()\n const direction = useDirection()\n const formatter = useFormatter()\n const labelId = useId()\n const instructionsId = useId()\n const environment = (): KanbanEnvironment => ({\n runner,\n token: page.token,\n t,\n notify: (notifications) => notifications.forEach((notification) => showToast(notification)),\n error: (message) => showToast(clientErrorNotification('kanban', message)),\n visit: (url) => router.visit(url),\n })\n // One controller per board (the parent keys this view by its action component).\n const [controller] = useState(() => new KanbanController(board, environment()))\n const [filters] = useState(() =>\n createKanbanFiltersStore(board, page.token, panel.endpoints.schemaOptions),\n )\n const root = useRef<HTMLDivElement>(null)\n const cards = useRef(new Map<string, HTMLElement>())\n const [focused, setFocused] = useState<string | null>(null)\n\n useEffect(() => {\n controller.setEnvironment(environment())\n })\n\n const first = useRef(true)\n\n useEffect(() => {\n if (first.current) {\n first.current = false\n\n return\n }\n\n controller.setBoard(board)\n }, [controller, board])\n\n useEffect(\n () => (root.current ? attachKanbanPointer(root.current, controller) : undefined),\n [controller],\n )\n useEffect(() => controller.startPolling(), [controller])\n useEffect(\n () => () => {\n controller.destroy()\n filters?.destroy()\n },\n [controller, filters],\n )\n\n const state = useStore(controller.store)\n const schema = controller.schema\n const lanes = controller.lanes()\n const keys = state.columns.flatMap((column) => column.cards.map((card) => card.key))\n const current = focused !== null && keys.includes(focused) ? focused : (keys[0] ?? null)\n\n const focus = (key: string | undefined) => {\n if (!key) return\n setFocused(key)\n requestAnimationFrame(() => cards.current.get(key)?.focus())\n }\n\n // A card picked up with the keyboard moves to another list: keep the focus on it.\n const picked = state.picked\n\n useEffect(() => {\n if (picked?.via === 'keyboard') {\n const element = cards.current.get(picked.key)\n\n if (element && document.activeElement !== element) element.focus()\n }\n }, [picked])\n\n const onKeyDown = (key: string) => (event: KeyboardEvent<HTMLElement>) => {\n if (event.target !== event.currentTarget) return\n\n const result = controller.handleKey(key, event, direction)\n\n if (!result.handled) return\n\n event.preventDefault()\n\n if (result.open) controller.open(key)\n else focus(result.focus)\n }\n\n const template = state.columns\n .map((column) => (state.collapsed.has(column.key) ? '3.25rem' : 'minmax(16rem, 20rem)'))\n .join(' ')\n\n return (\n <Card\n ref={root}\n role=\"region\"\n aria-labelledby={labelId}\n className=\"gap-3 py-4\"\n data-slot=\"kanban\"\n >\n <div className=\"flex flex-wrap items-center gap-2 px-4\" data-slot=\"kanban-toolbar\">\n {schema.heading ? (\n <CardTitle id={labelId} className=\"text-base\">\n {schema.heading}\n </CardTitle>\n ) : (\n <span id={labelId} className=\"sr-only\">\n {t('kanban.label', {}, 'Kanban board')}\n </span>\n )}\n <div className=\"ms-auto flex w-full flex-wrap items-center gap-2 sm:w-auto\">\n {state.loading && (\n <Spinner className=\"size-4\" aria-label={t('kanban.loading', {}, 'Loading board')} />\n )}\n {schema.searchable && (\n <KanbanSearch controller={controller} search={state.search} t={t} />\n )}\n {filters && <KanbanFilters controller={controller} store={filters} t={t} />}\n </div>\n </div>\n <p id={instructionsId} className=\"sr-only\">\n {t(\n 'kanban.instructions',\n {},\n 'Press Space to pick up a card. While it is picked up, use the arrow keys to move it, Space to drop it and Escape to cancel.',\n )}\n </p>\n <div\n className=\"relative overflow-x-auto overscroll-x-contain px-4 pb-2\"\n data-slot=\"kanban-scroller\"\n >\n <div\n className=\"grid w-max min-w-full snap-x gap-3\"\n style={{ gridTemplateColumns: template }}\n >\n {state.columns.map((column) => (\n <ColumnHeader\n key={column.key}\n column={column}\n id={`${labelId}-column-${column.key}`}\n collapsed={state.collapsed.has(column.key)}\n creatable={Boolean(column.creatable && schema.createAction)}\n filtered={state.search !== '' || kanbanActiveFilters(state.filters) > 0}\n onToggle={() => controller.toggleColumn(column.key)}\n onCreate={() => controller.create(column.key)}\n t={t}\n />\n ))}\n {lanes.map((lane) => {\n const laneId = lane ? `${labelId}-lane-${lane.key}` : undefined\n const collapsedLane = lane !== null && state.collapsedLanes.has(lane.key)\n\n return (\n <Fragment key={lane?.key ?? '__board'}>\n {lane && (\n <LaneHeader\n lane={lane}\n id={laneId!}\n count={state.columns.reduce(\n (sum, column) => sum + kanbanCardsIn(column, lane.key).length,\n 0,\n )}\n collapsed={collapsedLane}\n onToggle={() => controller.toggleLane(lane.key)}\n t={t}\n />\n )}\n {!collapsedLane &&\n state.columns.map((column) => {\n const collapsed = state.collapsed.has(column.key)\n const items = kanbanCardsIn(column, lane?.key ?? null)\n const hidden = lane === null ? column.total - column.cards.length : 0\n\n return (\n <ul\n key={column.key}\n role=\"list\"\n aria-labelledby={[`${labelId}-column-${column.key}`, laneId]\n .filter(Boolean)\n .join(' ')}\n data-slot=\"kanban-dropzone\"\n data-column={column.key}\n data-lane={lane?.key}\n data-refused={\n state.picked && state.refused?.column === column.key ? '' : undefined\n }\n className={cn(\n 'bg-muted/40 flex min-h-24 flex-col gap-2 rounded-lg p-2',\n lane === null && 'max-h-[70vh] overflow-y-auto',\n )}\n >\n {!collapsed &&\n items.map((card) => (\n <KanbanCard\n key={card.key}\n card={card}\n board={schema}\n token={page.token}\n current={current === card.key}\n picked={state.picked?.key === card.key ? state.picked.via : undefined}\n saving={state.saving.has(card.key)}\n describedBy={instructionsId}\n formatter={formatter}\n t={t}\n setRef={(element) => {\n if (element) cards.current.set(card.key, element)\n else cards.current.delete(card.key)\n }}\n onFocus={() => setFocused(card.key)}\n onKeyDown={onKeyDown(card.key)}\n onOpen={() => controller.open(card.key)}\n />\n ))}\n {!collapsed && items.length === 0 && (\n <li\n className=\"text-muted-foreground rounded-md border border-dashed px-2 py-6 text-center text-xs\"\n data-slot=\"kanban-empty\"\n >\n {state.search !== '' || kanbanActiveFilters(state.filters) > 0\n ? t('kanban.no_results', {}, 'No cards match')\n : (column.emptyState ?? t('kanban.empty', {}, 'No cards'))}\n </li>\n )}\n {!collapsed && hidden > 0 && (\n <li className=\"text-muted-foreground px-2 text-center text-xs\">\n {t('kanban.more_cards', { count: hidden }, '+:count more')}\n </li>\n )}\n </ul>\n )\n })}\n </Fragment>\n )\n })}\n </div>\n </div>\n <div className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {state.announcement}\n </div>\n </Card>\n )\n}\n\nfunction KanbanSearch({\n controller,\n search,\n t,\n}: {\n controller: KanbanController\n search: string\n t: Translate\n}) {\n const [value, setValue] = useState(search)\n const [synced, setSynced] = useState(search)\n\n if (synced !== search) {\n setSynced(search)\n setValue(search)\n }\n\n return (\n <div className=\"relative w-full sm:w-64\">\n <SearchIcon\n className=\"text-muted-foreground pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2\"\n aria-hidden=\"true\"\n />\n <Input\n type=\"search\"\n value={value}\n className=\"h-8 ps-8\"\n placeholder={t('kanban.search_placeholder', {}, 'Search…')}\n aria-label={t('kanban.search', {}, 'Search cards')}\n onChange={(event) => {\n setValue(event.target.value)\n controller.setSearch(event.target.value)\n }}\n />\n </div>\n )\n}\n\nfunction KanbanFilters({\n controller,\n store,\n t,\n}: {\n controller: KanbanController\n store: NonNullable<ReturnType<typeof createKanbanFiltersStore>>\n t: Translate\n}) {\n const form = useStore(store.store)\n const [initial] = useState(() => ({\n components: store.state.components,\n state: store.state.values,\n columns: store.state.columns,\n }))\n const active = kanbanActiveFilters(form.values)\n\n useEffect(() => {\n if (form.dirty.size > 0) controller.setFilters(form.values)\n }, [controller, form.values, form.dirty])\n\n return (\n <Popover>\n <PopoverTrigger asChild>\n <Button type=\"button\" variant=\"outline\" size=\"sm\" data-slot=\"kanban-filters\">\n <FilterIcon aria-hidden=\"true\" />\n {t('kanban.filters', {}, 'Filters')}\n {active > 0 && <Badge className=\"h-5 min-w-5 px-1\">{active}</Badge>}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"end\" className=\"w-80 sm:w-96\">\n <FormProvider store={store}>\n <div className=\"flex flex-col gap-4\">\n <SchemaRenderer components={form.components} columns={form.columns} className=\"gap-4\" />\n {active > 0 && (\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n onClick={() => {\n store.fill(initial)\n controller.setFilters(null, 0)\n }}\n >\n {t('kanban.reset_filters', {}, 'Reset')}\n </Button>\n </div>\n )}\n </div>\n </FormProvider>\n </PopoverContent>\n </Popover>\n )\n}\n\nfunction ColumnHeader({\n column,\n id,\n collapsed,\n creatable,\n filtered,\n onToggle,\n onCreate,\n t,\n}: {\n column: KanbanColumnSchema\n id: string\n collapsed: boolean\n creatable: boolean\n filtered: boolean\n onToggle: () => void\n onCreate: () => void\n t: Translate\n}) {\n const choice = useChoice()\n const wip = kanbanWipState(column)\n const shown =\n column.wipLimit !== undefined ? column.count : filtered ? column.total : column.count\n const countLabel =\n column.wipLimit !== undefined\n ? t('kanban.wip', { count: column.count, limit: column.wipLimit }, ':count of :limit')\n : filtered && column.total !== column.count\n ? t(\n 'kanban.count_filtered',\n { shown: column.total, count: column.count },\n ':shown of :count cards',\n )\n : choice('kanban.count', column.count, {}, '{1} :count card|[0,*] :count cards')\n const style = column.color ? { borderTopColor: cssColor(column.color) } : undefined\n const toggleLabel = collapsed\n ? t('kanban.expand', { column: column.label }, 'Expand :column')\n : t('kanban.collapse', { column: column.label }, 'Collapse :column')\n\n if (collapsed) {\n return (\n <div\n className=\"bg-muted/60 flex snap-start flex-col items-center gap-2 rounded-lg border border-t-4 px-1 py-2\"\n style={style}\n data-slot=\"kanban-column-header\"\n data-column={column.key}\n data-collapsed=\"\"\n data-wip={wip ?? undefined}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={toggleLabel}\n aria-expanded={false}\n onClick={onToggle}\n >\n <ChevronsLeftRightIcon aria-hidden=\"true\" />\n </Button>\n <span className=\"text-muted-foreground text-xs tabular-nums\" title={countLabel}>\n {shown}\n </span>\n <h3 id={id} className=\"text-sm font-medium [writing-mode:vertical-rl]\">\n {column.label}\n </h3>\n </div>\n )\n }\n\n return (\n <div\n className=\"bg-muted/60 flex snap-start flex-col gap-1 rounded-lg border border-t-4 px-3 py-2\"\n style={style}\n data-slot=\"kanban-column-header\"\n data-column={column.key}\n data-wip={wip ?? undefined}\n >\n <div className=\"flex items-center gap-2\">\n {column.icon && <Icon name={column.icon} className=\"text-muted-foreground size-4\" />}\n <h3 id={id} className=\"min-w-0 truncate text-sm font-semibold\">\n {column.label}\n </h3>\n <Badge\n variant=\"secondary\"\n className={cn(\n 'h-5 px-1.5 tabular-nums',\n wip === 'over' && 'bg-destructive/15 text-destructive',\n wip === 'full' && 'bg-warning/15 text-warning',\n )}\n title={countLabel}\n >\n <span aria-hidden=\"true\">\n {column.wipLimit !== undefined ? `${column.count}/${column.wipLimit}` : shown}\n </span>\n <span className=\"sr-only\">{countLabel}</span>\n </Badge>\n <div className=\"ms-auto flex items-center\">\n {creatable && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('kanban.add_card', { column: column.label }, 'Add a card to :column')}\n title={t('kanban.add_card', { column: column.label }, 'Add a card to :column')}\n onClick={onCreate}\n >\n <PlusIcon aria-hidden=\"true\" />\n </Button>\n )}\n {column.collapsible && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={toggleLabel}\n title={toggleLabel}\n aria-expanded={true}\n onClick={onToggle}\n >\n <ChevronsRightLeftIcon aria-hidden=\"true\" />\n </Button>\n )}\n </div>\n </div>\n {column.description && (\n <p className=\"text-muted-foreground line-clamp-2 text-xs\">{column.description}</p>\n )}\n {wip === 'over' && (\n <p className=\"text-destructive text-xs\">\n {choice(\n 'kanban.wip_over',\n column.wipLimit ?? 0,\n { limit: column.wipLimit ?? 0 },\n '{1} Over the limit of :limit card|[0,*] Over the limit of :limit cards',\n )}\n </p>\n )}\n {wip === 'full' && column.wipStrict && (\n <p className=\"text-muted-foreground text-xs\">\n {t('kanban.wip_full', {}, 'This column is full')}\n </p>\n )}\n </div>\n )\n}\n\nfunction LaneHeader({\n lane,\n id,\n count,\n collapsed,\n onToggle,\n t,\n}: {\n lane: KanbanLaneSchema\n id: string\n count: number\n collapsed: boolean\n onToggle: () => void\n t: Translate\n}) {\n return (\n <div\n className=\"flex items-center gap-2 pt-2\"\n style={{ gridColumn: '1 / -1' }}\n data-slot=\"kanban-lane\"\n data-lane={lane.key}\n >\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-expanded={!collapsed}\n aria-label={\n collapsed\n ? t('kanban.expand_lane', { lane: lane.label }, 'Expand row :lane')\n : t('kanban.collapse_lane', { lane: lane.label }, 'Collapse row :lane')\n }\n onClick={onToggle}\n >\n <ChevronRightIcon\n className={cn(\n 'transition-transform rtl:rotate-180',\n !collapsed && 'rotate-90 rtl:rotate-90',\n )}\n aria-hidden=\"true\"\n />\n </Button>\n {lane.avatar ? (\n <Avatar className=\"size-6\">\n <AvatarImage src={lane.avatar} alt=\"\" />\n <AvatarFallback>{kanbanInitials(lane.label)}</AvatarFallback>\n </Avatar>\n ) : (\n lane.icon && <Icon name={lane.icon} className=\"text-muted-foreground size-4\" />\n )}\n <span id={id} className=\"text-sm font-semibold\">\n {lane.label}\n </span>\n <Badge variant=\"secondary\" className=\"h-5 px-1.5 tabular-nums\">\n {count}\n </Badge>\n </div>\n )\n}\n\nfunction KanbanCard({\n card,\n board,\n token,\n current,\n picked,\n saving,\n describedBy,\n formatter,\n t,\n setRef,\n onFocus,\n onKeyDown,\n onOpen,\n}: {\n card: KanbanCardSchema\n board: KanbanSchema\n token: string\n current: boolean\n picked: 'keyboard' | 'pointer' | undefined\n saving: boolean\n describedBy: string\n formatter: LocaleFormatter\n t: Translate\n setRef: (element: HTMLElement | null) => void\n onFocus: () => void\n onKeyDown: (event: KeyboardEvent<HTMLElement>) => void\n onOpen: () => void\n}) {\n const titleId = useId()\n const opens = card.clickable !== false && Boolean(card.url || board.cardAction)\n const assignees = card.assignees ?? []\n const shownAssignees = assignees.slice(0, 3)\n\n return (\n <li\n className=\"relative\"\n data-slot=\"kanban-card\"\n data-key={card.key}\n data-movable={board.movable && card.movable !== false ? undefined : 'false'}\n data-picked={picked}\n data-saving={saving ? '' : undefined}\n >\n <div\n ref={setRef}\n role=\"button\"\n tabIndex={current ? 0 : -1}\n aria-labelledby={titleId}\n aria-describedby={describedBy}\n aria-disabled={opens ? undefined : true}\n data-slot=\"kanban-card-body\"\n className={cn(\n 'bg-card text-card-foreground focus-visible:ring-ring/50 flex touch-manipulation flex-col gap-2 overflow-hidden rounded-lg border p-3 text-start shadow-xs outline-none select-none focus-visible:ring-[3px]',\n opens && 'hover:border-ring/60 cursor-pointer',\n card.color && 'border-s-4',\n )}\n style={card.color ? { borderInlineStartColor: cssColor(card.color) } : undefined}\n onFocus={(event) => event.target === event.currentTarget && onFocus()}\n onKeyDown={onKeyDown}\n onClick={() => opens && onOpen()}\n >\n {card.coverImage && (\n <img\n src={card.coverImage}\n alt={card.coverImageAlt ?? ''}\n loading=\"lazy\"\n draggable={false}\n className=\"-mx-3 -mt-3 h-32 w-[calc(100%+1.5rem)] max-w-none object-cover\"\n />\n )}\n <span\n id={titleId}\n className={cn('text-sm leading-snug font-medium', card.actions && 'pe-7')}\n >\n {card.title}\n </span>\n {card.description && (\n <p className=\"text-muted-foreground line-clamp-3 text-xs\">{card.description}</p>\n )}\n {card.badges && card.badges.length > 0 && (\n <div className=\"flex flex-wrap gap-1\">\n {card.badges.map((badge, index) => (\n <Badge\n key={index}\n variant=\"outline\"\n title={badge.tooltip}\n className={colorClass(badgeColorClasses, badge.color, 'gray')}\n >\n {badge.icon && <Icon name={badge.icon} />}\n {badge.label}\n </Badge>\n ))}\n </div>\n )}\n {card.schema && card.schema.length > 0 && (\n <SchemaRenderer components={card.schema} className=\"gap-2 text-xs\" />\n )}\n {card.progress !== undefined && (\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={Math.round(card.progress)}\n aria-label={t(\n 'kanban.progress',\n { percent: formatter.percent(card.progress / 100) },\n ':percent complete',\n )}\n className=\"bg-muted h-1.5 overflow-hidden rounded-full\"\n >\n <div\n className=\"bg-primary h-full rounded-full\"\n style={{ width: `${card.progress}%` }}\n />\n </div>\n )}\n {(card.dueDate || assignees.length > 0) && (\n <div className=\"flex items-center gap-2 text-xs\">\n {card.dueDate && (\n <span\n className={cn(\n 'inline-flex items-center gap-1',\n card.overdue ? 'text-destructive font-medium' : 'text-muted-foreground',\n )}\n >\n <CalendarIcon className=\"size-3.5\" aria-hidden=\"true\" />\n <span className=\"sr-only\">\n {t('kanban.due', { date: formatter.date(card.dueDate) }, 'Due :date')}\n </span>\n <time dateTime={card.dueDate} aria-hidden=\"true\">\n {formatter.date(card.dueDate)}\n </time>\n {card.overdue && (\n <span className=\"sr-only\">{t('kanban.overdue', {}, 'Overdue')}</span>\n )}\n </span>\n )}\n {assignees.length > 0 && (\n <div className=\"ms-auto flex items-center\">\n <span className=\"sr-only\">\n {t(\n 'kanban.assignees',\n { names: formatter.list(assignees.map((assignee) => assignee.name)) },\n 'Assigned to :names',\n )}\n </span>\n <div className=\"flex -space-x-1.5 rtl:space-x-reverse\" aria-hidden=\"true\">\n {shownAssignees.map((assignee, index) => (\n <Avatar key={index} className=\"ring-card size-6 ring-2\" title={assignee.name}>\n {assignee.avatar && <AvatarImage src={assignee.avatar} alt=\"\" />}\n <AvatarFallback className=\"text-[0.625rem]\">\n {kanbanInitials(assignee.name)}\n </AvatarFallback>\n </Avatar>\n ))}\n {assignees.length > shownAssignees.length && (\n <span className=\"bg-muted ring-card flex size-6 items-center justify-center rounded-full text-[0.625rem] font-medium ring-2\">\n {t(\n 'kanban.more_assignees',\n { count: assignees.length - shownAssignees.length },\n '+:count',\n )}\n </span>\n )}\n </div>\n </div>\n )}\n </div>\n )}\n </div>\n {card.actions && card.actions.length > 0 && (\n <div className=\"absolute end-1.5 top-1.5\" data-kanban-no-drag=\"\">\n <ActionList\n actions={card.actions}\n context={{ token, component: board.actionComponent, record: card.key }}\n size=\"xs\"\n />\n </div>\n )}\n </li>\n )\n}\n"],"mappings":"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"}
@@ -23,6 +23,7 @@ function FusedGroupComponent({ component }) {
23
23
  ...extraProps(component.extraAttributes, "flex flex-col gap-1.5"),
24
24
  children: [component.label && /* @__PURE__ */ jsx("span", {
25
25
  id: labelId,
26
+ "data-slot": "label",
26
27
  className: "text-sm leading-none font-medium select-none",
27
28
  children: component.label
28
29
  }), store ? /* @__PURE__ */ jsx(FusedGroupBody, {
@@ -1 +1 @@
1
- {"version":3,"file":"fused-group.js","names":[],"sources":["../../../src/components/layout/fused-group.tsx"],"sourcesContent":["import type { FormStore, FusedGroupSchema } from '@alfakit/core'\nimport { collectFieldStatePaths, gridColumnsClasses } from '@alfakit/core'\nimport { useId, useMemo } from 'react'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { cn } from '../../lib/utils'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { FusedGroupContext } from '../schema/fused-group-context'\nimport { RenderComponent } from '../schema/schema-renderer'\n\n/**\n * Fields fused into one control: one border around them (styled by `themes.css`), the\n * group label above, and the fields' validation errors below.\n */\nexport function FusedGroupComponent({ component }: { component: FusedGroupSchema }) {\n const labelId = `${useId()}-label`\n const store = useOptionalFormStore()\n const paths = useMemo(() => collectFieldStatePaths(component.schema), [component.schema])\n\n return (\n <div\n data-slot=\"fused-group\"\n role=\"group\"\n aria-labelledby={component.label ? labelId : undefined}\n {...extraProps(component.extraAttributes, 'flex flex-col gap-1.5')}\n >\n {component.label && (\n <span id={labelId} className=\"text-sm leading-none font-medium select-none\">\n {component.label}\n </span>\n )}\n {store ? (\n <FusedGroupBody component={component} store={store} paths={paths} />\n ) : (\n <FusedGroupControls component={component} />\n )}\n </div>\n )\n}\n\nfunction FusedGroupBody({\n component,\n store,\n paths,\n}: {\n component: FusedGroupSchema\n store: FormStore\n paths: string[]\n}) {\n // A joined string keeps the selected snapshot stable between renders.\n const joined = useStoreSelector(store.store, (state) =>\n paths\n .map((path) => state.errors[path]?.[0])\n .filter((message): message is string => Boolean(message))\n .join('\\n'),\n )\n const errors = joined === '' ? [] : joined.split('\\n')\n\n return (\n <>\n <FusedGroupControls component={component} invalid={errors.length > 0} />\n {errors.length > 0 && (\n // The fields announce their own errors; this list is the visible copy.\n <div aria-hidden=\"true\" className=\"flex flex-col gap-0.5\">\n {errors.map((message, index) => (\n <p key={index} className=\"text-destructive text-xs\">\n {message}\n </p>\n ))}\n </div>\n )}\n </>\n )\n}\n\nfunction FusedGroupControls({\n component,\n invalid = false,\n}: {\n component: FusedGroupSchema\n invalid?: boolean\n}) {\n return (\n <FusedGroupContext.Provider value={true}>\n <div\n data-slot=\"fused-group-controls\"\n data-invalid={invalid ? '' : undefined}\n className={cn('grid', gridColumnsClasses(component.columns ?? { default: 1 }))}\n >\n {(component.schema ?? []).map((child) => (\n <RenderComponent key={child.key} component={child} />\n ))}\n </div>\n </FusedGroupContext.Provider>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAgB,oBAAoB,EAAE,aAA8C;CAClF,MAAM,UAAU,GAAG,MAAM,EAAE;CAC3B,MAAM,QAAQ,qBAAqB;CACnC,MAAM,QAAQ,cAAc,uBAAuB,UAAU,MAAM,GAAG,CAAC,UAAU,MAAM,CAAC;CAExF,OACE,qBAAC,OAAD;EACE,aAAU;EACV,MAAK;EACL,mBAAiB,UAAU,QAAQ,UAAU,KAAA;EAC7C,GAAI,WAAW,UAAU,iBAAiB,uBAAuB;EAJnE,UAAA,CAMG,UAAU,SACT,oBAAC,QAAD;GAAM,IAAI;GAAS,WAAU;GAC1B,UAAA,UAAU;EACP,CAAA,GAEP,QACC,oBAAC,gBAAD;GAA2B;GAAkB;GAAc;EAAQ,CAAA,IAEnE,oBAAC,oBAAD,EAA+B,UAAY,CAAA,CAE1C;;AAET;AAEA,SAAS,eAAe,EACtB,WACA,OACA,SAKC;CAED,MAAM,SAAS,iBAAiB,MAAM,QAAQ,UAC5C,MACG,KAAK,SAAS,MAAM,OAAO,KAAK,GAAG,EAAE,CAAC,CACtC,QAAQ,YAA+B,QAAQ,OAAO,CAAC,CAAC,CACxD,KAAK,IAAI,CACd;CACA,MAAM,SAAS,WAAW,KAAK,CAAC,IAAI,OAAO,MAAM,IAAI;CAErD,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,oBAAD;EAA+B;EAAW,SAAS,OAAO,SAAS;CAAI,CAAA,GACtE,OAAO,SAAS,KAEf,oBAAC,OAAD;EAAK,eAAY;EAAO,WAAU;EAC/B,UAAA,OAAO,KAAK,SAAS,UACpB,oBAAC,KAAD;GAAe,WAAU;GACtB,UAAA;EACA,GAFK,KAEL,CACJ;CACE,CAAA,CAEP,EAAA,CAAA;AAEN;AAEA,SAAS,mBAAmB,EAC1B,WACA,UAAU,SAIT;CACD,OACE,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,oBAAC,OAAD;GACE,aAAU;GACV,gBAAc,UAAU,KAAK,KAAA;GAC7B,WAAW,GAAG,QAAQ,mBAAmB,UAAU,WAAW,EAAE,SAAS,EAAE,CAAC,CAAC;GAE3E,WAAA,UAAU,UAAU,CAAC,EAAA,CAAG,KAAK,UAC7B,oBAAC,iBAAD,EAAiC,WAAW,MAAQ,GAA9B,MAAM,GAAwB,CACrD;EACE,CAAA;CACqB,CAAA;AAEhC"}
1
+ {"version":3,"file":"fused-group.js","names":[],"sources":["../../../src/components/layout/fused-group.tsx"],"sourcesContent":["import type { FormStore, FusedGroupSchema } from '@alfakit/core'\nimport { collectFieldStatePaths, gridColumnsClasses } from '@alfakit/core'\nimport { useId, useMemo } from 'react'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { cn } from '../../lib/utils'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { FusedGroupContext } from '../schema/fused-group-context'\nimport { RenderComponent } from '../schema/schema-renderer'\n\n/**\n * Fields fused into one control: one border around them (styled by `themes.css`), the\n * group label above, and the fields' validation errors below.\n */\nexport function FusedGroupComponent({ component }: { component: FusedGroupSchema }) {\n const labelId = `${useId()}-label`\n const store = useOptionalFormStore()\n const paths = useMemo(() => collectFieldStatePaths(component.schema), [component.schema])\n\n return (\n <div\n data-slot=\"fused-group\"\n role=\"group\"\n aria-labelledby={component.label ? labelId : undefined}\n {...extraProps(component.extraAttributes, 'flex flex-col gap-1.5')}\n >\n {component.label && (\n <span\n id={labelId}\n data-slot=\"label\"\n className=\"text-sm leading-none font-medium select-none\"\n >\n {component.label}\n </span>\n )}\n {store ? (\n <FusedGroupBody component={component} store={store} paths={paths} />\n ) : (\n <FusedGroupControls component={component} />\n )}\n </div>\n )\n}\n\nfunction FusedGroupBody({\n component,\n store,\n paths,\n}: {\n component: FusedGroupSchema\n store: FormStore\n paths: string[]\n}) {\n // A joined string keeps the selected snapshot stable between renders.\n const joined = useStoreSelector(store.store, (state) =>\n paths\n .map((path) => state.errors[path]?.[0])\n .filter((message): message is string => Boolean(message))\n .join('\\n'),\n )\n const errors = joined === '' ? [] : joined.split('\\n')\n\n return (\n <>\n <FusedGroupControls component={component} invalid={errors.length > 0} />\n {errors.length > 0 && (\n // The fields announce their own errors; this list is the visible copy.\n <div aria-hidden=\"true\" className=\"flex flex-col gap-0.5\">\n {errors.map((message, index) => (\n <p key={index} className=\"text-destructive text-xs\">\n {message}\n </p>\n ))}\n </div>\n )}\n </>\n )\n}\n\nfunction FusedGroupControls({\n component,\n invalid = false,\n}: {\n component: FusedGroupSchema\n invalid?: boolean\n}) {\n return (\n <FusedGroupContext.Provider value={true}>\n <div\n data-slot=\"fused-group-controls\"\n data-invalid={invalid ? '' : undefined}\n className={cn('grid', gridColumnsClasses(component.columns ?? { default: 1 }))}\n >\n {(component.schema ?? []).map((child) => (\n <RenderComponent key={child.key} component={child} />\n ))}\n </div>\n </FusedGroupContext.Provider>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;AAcA,SAAgB,oBAAoB,EAAE,aAA8C;CAClF,MAAM,UAAU,GAAG,MAAM,EAAE;CAC3B,MAAM,QAAQ,qBAAqB;CACnC,MAAM,QAAQ,cAAc,uBAAuB,UAAU,MAAM,GAAG,CAAC,UAAU,MAAM,CAAC;CAExF,OACE,qBAAC,OAAD;EACE,aAAU;EACV,MAAK;EACL,mBAAiB,UAAU,QAAQ,UAAU,KAAA;EAC7C,GAAI,WAAW,UAAU,iBAAiB,uBAAuB;EAJnE,UAAA,CAMG,UAAU,SACT,oBAAC,QAAD;GACE,IAAI;GACJ,aAAU;GACV,WAAU;GAET,UAAA,UAAU;EACP,CAAA,GAEP,QACC,oBAAC,gBAAD;GAA2B;GAAkB;GAAc;EAAQ,CAAA,IAEnE,oBAAC,oBAAD,EAA+B,UAAY,CAAA,CAE1C;;AAET;AAEA,SAAS,eAAe,EACtB,WACA,OACA,SAKC;CAED,MAAM,SAAS,iBAAiB,MAAM,QAAQ,UAC5C,MACG,KAAK,SAAS,MAAM,OAAO,KAAK,GAAG,EAAE,CAAC,CACtC,QAAQ,YAA+B,QAAQ,OAAO,CAAC,CAAC,CACxD,KAAK,IAAI,CACd;CACA,MAAM,SAAS,WAAW,KAAK,CAAC,IAAI,OAAO,MAAM,IAAI;CAErD,OACE,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,oBAAD;EAA+B;EAAW,SAAS,OAAO,SAAS;CAAI,CAAA,GACtE,OAAO,SAAS,KAEf,oBAAC,OAAD;EAAK,eAAY;EAAO,WAAU;EAC/B,UAAA,OAAO,KAAK,SAAS,UACpB,oBAAC,KAAD;GAAe,WAAU;GACtB,UAAA;EACA,GAFK,KAEL,CACJ;CACE,CAAA,CAEP,EAAA,CAAA;AAEN;AAEA,SAAS,mBAAmB,EAC1B,WACA,UAAU,SAIT;CACD,OACE,oBAAC,kBAAkB,UAAnB;EAA4B,OAAO;EACjC,UAAA,oBAAC,OAAD;GACE,aAAU;GACV,gBAAc,UAAU,KAAK,KAAA;GAC7B,WAAW,GAAG,QAAQ,mBAAmB,UAAU,WAAW,EAAE,SAAS,EAAE,CAAC,CAAC;GAE3E,WAAA,UAAU,UAAU,CAAC,EAAA,CAAG,KAAK,UAC7B,oBAAC,iBAAD,EAAiC,WAAW,MAAQ,GAA9B,MAAM,GAAwB,CACrD;EACE,CAAA;CACqB,CAAA;AAEhC"}
@@ -49,8 +49,8 @@ function SectionComponent({ component }) {
49
49
  }),
50
50
  /* @__PURE__ */ jsxs("div", {
51
51
  className: "min-w-0 flex-1 basis-48",
52
- children: [component.heading && createElement(headingTag(component.headingLevel), { className: "text-base leading-6 font-semibold" }, component.heading), component.description && /* @__PURE__ */ jsx("p", {
53
- className: "text-muted-foreground mt-1 text-sm",
52
+ children: [component.heading && createElement(headingTag(component.headingLevel), { className: "text-base leading-6 font-semibold break-words" }, component.heading), component.description && /* @__PURE__ */ jsx("p", {
53
+ className: "text-muted-foreground mt-1 text-sm break-words",
54
54
  children: component.description
55
55
  })]
56
56
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"section.js","names":[],"sources":["../../../src/components/layout/section.tsx"],"sourcesContent":["import type { SectionSchema } from '@alfakit/core'\nimport {\n actionsAlignmentClass,\n headingTag,\n readPersisted,\n schemaGapClass,\n writePersisted,\n} from '@alfakit/core'\nimport { ChevronDownIcon } from 'lucide-react'\nimport { createElement, useState } from 'react'\nimport { useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { ActionList } from '../actions/action-list'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'\nimport { DeferredSchema } from './deferred'\nimport { MixedContent } from './mixed-content'\n\nexport function SectionComponent({ component }: { component: SectionSchema }) {\n const t = useT()\n const [choice, setChoice] = useState<boolean | null>(null)\n // Inside a form the section's actions run as field actions, so `$get` / `$set` work.\n const form = useOptionalFormStore()\n const pageContext = usePageActionContext()\n const fieldContext = useFieldActionContext(component)\n const context = form ? fieldContext : pageContext\n const persistKey = component.persistCollapsed\n const hydrated = useHydrated()\n // Read only after hydration, so the server render and hydration use the schema's default.\n const stored = hydrated && persistKey ? readPersisted(persistKey) : null\n const open = choice ?? (stored !== null ? stored !== 'collapsed' : !component.collapsed)\n const hasHeader =\n component.heading ||\n component.description ||\n component.icon ||\n component.headerActions?.length ||\n component.afterHeader?.length\n\n const setOpen = (next: boolean) => {\n setChoice(next)\n\n if (persistKey) writePersisted(persistKey, next ? 'expanded' : 'collapsed')\n }\n\n const content = (\n <DeferredSchema component={component}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n className={schemaGapClass(loaded)}\n />\n )}\n </DeferredSchema>\n )\n\n const header = hasHeader ? (\n <div\n className={cn(\n 'flex flex-wrap items-start gap-3',\n component.aside ? 'lg:col-span-1' : 'border-b px-6 py-4',\n component.compact && !component.aside && 'px-4 py-3',\n )}\n >\n {component.icon && (\n <Icon name={component.icon} className=\"text-muted-foreground mt-0.5 size-5\" />\n )}\n <div className=\"min-w-0 flex-1 basis-48\">\n {component.heading &&\n createElement(\n headingTag(component.headingLevel),\n { className: 'text-base leading-6 font-semibold' },\n component.heading,\n )}\n {component.description && (\n <p className=\"text-muted-foreground mt-1 text-sm\">{component.description}</p>\n )}\n </div>\n {component.headerActions && component.headerActions.length > 0 && (\n <ActionList actions={component.headerActions} context={context} size=\"sm\" />\n )}\n <MixedContent items={component.afterHeader} name=\"section-after-header\" />\n {component.collapsible && (\n <CollapsibleTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={t('toggle_section', {}, 'Toggle section')}\n >\n <Icon\n name=\"alfakit::schemas.section.collapse-button\"\n fallback={ChevronDownIcon}\n className={cn('transition-transform', open && 'rotate-180')}\n />\n </Button>\n </CollapsibleTrigger>\n )}\n </div>\n ) : null\n\n const hasFooterActions = Boolean(component.footerActions?.length)\n const footer =\n hasFooterActions || component.footer?.length ? (\n <div\n data-slot=\"section-footer\"\n className={cn(\n 'flex flex-wrap items-center gap-3 border-t px-6 py-4',\n component.compact && 'px-4 py-3',\n )}\n >\n {hasFooterActions && (\n <ActionList\n actions={component.footerActions ?? []}\n context={context}\n className={actionsAlignmentClass(component.footerActionsAlignment) || undefined}\n />\n )}\n <MixedContent items={component.footer} name=\"section-footer-content\" />\n </div>\n ) : null\n\n if (component.aside) {\n return (\n <Collapsible open={!component.collapsible || open} onOpenChange={setOpen} asChild>\n <section\n data-slot=\"section\"\n {...extraProps(component.extraAttributes, 'grid gap-6 lg:grid-cols-3')}\n >\n {header}\n <CollapsibleContent asChild>\n <div\n data-surface=\"section\"\n className={cn(\n 'bg-card rounded-xl border shadow-xs lg:col-span-2',\n component.secondary && 'bg-muted/40',\n )}\n >\n <div className={cn('p-6', component.compact && 'p-4')}>{content}</div>\n {footer}\n </div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n )\n }\n\n return (\n <Collapsible open={!component.collapsible || open} onOpenChange={setOpen} asChild>\n <section\n data-slot=\"section\"\n data-surface=\"section\"\n {...extraProps(\n component.extraAttributes,\n cn('bg-card rounded-xl border shadow-xs', component.secondary && 'bg-muted/40'),\n )}\n >\n {header}\n <CollapsibleContent>\n <div className={cn('p-6', component.compact && 'p-4')}>{content}</div>\n {footer}\n </CollapsibleContent>\n </section>\n </Collapsible>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"section.js","names":[],"sources":["../../../src/components/layout/section.tsx"],"sourcesContent":["import type { SectionSchema } from '@alfakit/core'\nimport {\n actionsAlignmentClass,\n headingTag,\n readPersisted,\n schemaGapClass,\n writePersisted,\n} from '@alfakit/core'\nimport { ChevronDownIcon } from 'lucide-react'\nimport { createElement, useState } from 'react'\nimport { useT } from '../../context'\nimport { useHydrated } from '../../hooks/use-hydrated'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { ActionList } from '../actions/action-list'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../ui/collapsible'\nimport { DeferredSchema } from './deferred'\nimport { MixedContent } from './mixed-content'\n\nexport function SectionComponent({ component }: { component: SectionSchema }) {\n const t = useT()\n const [choice, setChoice] = useState<boolean | null>(null)\n // Inside a form the section's actions run as field actions, so `$get` / `$set` work.\n const form = useOptionalFormStore()\n const pageContext = usePageActionContext()\n const fieldContext = useFieldActionContext(component)\n const context = form ? fieldContext : pageContext\n const persistKey = component.persistCollapsed\n const hydrated = useHydrated()\n // Read only after hydration, so the server render and hydration use the schema's default.\n const stored = hydrated && persistKey ? readPersisted(persistKey) : null\n const open = choice ?? (stored !== null ? stored !== 'collapsed' : !component.collapsed)\n const hasHeader =\n component.heading ||\n component.description ||\n component.icon ||\n component.headerActions?.length ||\n component.afterHeader?.length\n\n const setOpen = (next: boolean) => {\n setChoice(next)\n\n if (persistKey) writePersisted(persistKey, next ? 'expanded' : 'collapsed')\n }\n\n const content = (\n <DeferredSchema component={component}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n className={schemaGapClass(loaded)}\n />\n )}\n </DeferredSchema>\n )\n\n const header = hasHeader ? (\n <div\n className={cn(\n 'flex flex-wrap items-start gap-3',\n component.aside ? 'lg:col-span-1' : 'border-b px-6 py-4',\n component.compact && !component.aside && 'px-4 py-3',\n )}\n >\n {component.icon && (\n <Icon name={component.icon} className=\"text-muted-foreground mt-0.5 size-5\" />\n )}\n <div className=\"min-w-0 flex-1 basis-48\">\n {component.heading &&\n createElement(\n headingTag(component.headingLevel),\n { className: 'text-base leading-6 font-semibold break-words' },\n component.heading,\n )}\n {component.description && (\n <p className=\"text-muted-foreground mt-1 text-sm break-words\">{component.description}</p>\n )}\n </div>\n {component.headerActions && component.headerActions.length > 0 && (\n <ActionList actions={component.headerActions} context={context} size=\"sm\" />\n )}\n <MixedContent items={component.afterHeader} name=\"section-after-header\" />\n {component.collapsible && (\n <CollapsibleTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={t('toggle_section', {}, 'Toggle section')}\n >\n <Icon\n name=\"alfakit::schemas.section.collapse-button\"\n fallback={ChevronDownIcon}\n className={cn('transition-transform', open && 'rotate-180')}\n />\n </Button>\n </CollapsibleTrigger>\n )}\n </div>\n ) : null\n\n const hasFooterActions = Boolean(component.footerActions?.length)\n const footer =\n hasFooterActions || component.footer?.length ? (\n <div\n data-slot=\"section-footer\"\n className={cn(\n 'flex flex-wrap items-center gap-3 border-t px-6 py-4',\n component.compact && 'px-4 py-3',\n )}\n >\n {hasFooterActions && (\n <ActionList\n actions={component.footerActions ?? []}\n context={context}\n className={actionsAlignmentClass(component.footerActionsAlignment) || undefined}\n />\n )}\n <MixedContent items={component.footer} name=\"section-footer-content\" />\n </div>\n ) : null\n\n if (component.aside) {\n return (\n <Collapsible open={!component.collapsible || open} onOpenChange={setOpen} asChild>\n <section\n data-slot=\"section\"\n {...extraProps(component.extraAttributes, 'grid gap-6 lg:grid-cols-3')}\n >\n {header}\n <CollapsibleContent asChild>\n <div\n data-surface=\"section\"\n className={cn(\n 'bg-card rounded-xl border shadow-xs lg:col-span-2',\n component.secondary && 'bg-muted/40',\n )}\n >\n <div className={cn('p-6', component.compact && 'p-4')}>{content}</div>\n {footer}\n </div>\n </CollapsibleContent>\n </section>\n </Collapsible>\n )\n }\n\n return (\n <Collapsible open={!component.collapsible || open} onOpenChange={setOpen} asChild>\n <section\n data-slot=\"section\"\n data-surface=\"section\"\n {...extraProps(\n component.extraAttributes,\n cn('bg-card rounded-xl border shadow-xs', component.secondary && 'bg-muted/40'),\n )}\n >\n {header}\n <CollapsibleContent>\n <div className={cn('p-6', component.compact && 'p-4')}>{content}</div>\n {footer}\n </CollapsibleContent>\n </section>\n </Collapsible>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tabs.d.ts","names":[],"sources":["../../../src/components/layout/tabs.tsx"],"mappings":";;wBAcgB,gBAAgB;EAAe,WAAW;oBAAY,IAAA"}
1
+ {"version":3,"file":"tabs.d.ts","names":[],"sources":["../../../src/components/layout/tabs.tsx"],"mappings":";;wBAmCgB,gBAAgB;EAAe,WAAW;oBAAY,IAAA"}
@@ -1,37 +1,60 @@
1
+ import { useT } from "../../context.js";
1
2
  import { cn } from "../../lib/utils.js";
2
3
  import { badgeColorClasses, colorClass } from "../../lib/colors.js";
3
4
  import { Icon } from "../../lib/icons.js";
4
5
  import { Badge } from "../ui/badge.js";
6
+ import { useStoreSelector } from "../../hooks/use-store.js";
5
7
  import { extraProps } from "../../lib/attributes.js";
6
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs.js";
9
+ import { useOptionalFormStore } from "../form/form-context.js";
7
10
  import { DeferredSchema, useDeferredBadge } from "./deferred.js";
8
11
  import { SchemaRenderer } from "../schema/schema-renderer.js";
9
12
  import { useHydrated } from "../../hooks/use-hydrated.js";
10
- import { readPersisted, schemaGapClass, writePersisted } from "@alfakit/core";
13
+ import { componentHasErrors, readPersisted, schemaGapClass, tabFromQuery, tabQueryValue, tabToReveal, writePersisted } from "@alfakit/core";
11
14
  import { usePage } from "@inertiajs/react";
12
15
  import { useEffect, useState } from "react";
13
- import { jsx, jsxs } from "react/jsx-runtime";
16
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
14
17
  //#region src/components/layout/tabs.tsx
18
+ const NO_ERRORS = {};
19
+ const NO_FORM_STORE = {
20
+ get: () => ({ errors: NO_ERRORS }),
21
+ subscribe: () => () => {}
22
+ };
23
+ const TAB_INVALID_CLASSES = "text-destructive data-[state=active]:text-destructive dark:text-destructive dark:data-[state=active]:text-destructive";
15
24
  function TabsComponent({ component }) {
16
25
  const tabs = component.schema ?? [];
17
26
  const url = usePage().url;
18
- const fromQuery = component.queryStringKey ? new URL(url, "http://localhost").searchParams.get(component.queryStringKey) : null;
19
- const [initial] = useState(() => tabs.find((tab) => tab.key === fromQuery)?.key ?? tabs[Math.max(0, component.activeTab - 1)]?.key ?? tabs[0]?.key ?? "");
27
+ const fromQuery = component.queryStringKey ? tabFromQuery(tabs, new URL(url, "http://localhost").searchParams.get(component.queryStringKey)) : null;
28
+ const [initial] = useState(() => fromQuery ?? tabs[Math.max(0, component.activeTab - 1)]?.key ?? tabs[0]?.key ?? "");
20
29
  const persist = component.persistTab;
21
30
  const hydrated = useHydrated();
22
31
  const [chosen, setChosen] = useState(null);
23
32
  const stored = hydrated && persist && !fromQuery ? readPersisted(persist.key, persist.storage) : null;
24
33
  const active = chosen ?? (stored && tabs.some((tab) => tab.key === stored) ? stored : initial);
34
+ const t = useT();
35
+ const store = useOptionalFormStore();
36
+ const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors);
37
+ const [seenErrors, setSeenErrors] = useState({});
38
+ if (seenErrors !== errors) {
39
+ setSeenErrors(errors);
40
+ const reveal = tabToReveal(tabs, active, seenErrors, errors);
41
+ if (reveal !== null) setChosen(reveal);
42
+ }
25
43
  const changeTab = (value) => {
26
44
  setChosen(value);
27
45
  if (persist) writePersisted(persist.key, value, persist.storage);
28
46
  };
29
47
  useEffect(() => {
30
48
  if (!component.queryStringKey || typeof window === "undefined") return;
49
+ const tab = tabs.find((item) => item.key === active);
31
50
  const url = new URL(window.location.href);
32
- url.searchParams.set(component.queryStringKey, active);
51
+ url.searchParams.set(component.queryStringKey, tab ? tabQueryValue(tab) : active);
33
52
  window.history.replaceState(window.history.state, "", url);
34
- }, [active, component.queryStringKey]);
53
+ }, [
54
+ active,
55
+ component.queryStringKey,
56
+ tabs
57
+ ]);
35
58
  return /* @__PURE__ */ jsxs(Tabs, {
36
59
  "data-surface": component.contained ? "tabs" : void 0,
37
60
  value: active,
@@ -41,15 +64,31 @@ function TabsComponent({ component }) {
41
64
  children: [/* @__PURE__ */ jsx(TabsList, {
42
65
  "data-scrollable": component.scrollable === false ? "false" : void 0,
43
66
  className: cn(component.contained && "m-2", component.vertical ? "self-start" : component.scrollable === false ? "h-auto max-w-full flex-wrap justify-start" : "relative max-w-full overflow-x-auto"),
44
- children: tabs.map((tab) => /* @__PURE__ */ jsxs(TabsTrigger, {
45
- value: tab.key,
46
- children: [
47
- tab.icon && tab.iconPosition !== "after" && /* @__PURE__ */ jsx(Icon, { name: tab.icon }),
48
- tab.label,
49
- tab.icon && tab.iconPosition === "after" && /* @__PURE__ */ jsx(Icon, { name: tab.icon }),
50
- /* @__PURE__ */ jsx(TabBadge, { tab })
51
- ]
52
- }, tab.key))
67
+ children: tabs.map((tab) => {
68
+ const invalid = componentHasErrors(tab, errors);
69
+ return /* @__PURE__ */ jsxs(TabsTrigger, {
70
+ value: tab.key,
71
+ "data-invalid": invalid || void 0,
72
+ className: invalid ? TAB_INVALID_CLASSES : void 0,
73
+ children: [
74
+ tab.icon && tab.iconPosition !== "after" && /* @__PURE__ */ jsx(Icon, { name: tab.icon }),
75
+ tab.label,
76
+ tab.icon && tab.iconPosition === "after" && /* @__PURE__ */ jsx(Icon, { name: tab.icon }),
77
+ /* @__PURE__ */ jsx(TabBadge, { tab }),
78
+ invalid && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
79
+ "aria-hidden": "true",
80
+ className: "bg-destructive size-1.5 shrink-0 rounded-full"
81
+ }), /* @__PURE__ */ jsxs("span", {
82
+ className: "sr-only",
83
+ children: [
84
+ "(",
85
+ t("translatable.has_errors"),
86
+ ")"
87
+ ]
88
+ })] })
89
+ ]
90
+ }, tab.key);
91
+ })
53
92
  }), tabs.map((tab) => /* @__PURE__ */ jsx(TabsContent, {
54
93
  value: tab.key,
55
94
  forceMount: true,