@alfakit/react 0.1.2 → 0.1.7

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 (171) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/actions/action-modals.js +4 -0
  3. package/dist/components/actions/action-modals.js.map +1 -1
  4. package/dist/components/ai/copilot.d.ts +3 -1
  5. package/dist/components/ai/copilot.d.ts.map +1 -1
  6. package/dist/components/ai/copilot.js +4 -1
  7. package/dist/components/ai/copilot.js.map +1 -1
  8. package/dist/components/cms/live-preview.js +3 -2
  9. package/dist/components/cms/live-preview.js.map +1 -1
  10. package/dist/components/cms/page.js +1 -1
  11. package/dist/components/fields/builder.js +15 -6
  12. package/dist/components/fields/builder.js.map +1 -1
  13. package/dist/components/fields/image-editor-dialog.js +4 -2
  14. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  15. package/dist/components/fields/item-actions.js +10 -10
  16. package/dist/components/fields/item-actions.js.map +1 -1
  17. package/dist/components/fields/key-value.d.ts.map +1 -1
  18. package/dist/components/fields/key-value.js +3 -3
  19. package/dist/components/fields/key-value.js.map +1 -1
  20. package/dist/components/fields/modal-table-select.js +6 -5
  21. package/dist/components/fields/modal-table-select.js.map +1 -1
  22. package/dist/components/fields/repeater.js +2 -2
  23. package/dist/components/fields/repeater.js.map +1 -1
  24. package/dist/components/fields/rich-editor.js +2 -1
  25. package/dist/components/fields/rich-editor.js.map +1 -1
  26. package/dist/components/form/form-block.js +1 -1
  27. package/dist/components/form/form-block.js.map +1 -1
  28. package/dist/components/form/form-context.js +2 -2
  29. package/dist/components/form/form-context.js.map +1 -1
  30. package/dist/components/kanban/kanban.js +4 -3
  31. package/dist/components/kanban/kanban.js.map +1 -1
  32. package/dist/components/layout/section.js +2 -2
  33. package/dist/components/layout/section.js.map +1 -1
  34. package/dist/components/panel/app-sidebar.d.ts +2 -3
  35. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  36. package/dist/components/panel/app-sidebar.js +1 -24
  37. package/dist/components/panel/app-sidebar.js.map +1 -1
  38. package/dist/components/panel/brand.d.ts +10 -0
  39. package/dist/components/panel/brand.d.ts.map +1 -0
  40. package/dist/components/panel/brand.js +37 -0
  41. package/dist/components/panel/brand.js.map +1 -0
  42. package/dist/components/panel/index.d.ts +2 -1
  43. package/dist/components/panel/topbar.js +1 -1
  44. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  45. package/dist/components/realtime/live-records-pill.js +6 -3
  46. package/dist/components/realtime/live-records-pill.js.map +1 -1
  47. package/dist/components/table/cells.d.ts +6 -0
  48. package/dist/components/table/cells.d.ts.map +1 -1
  49. package/dist/components/table/cells.js +6 -2
  50. package/dist/components/table/cells.js.map +1 -1
  51. package/dist/components/table/column-manager.js +8 -7
  52. package/dist/components/table/column-manager.js.map +1 -1
  53. package/dist/components/table/content-layout.js +2 -1
  54. package/dist/components/table/content-layout.js.map +1 -1
  55. package/dist/components/table/pagination.js +16 -13
  56. package/dist/components/table/pagination.js.map +1 -1
  57. package/dist/components/table/table-block.js +14 -4
  58. package/dist/components/table/table-block.js.map +1 -1
  59. package/dist/components/ui/dialog.d.ts.map +1 -1
  60. package/dist/components/ui/dialog.js +3 -1
  61. package/dist/components/ui/dialog.js.map +1 -1
  62. package/dist/components/ui/popover.d.ts.map +1 -1
  63. package/dist/components/ui/popover.js +3 -1
  64. package/dist/components/ui/popover.js.map +1 -1
  65. package/dist/components/ui/sheet.d.ts.map +1 -1
  66. package/dist/components/ui/sheet.js +3 -1
  67. package/dist/components/ui/sheet.js.map +1 -1
  68. package/dist/components/ui/tooltip.js +3 -1
  69. package/dist/components/ui/tooltip.js.map +1 -1
  70. package/dist/context.d.ts +5 -0
  71. package/dist/context.d.ts.map +1 -1
  72. package/dist/context.js +5 -1
  73. package/dist/context.js.map +1 -1
  74. package/dist/create-alfakit.d.ts.map +1 -1
  75. package/dist/create-alfakit.js +8 -5
  76. package/dist/create-alfakit.js.map +1 -1
  77. package/dist/eject-manifest.json +16 -3
  78. package/dist/index.d.ts +6 -3
  79. package/dist/index.js +24 -21
  80. package/dist/layouts/app-layout.d.ts.map +1 -1
  81. package/dist/layouts/app-layout.js +2 -29
  82. package/dist/layouts/app-layout.js.map +1 -1
  83. package/dist/layouts/auth-layout.js +2 -2
  84. package/dist/layouts/auth-layout.js.map +1 -1
  85. package/dist/layouts/fullscreen-layout.js +1 -1
  86. package/dist/layouts/fullscreen-layout.js.map +1 -1
  87. package/dist/layouts/lazy-layouts.js +15 -0
  88. package/dist/layouts/lazy-layouts.js.map +1 -0
  89. package/dist/layouts/max-widths.js +34 -0
  90. package/dist/layouts/max-widths.js.map +1 -0
  91. package/dist/layouts/panel-layouts.d.ts +10 -0
  92. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  93. package/dist/layouts/panel-layouts.js +20 -0
  94. package/dist/layouts/panel-layouts.js.map +1 -0
  95. package/dist/layouts/top-nav-layout.js +2 -2
  96. package/dist/layouts/top-nav-layout.js.map +1 -1
  97. package/dist/pages/page.d.ts.map +1 -1
  98. package/dist/pages/page.js +12 -7
  99. package/dist/pages/page.js.map +1 -1
  100. package/dist/standalone/alfakit-component.js +11 -6
  101. package/dist/standalone/alfakit-component.js.map +1 -1
  102. package/dist/standalone.css +7 -0
  103. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  104. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  105. package/dist/starter-kit/dashboard-widgets.js +53 -0
  106. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  107. package/dist/starter-kit/impersonation-banner.js +34 -0
  108. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  109. package/dist/starter-kit/index.d.ts +3 -2
  110. package/dist/starter-kit/index.js +3 -2
  111. package/dist/starter-kit/starter-kit.d.ts +6 -0
  112. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  113. package/dist/starter-kit/starter-kit.js +18 -7
  114. package/dist/starter-kit/starter-kit.js.map +1 -1
  115. package/dist/starter-kit/widgets-loaded.js +20 -4
  116. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  117. package/dist/starter-kit/widgets-triggers.js +34 -3
  118. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  119. package/dist/starter-kit/widgets.d.ts +13 -5
  120. package/dist/starter-kit/widgets.d.ts.map +1 -1
  121. package/dist/starter-kit/widgets.js +56 -18
  122. package/dist/starter-kit/widgets.js.map +1 -1
  123. package/dist/styles.css +15 -2
  124. package/dist/themes.css +6 -1
  125. package/package.json +2 -2
  126. package/src/components/actions/action-modals.tsx +10 -0
  127. package/src/components/ai/copilot.tsx +14 -1
  128. package/src/components/cms/live-preview.tsx +5 -2
  129. package/src/components/fields/builder.tsx +14 -5
  130. package/src/components/fields/image-editor-dialog.tsx +7 -2
  131. package/src/components/fields/item-actions.tsx +32 -9
  132. package/src/components/fields/key-value.tsx +3 -2
  133. package/src/components/fields/modal-table-select.tsx +8 -3
  134. package/src/components/fields/repeater.tsx +2 -3
  135. package/src/components/fields/rich-editor.tsx +3 -0
  136. package/src/components/form/form-block.tsx +2 -1
  137. package/src/components/form/form-context.tsx +2 -2
  138. package/src/components/kanban/kanban.tsx +16 -3
  139. package/src/components/layout/section.tsx +2 -2
  140. package/src/components/panel/app-sidebar.tsx +2 -38
  141. package/src/components/panel/brand.tsx +46 -0
  142. package/src/components/realtime/live-records-pill.tsx +4 -3
  143. package/src/components/table/cells.tsx +9 -2
  144. package/src/components/table/column-manager.tsx +12 -10
  145. package/src/components/table/content-layout.tsx +5 -1
  146. package/src/components/table/pagination.tsx +16 -9
  147. package/src/components/table/table-block.tsx +25 -12
  148. package/src/components/ui/dialog.tsx +5 -1
  149. package/src/components/ui/popover.tsx +5 -1
  150. package/src/components/ui/sheet.tsx +5 -1
  151. package/src/components/ui/tooltip.tsx +5 -1
  152. package/src/context.tsx +10 -0
  153. package/src/create-alfakit.tsx +11 -2
  154. package/src/index.ts +2 -0
  155. package/src/layouts/app-layout.tsx +4 -28
  156. package/src/layouts/auth-layout.tsx +2 -2
  157. package/src/layouts/fullscreen-layout.tsx +1 -1
  158. package/src/layouts/lazy-layouts.ts +14 -0
  159. package/src/layouts/max-widths.ts +29 -0
  160. package/src/layouts/panel-layouts.ts +17 -0
  161. package/src/layouts/top-nav-layout.tsx +2 -2
  162. package/src/pages/page.tsx +21 -12
  163. package/src/standalone/alfakit-component.tsx +18 -7
  164. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  165. package/src/starter-kit/impersonation-banner.tsx +48 -0
  166. package/src/starter-kit/index.ts +2 -0
  167. package/src/starter-kit/starter-kit.tsx +31 -4
  168. package/src/starter-kit/widgets-loaded.tsx +22 -0
  169. package/src/starter-kit/widgets-triggers.tsx +36 -1
  170. package/src/starter-kit/widgets.tsx +95 -20
  171. package/src/styles.css +15 -2
@@ -1,8 +1,9 @@
1
1
  import { lazyComponent } from "../lib/lazy.js";
2
- import { NotificationsTrigger, SearchTrigger, TenantSwitcherTrigger } from "./widgets-triggers.js";
3
- import { starterKitWidgets } from "@alfakit/core";
2
+ import { CopilotTrigger, NotificationsTrigger, SearchTrigger, TenantSwitcherTrigger } from "./widgets-triggers.js";
3
+ import { isAlfakitPage, starterKitWidgets } from "@alfakit/core";
4
4
  import { usePage } from "@inertiajs/react";
5
- import { jsx } from "react/jsx-runtime";
5
+ import { Suspense, lazy } from "react";
6
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
6
7
  //#region src/starter-kit/widgets.tsx
7
8
  const load = () => import("./widgets-loaded.js");
8
9
  const LoadedSearch = lazyComponent(() => load().then((module) => module.LoadedSearch), (props) => /* @__PURE__ */ jsx(SearchTrigger, {
@@ -14,8 +15,26 @@ function preloadSearch() {
14
15
  }
15
16
  const LoadedNotifications = lazyComponent(() => load().then((module) => module.LoadedNotifications), (props) => /* @__PURE__ */ jsx(NotificationsTrigger, { ...props }));
16
17
  const LoadedTenantSwitcher = lazyComponent(() => load().then((module) => module.LoadedTenantSwitcher), (props) => /* @__PURE__ */ jsx(TenantSwitcherTrigger, { ...props }));
17
- function useWidgets(panelId) {
18
- return starterKitWidgets(usePage().props, panelId);
18
+ const LoadedCopilot = lazyComponent(() => load().then((module) => module.LoadedCopilot), (props) => /* @__PURE__ */ jsx(CopilotTrigger, {
19
+ ...props,
20
+ onLoad: preloadCopilot
21
+ }));
22
+ function preloadCopilot() {
23
+ LoadedCopilot.preload();
24
+ }
25
+ const LazyImpersonationBanner = lazy(() => import("./impersonation-banner.js"));
26
+ /** While impersonating, the first header component shows the banner on host pages. */
27
+ function StarterKitImpersonationBanner({ source }) {
28
+ const props = usePage().props;
29
+ const impersonation = source?.widgets.impersonation;
30
+ if (!impersonation || isAlfakitPage(props)) return null;
31
+ return /* @__PURE__ */ jsx(Suspense, {
32
+ fallback: null,
33
+ children: /* @__PURE__ */ jsx(LazyImpersonationBanner, { impersonation })
34
+ });
35
+ }
36
+ function useWidgets(panelId, part) {
37
+ return starterKitWidgets(usePage().props, panelId, part);
19
38
  }
20
39
  /**
21
40
  * AlfaKit's command palette and global search: the search field opens it, and so do the
@@ -23,13 +42,14 @@ function useWidgets(panelId) {
23
42
  * nothing for guests or without global search and a command palette.
24
43
  */
25
44
  function AlfakitSearch({ panel } = {}) {
26
- const source = useWidgets(panel);
27
- if (!source?.widgets.search) return null;
28
- return /* @__PURE__ */ jsx("span", {
45
+ const source = useWidgets(panel, "search");
46
+ const banner = /* @__PURE__ */ jsx(StarterKitImpersonationBanner, { source });
47
+ if (!source?.widgets.search) return banner;
48
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [banner, /* @__PURE__ */ jsx("span", {
29
49
  "data-slot": "starter-kit-search",
30
50
  className: "inline-flex",
31
51
  children: /* @__PURE__ */ jsx(LoadedSearch, { source })
32
- });
52
+ })] });
33
53
  }
34
54
  /**
35
55
  * The bell with the user's database notifications (`databaseNotifications()`), polling and
@@ -37,28 +57,46 @@ function AlfakitSearch({ panel } = {}) {
37
57
  * notifications.
38
58
  */
39
59
  function AlfakitNotifications({ panel } = {}) {
40
- const source = useWidgets(panel);
41
- if (!source?.widgets.notifications) return null;
42
- return /* @__PURE__ */ jsx("span", {
60
+ const source = useWidgets(panel, "notifications");
61
+ const banner = /* @__PURE__ */ jsx(StarterKitImpersonationBanner, { source });
62
+ if (!source?.widgets.notifications) return banner;
63
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [banner, /* @__PURE__ */ jsx("span", {
43
64
  "data-slot": "starter-kit-notifications",
44
65
  className: "inline-flex",
45
66
  children: /* @__PURE__ */ jsx(LoadedNotifications, { source })
46
- });
67
+ })] });
47
68
  }
48
69
  /**
49
70
  * The tenant switcher of a tenancy panel: the current tenant inside the panel, or a menu
50
71
  * to choose one on the host's own pages. Renders nothing for guests or other panels.
51
72
  */
52
73
  function AlfakitTenantSwitcher({ panel } = {}) {
53
- const source = useWidgets(panel);
54
- if (!source?.widgets.tenancy) return null;
55
- return /* @__PURE__ */ jsx("span", {
74
+ const source = useWidgets(panel, "tenancy");
75
+ const banner = /* @__PURE__ */ jsx(StarterKitImpersonationBanner, { source });
76
+ if (!source?.widgets.tenancy) return banner;
77
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [banner, /* @__PURE__ */ jsx("span", {
56
78
  "data-slot": "starter-kit-tenant-switcher",
57
79
  className: "inline-flex",
58
80
  children: /* @__PURE__ */ jsx(LoadedTenantSwitcher, { source })
59
- });
81
+ })] });
82
+ }
83
+ /**
84
+ * The AI copilot of alfasix/alfakit-ai (`AiPlugin::copilot()`): its button and chat
85
+ * slide-over, opened with its key bindings (`mod+j`) from any page. Renders nothing for
86
+ * guests, without the plugin's copilot, for users who may not use it and (tenancy panels)
87
+ * outside a tenant.
88
+ */
89
+ function AlfakitCopilot({ panel } = {}) {
90
+ const source = useWidgets(panel, "copilot");
91
+ const banner = /* @__PURE__ */ jsx(StarterKitImpersonationBanner, { source });
92
+ if (!source?.widgets.copilot) return banner;
93
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [banner, /* @__PURE__ */ jsx("span", {
94
+ "data-slot": "starter-kit-copilot",
95
+ className: "inline-flex",
96
+ children: /* @__PURE__ */ jsx(LoadedCopilot, { source })
97
+ })] });
60
98
  }
61
99
  //#endregion
62
- export { AlfakitNotifications, AlfakitSearch, AlfakitTenantSwitcher };
100
+ export { AlfakitCopilot, AlfakitNotifications, AlfakitSearch, AlfakitTenantSwitcher };
63
101
 
64
102
  //# sourceMappingURL=widgets.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"widgets.js","names":[],"sources":["../../src/starter-kit/widgets.tsx"],"sourcesContent":["import { starterKitWidgets, type StarterKitWidgets } from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { lazyComponent } from '../lib/lazy'\nimport {\n NotificationsTrigger,\n SearchTrigger,\n TenantSwitcherTrigger,\n type WidgetProps,\n} from './widgets-triggers'\n\n/**\n * Drop-in header components of starter kit mode (`Panel::starterKit()`): AlfaKit's search,\n * notifications bell and tenant switcher in the host's own header, on every page of the\n * application. They read the shared `alfakitNavigation` prop, render a lightweight trigger\n * straight away, and load their code and the panel data the first time they render\n * (in the browser, never during SSR).\n *\n * ```tsx\n * import { AlfakitNotifications, AlfakitSearch } from '@alfakit/react'\n *\n * <AlfakitSearch />\n * <AlfakitNotifications />\n * ```\n */\nexport interface AlfakitHeaderWidgetProps {\n /** The `starterKit()` panel to use (the first one by default). */\n panel?: string\n}\n\nconst load = () => import('./widgets-loaded')\n\nconst LoadedSearch = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedSearch),\n (props) => <SearchTrigger {...props} onLoad={preloadSearch} />,\n)\n\nfunction preloadSearch() {\n void LoadedSearch.preload()\n}\n\nconst LoadedNotifications = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedNotifications),\n (props) => <NotificationsTrigger {...props} />,\n)\n\nconst LoadedTenantSwitcher = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedTenantSwitcher),\n (props) => <TenantSwitcherTrigger {...props} />,\n)\n\nfunction useWidgets(panelId: string | undefined): StarterKitWidgets | null {\n return starterKitWidgets(usePage().props, panelId)\n}\n\n/**\n * AlfaKit's command palette and global search: the search field opens it, and so do the\n * panel's key bindings (⌘K / Ctrl+K by default), registered once for the page. Renders\n * nothing for guests or without global search and a command palette.\n */\nexport function AlfakitSearch({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel)\n\n if (!source?.widgets.search) return null\n\n return (\n <span data-slot=\"starter-kit-search\" className=\"inline-flex\">\n <LoadedSearch source={source} />\n </span>\n )\n}\n\n/**\n * The bell with the user's database notifications (`databaseNotifications()`), polling and\n * realtime updates as in the panel. Renders nothing for guests or without database\n * notifications.\n */\nexport function AlfakitNotifications({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel)\n\n if (!source?.widgets.notifications) return null\n\n return (\n <span data-slot=\"starter-kit-notifications\" className=\"inline-flex\">\n <LoadedNotifications source={source} />\n </span>\n )\n}\n\n/**\n * The tenant switcher of a tenancy panel: the current tenant inside the panel, or a menu\n * to choose one on the host's own pages. Renders nothing for guests or other panels.\n */\nexport function AlfakitTenantSwitcher({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel)\n\n if (!source?.widgets.tenancy) return null\n\n return (\n <span data-slot=\"starter-kit-tenant-switcher\" className=\"inline-flex\">\n <LoadedTenantSwitcher source={source} />\n </span>\n )\n}\n"],"mappings":";;;;;;AA6BA,MAAM,aAAa,OAAO;AAE1B,MAAM,eAAe,oBACb,KAAK,CAAC,CAAC,MAAM,WAAW,OAAO,YAAY,IAChD,UAAU,oBAAC,eAAD;CAAe,GAAI;CAAO,QAAQ;AAAgB,CAAA,CAC/D;AAEA,SAAS,gBAAgB;CACvB,aAAkB,QAAQ;AAC5B;AAEA,MAAM,sBAAsB,oBACpB,KAAK,CAAC,CAAC,MAAM,WAAW,OAAO,mBAAmB,IACvD,UAAU,oBAAC,sBAAD,EAAsB,GAAI,MAAQ,CAAA,CAC/C;AAEA,MAAM,uBAAuB,oBACrB,KAAK,CAAC,CAAC,MAAM,WAAW,OAAO,oBAAoB,IACxD,UAAU,oBAAC,uBAAD,EAAuB,GAAI,MAAQ,CAAA,CAChD;AAEA,SAAS,WAAW,SAAuD;CACzE,OAAO,kBAAkB,QAAQ,CAAC,CAAC,OAAO,OAAO;AACnD;;;;;;AAOA,SAAgB,cAAc,EAAE,UAAoC,CAAC,GAAG;CACtE,MAAM,SAAS,WAAW,KAAK;CAE/B,IAAI,CAAC,QAAQ,QAAQ,QAAQ,OAAO;CAEpC,OACE,oBAAC,QAAD;EAAM,aAAU;EAAqB,WAAU;EAC7C,UAAA,oBAAC,cAAD,EAAsB,OAAS,CAAA;CAC3B,CAAA;AAEV;;;;;;AAOA,SAAgB,qBAAqB,EAAE,UAAoC,CAAC,GAAG;CAC7E,MAAM,SAAS,WAAW,KAAK;CAE/B,IAAI,CAAC,QAAQ,QAAQ,eAAe,OAAO;CAE3C,OACE,oBAAC,QAAD;EAAM,aAAU;EAA4B,WAAU;EACpD,UAAA,oBAAC,qBAAD,EAA6B,OAAS,CAAA;CAClC,CAAA;AAEV;;;;;AAMA,SAAgB,sBAAsB,EAAE,UAAoC,CAAC,GAAG;CAC9E,MAAM,SAAS,WAAW,KAAK;CAE/B,IAAI,CAAC,QAAQ,QAAQ,SAAS,OAAO;CAErC,OACE,oBAAC,QAAD;EAAM,aAAU;EAA8B,WAAU;EACtD,UAAA,oBAAC,sBAAD,EAA8B,OAAS,CAAA;CACnC,CAAA;AAEV"}
1
+ {"version":3,"file":"widgets.js","names":[],"sources":["../../src/starter-kit/widgets.tsx"],"sourcesContent":["import {\n isAlfakitPage,\n starterKitWidgets,\n type StarterKitWidgets,\n type StarterKitWidgetsSchema,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { lazy, Suspense } from 'react'\nimport { lazyComponent } from '../lib/lazy'\nimport {\n CopilotTrigger,\n NotificationsTrigger,\n SearchTrigger,\n TenantSwitcherTrigger,\n type WidgetProps,\n} from './widgets-triggers'\n\n/**\n * Drop-in header components of starter kit mode (`Panel::starterKit()`): AlfaKit's search,\n * notifications bell and tenant switcher in the host's own header, on every page of the\n * application. They read the shared `alfakitNavigation` prop, render a lightweight trigger\n * straight away, and load their code and the panel data the first time they render\n * (in the browser, never during SSR).\n *\n * ```tsx\n * import { AlfakitCopilot, AlfakitNotifications, AlfakitSearch } from '@alfakit/react'\n *\n * <AlfakitSearch />\n * <AlfakitNotifications />\n * <AlfakitCopilot />\n * ```\n */\nexport interface AlfakitHeaderWidgetProps {\n /** The `starterKit()` panel to use (by default the open AlfaKit page's panel, else the first one that has this component). */\n panel?: string\n}\n\nconst load = () => import('./widgets-loaded')\n\nconst LoadedSearch = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedSearch),\n (props) => <SearchTrigger {...props} onLoad={preloadSearch} />,\n)\n\nfunction preloadSearch() {\n void LoadedSearch.preload()\n}\n\nconst LoadedNotifications = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedNotifications),\n (props) => <NotificationsTrigger {...props} />,\n)\n\nconst LoadedTenantSwitcher = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedTenantSwitcher),\n (props) => <TenantSwitcherTrigger {...props} />,\n)\n\nconst LoadedCopilot = lazyComponent<WidgetProps>(\n () => load().then((module) => module.LoadedCopilot),\n (props) => <CopilotTrigger {...props} onLoad={preloadCopilot} />,\n)\n\nfunction preloadCopilot() {\n void LoadedCopilot.preload()\n}\n\n// Only needed while impersonating: its code loads then (and never on AlfaKit pages).\nconst LazyImpersonationBanner = lazy(() => import('./impersonation-banner'))\n\n/** While impersonating, the first header component shows the banner on host pages. */\nfunction StarterKitImpersonationBanner({ source }: { source: StarterKitWidgets | null }) {\n const props = usePage().props\n const impersonation = source?.widgets.impersonation\n\n if (!impersonation || isAlfakitPage(props)) return null\n\n return (\n <Suspense fallback={null}>\n <LazyImpersonationBanner impersonation={impersonation} />\n </Suspense>\n )\n}\n\nfunction useWidgets(\n panelId: string | undefined,\n part: keyof StarterKitWidgetsSchema,\n): StarterKitWidgets | null {\n return starterKitWidgets(usePage().props, panelId, part)\n}\n\n/**\n * AlfaKit's command palette and global search: the search field opens it, and so do the\n * panel's key bindings (⌘K / Ctrl+K by default), registered once for the page. Renders\n * nothing for guests or without global search and a command palette.\n */\nexport function AlfakitSearch({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel, 'search')\n\n const banner = <StarterKitImpersonationBanner source={source} />\n\n if (!source?.widgets.search) return banner\n\n return (\n <>\n {banner}\n <span data-slot=\"starter-kit-search\" className=\"inline-flex\">\n <LoadedSearch source={source} />\n </span>\n </>\n )\n}\n\n/**\n * The bell with the user's database notifications (`databaseNotifications()`), polling and\n * realtime updates as in the panel. Renders nothing for guests or without database\n * notifications.\n */\nexport function AlfakitNotifications({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel, 'notifications')\n\n const banner = <StarterKitImpersonationBanner source={source} />\n\n if (!source?.widgets.notifications) return banner\n\n return (\n <>\n {banner}\n <span data-slot=\"starter-kit-notifications\" className=\"inline-flex\">\n <LoadedNotifications source={source} />\n </span>\n </>\n )\n}\n\n/**\n * The tenant switcher of a tenancy panel: the current tenant inside the panel, or a menu\n * to choose one on the host's own pages. Renders nothing for guests or other panels.\n */\nexport function AlfakitTenantSwitcher({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel, 'tenancy')\n\n const banner = <StarterKitImpersonationBanner source={source} />\n\n if (!source?.widgets.tenancy) return banner\n\n return (\n <>\n {banner}\n <span data-slot=\"starter-kit-tenant-switcher\" className=\"inline-flex\">\n <LoadedTenantSwitcher source={source} />\n </span>\n </>\n )\n}\n\n/**\n * The AI copilot of alfasix/alfakit-ai (`AiPlugin::copilot()`): its button and chat\n * slide-over, opened with its key bindings (`mod+j`) from any page. Renders nothing for\n * guests, without the plugin's copilot, for users who may not use it and (tenancy panels)\n * outside a tenant.\n */\nexport function AlfakitCopilot({ panel }: AlfakitHeaderWidgetProps = {}) {\n const source = useWidgets(panel, 'copilot')\n\n const banner = <StarterKitImpersonationBanner source={source} />\n\n if (!source?.widgets.copilot) return banner\n\n return (\n <>\n {banner}\n <span data-slot=\"starter-kit-copilot\" className=\"inline-flex\">\n <LoadedCopilot source={source} />\n </span>\n </>\n )\n}\n"],"mappings":"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"}
package/dist/styles.css CHANGED
@@ -11,8 +11,21 @@
11
11
  /* Built-in themes (alfakit, minimal, soft, corporate, filament); neutral is below. */
12
12
  @import './themes.css';
13
13
 
14
+ /*
15
+ * Toast styles, as in the Vue renderer. Sonner also injects them at runtime, which a
16
+ * nonce-based Content Security Policy blocks; from the stylesheet they always apply.
17
+ */
18
+ @import 'sonner/dist/styles.css';
19
+
14
20
  @custom-variant dark (&:is(.dark *));
15
21
 
22
+ /*
23
+ * Class lists that live in @alfakit/core (stacked-on-mobile tables, filter and column manager
24
+ * widths in `table.ts`, …): scan core's build too. Relative to this file in
25
+ * node_modules/@alfakit/<renderer>/dist, next to node_modules/@alfakit/core.
26
+ */
27
+ @source '../../core/dist';
28
+
16
29
  /*
17
30
  * Grid classes are composed at runtime from schema breakpoints (`columns`, `columnSpan`),
18
31
  * so force Tailwind to generate every variant the renderers can produce.
@@ -116,10 +129,10 @@
116
129
  --secondary: oklch(0.97 0 0);
117
130
  --secondary-foreground: oklch(0.205 0 0);
118
131
  --muted: oklch(0.97 0 0);
119
- --muted-foreground: oklch(0.556 0 0);
132
+ --muted-foreground: oklch(0.54 0 0);
120
133
  --accent: oklch(0.97 0 0);
121
134
  --accent-foreground: oklch(0.205 0 0);
122
- --destructive: var(--alfakit-danger, oklch(0.55 0.245 27.325));
135
+ --destructive: var(--alfakit-danger, oklch(0.5 0.245 27.325));
123
136
  --destructive-foreground: var(--alfakit-danger-foreground, oklch(0.985 0 0));
124
137
  --border: oklch(0.922 0 0);
125
138
  --input: oklch(0.64 0 0);
package/dist/themes.css CHANGED
@@ -287,7 +287,7 @@
287
287
  --secondary: oklch(0.967 0.003 264.542);
288
288
  --secondary-foreground: oklch(0.21 0.034 264.665);
289
289
  --muted: oklch(0.967 0.003 264.542);
290
- --muted-foreground: oklch(0.551 0.027 264.364);
290
+ --muted-foreground: oklch(0.535 0.027 264.364);
291
291
  --accent: oklch(0.967 0.003 264.542);
292
292
  --accent-foreground: oklch(0.21 0.034 264.665);
293
293
  --border: oklch(0.928 0.006 264.531);
@@ -678,6 +678,11 @@
678
678
  color: var(--primary);
679
679
  }
680
680
 
681
+ /* The schema inspector's round button (dev tools) sits above the bar, not on its last item. */
682
+ :root[data-mobile='bottom'] [data-slot='dev-tools-toggle'] {
683
+ bottom: calc(var(--alfakit-bottom-nav-height) + env(safe-area-inset-bottom, 0px) + 1rem);
684
+ }
685
+
681
686
  /* Room for the bar, sticky actions sit right above it. */
682
687
  :root[data-mobile='bottom'] [data-slot='content'] {
683
688
  padding-bottom: calc(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfakit/react",
3
- "version": "0.1.2",
3
+ "version": "0.1.7",
4
4
  "description": "React + shadcn/ui renderer for Alfakit, the Laravel admin panel builder by Alfa Six.",
5
5
  "license": "MIT",
6
6
  "author": "Alfa Six (https://alfasixbh.com)",
@@ -96,7 +96,7 @@
96
96
  "react-day-picker": "^10.0.2",
97
97
  "sonner": "^2.0.8",
98
98
  "tailwind-merge": "^3.7.0",
99
- "@alfakit/core": "0.1.2"
99
+ "@alfakit/core": "0.1.7"
100
100
  },
101
101
  "peerDependencies": {
102
102
  "@inertiajs/react": "^3.0.0",
@@ -222,6 +222,16 @@ function ActionModal({ modal, hidden = false }: { modal: OpenModal; hidden?: boo
222
222
 
223
223
  const submit = (createAnother = false) => {
224
224
  submittedRef.current = true
225
+
226
+ // A click that blurred a live field started a refresh: send the state it returns.
227
+ if (form) {
228
+ form.whenSettled(() => run(createAnother))
229
+ } else {
230
+ run(createAnother)
231
+ }
232
+ }
233
+
234
+ const run = (createAnother: boolean) => {
225
235
  const values = form?.state.values ?? {}
226
236
  const options = {
227
237
  ...(schema.passwordConfirmation ? { password } : {}),
@@ -29,7 +29,14 @@ export function useCopilotState(controller: CopilotController): CopilotState {
29
29
  * a chat slide-over. Questions, tools and actions run on the server; proposed actions
30
30
  * run only when the user confirms them in the chat.
31
31
  */
32
- export function AiCopilot({ component }: { component: AiCopilotSchema }) {
32
+ export function AiCopilot({
33
+ component,
34
+ autoOpen = false,
35
+ }: {
36
+ component: AiCopilotSchema
37
+ /** Open once mounted (starter kit header: clicked or `mod+j` before its code loaded). */
38
+ autoOpen?: boolean
39
+ }) {
33
40
  const t = useT()
34
41
  // One controller per panel: Inertia visits pass new props objects.
35
42
  // eslint-disable-next-line react-hooks/exhaustive-deps
@@ -42,6 +49,12 @@ export function AiCopilot({ component }: { component: AiCopilotSchema }) {
42
49
  // The panel mounts the first time the copilot opens (button or shortcut), then stays.
43
50
  useEffect(() => controller.subscribe((next) => next.open && setMounted(true)), [controller])
44
51
 
52
+ useEffect(() => {
53
+ if (autoOpen && component.canUse !== false) controller.open()
54
+ // Only when it mounts.
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ }, [controller])
57
+
45
58
  useEffect(() => {
46
59
  const bindings = JSON.parse(keys) as string[]
47
60
 
@@ -78,6 +78,9 @@ export function CmsLivePreview({ component }: { component: CmsLivePreviewSchema
78
78
  const autosaveRef = useRef<CmsAutosave | null>(null)
79
79
  const status =
80
80
  snapshot.hasChanges && component.changedStatus ? component.changedStatus : component.status
81
+ // "Publish changes" appears as soon as an autosave makes the draft differ.
82
+ const actions =
83
+ snapshot.hasChanges && component.changedActions ? component.changedActions : component.actions
81
84
 
82
85
  // Measured in the browser after mounting: stick right under the panel's topbar when it
83
86
  // sticks too, and open the pane beside the form on wide screens (the viewer's last
@@ -220,9 +223,9 @@ export function CmsLivePreview({ component }: { component: CmsLivePreviewSchema
220
223
  : t('cms.live_preview', {}, 'Live preview')}
221
224
  </Button>
222
225
  )}
223
- {component.actions && component.actions.length > 0 && (
226
+ {actions && actions.length > 0 && (
224
227
  <div onPointerDownCapture={() => void autosaveRef.current?.flush()}>
225
- <ActionList actions={component.actions} context={actionContext} size="sm" />
228
+ <ActionList actions={actions} context={actionContext} size="sm" />
226
229
  </div>
227
230
  )}
228
231
  </div>
@@ -89,7 +89,7 @@ function BuilderFieldContent({ component }: { component: BuilderSchema }) {
89
89
  const context = useFieldActionContext(component)
90
90
  const runner = useActionRunner()
91
91
  const runItemAction = useRunItemAction(context)
92
- const fallbacks = useItemActionFallbacks(component.addActionLabel)
92
+ const fallbacks = useItemActionFallbacks(component.addActionLabel, 'builder')
93
93
  const resolve = useItemActions(component, fallbacks)
94
94
  // Long lists render only the visible blocks (plus a few around them).
95
95
  const virtual = useVirtualList(
@@ -142,6 +142,13 @@ function BuilderFieldContent({ component }: { component: BuilderSchema }) {
142
142
  }
143
143
 
144
144
  const blockOf = (type: string) => component.blocks.find((block) => block.name === type)
145
+ // Summaries are made on the server: collapsing items whose block has one brings them up
146
+ // to date with the edits.
147
+ const refreshSummaries = (indexes: number[]) => {
148
+ if (indexes.some((index) => blockOf(items[index]?.type ?? '')?.summary)) {
149
+ void form?.refresh(component.statePath)
150
+ }
151
+ }
145
152
  const underMax = component.maxItems == null || items.length < component.maxItems
146
153
  const canAdd = component.addable && !locked && underMax
147
154
  const canDelete =
@@ -197,9 +204,10 @@ function BuilderFieldContent({ component }: { component: BuilderSchema }) {
197
204
  <CollapseAllActions
198
205
  context={context}
199
206
  resolve={(name) => resolve(name)}
200
- onCollapseAll={() =>
207
+ onCollapseAll={() => {
201
208
  setCollapsed(Object.fromEntries(items.map((_, index) => [index, true])))
202
- }
209
+ refreshSummaries(items.map((_, index) => index))
210
+ }}
203
211
  onExpandAll={() =>
204
212
  setCollapsed(Object.fromEntries(items.map((_, index) => [index, false])))
205
213
  }
@@ -317,9 +325,10 @@ function BuilderFieldContent({ component }: { component: BuilderSchema }) {
317
325
  onMove={moveWithButton}
318
326
  onClone={() => cloneItem(index)}
319
327
  onDelete={() => removeItem(index)}
320
- onToggle={() =>
328
+ onToggle={() => {
321
329
  setCollapsed((state) => ({ ...state, [index]: !isCollapsed }))
322
- }
330
+ if (!isCollapsed) refreshSummaries([index])
331
+ }}
323
332
  >
324
333
  {hasPreview && !locked && (
325
334
  <Button
@@ -4,7 +4,7 @@ import type {
4
4
  ImageEditorData,
5
5
  ImageEditorDragMode,
6
6
  } from '@alfakit/core'
7
- import { ImageEditor, loadEditorImage } from '@alfakit/core'
7
+ import { ImageEditor, imageEditorOptionsKey, loadEditorImage } from '@alfakit/core'
8
8
  import {
9
9
  FlipHorizontal2Icon,
10
10
  FlipVertical2Icon,
@@ -63,9 +63,14 @@ export function ImageEditorDialog({
63
63
  const [saving, setSaving] = useState(false)
64
64
  const hintId = useId()
65
65
 
66
+ // Restart only for another image or other settings: the schema object itself is new on
67
+ // every render of a template item (a new builder block), which kept restarting the load.
68
+ const optionsKey = imageEditorOptionsKey(options)
69
+
66
70
  useEffect(() => {
67
71
  if (!request) return
68
72
 
73
+ const options = JSON.parse(optionsKey) as FileUploadImageEditorSchema
69
74
  let cancelled = false
70
75
 
71
76
  loadEditorImage(request.file)
@@ -97,7 +102,7 @@ export function ImageEditorDialog({
97
102
  return null
98
103
  })
99
104
  }
100
- }, [request, options])
105
+ }, [request, optionsKey])
101
106
 
102
107
  const finish = (file: File | null) => {
103
108
  request?.resolve(file)
@@ -12,6 +12,7 @@ import {
12
12
  ItemMoveTracker,
13
13
  addActionJustify,
14
14
  itemActionContext,
15
+ itemActionIcon,
15
16
  resolveItemAction,
16
17
  withItemActionState,
17
18
  } from '@alfakit/core'
@@ -154,19 +155,41 @@ export function useRunItemAction(context: ActionContext) {
154
155
  }
155
156
  }
156
157
 
157
- /** Default looks of the built-in actions, for payloads without `itemActions`. */
158
- export function useItemActionFallbacks(addLabel = ''): Record<ItemActionName, ItemActionFallback> {
158
+ /** Default looks of the built-in actions, for payloads without `itemActions` (icons follow their `alfakit::` aliases). */
159
+ export function useItemActionFallbacks(
160
+ addLabel = '',
161
+ kind: 'repeater' | 'builder' = 'repeater',
162
+ ): Record<ItemActionName, ItemActionFallback> {
159
163
  const t = useT()
160
164
 
161
165
  return {
162
166
  add: { label: addLabel, icon: 'plus', variant: 'outline' },
163
- delete: { label: t('form.remove_item', {}, 'Remove'), icon: 'trash-2', color: 'danger' },
164
- clone: { label: t('form.clone', {}, 'Clone'), icon: 'copy' },
165
- reorder: { label: t('form.reorder_item', {}, 'Reorder'), icon: 'grip-vertical' },
166
- moveUp: { label: t('form.move_up', {}, 'Move up'), icon: 'chevron-up' },
167
- moveDown: { label: t('form.move_down', {}, 'Move down'), icon: 'chevron-down' },
168
- collapse: { label: t('form.collapse_item', {}, 'Collapse'), icon: 'chevron-up' },
169
- expand: { label: t('form.expand_item', {}, 'Expand'), icon: 'chevron-down' },
167
+ delete: {
168
+ label: t('form.remove_item', {}, 'Remove'),
169
+ icon: itemActionIcon(kind, 'delete', 'trash-2'),
170
+ color: 'danger',
171
+ },
172
+ clone: { label: t('form.clone', {}, 'Clone'), icon: itemActionIcon(kind, 'clone', 'copy') },
173
+ reorder: {
174
+ label: t('form.reorder_item', {}, 'Reorder'),
175
+ icon: itemActionIcon(kind, 'reorder', 'grip-vertical'),
176
+ },
177
+ moveUp: {
178
+ label: t('form.move_up', {}, 'Move up'),
179
+ icon: itemActionIcon(kind, 'move-up', 'chevron-up'),
180
+ },
181
+ moveDown: {
182
+ label: t('form.move_down', {}, 'Move down'),
183
+ icon: itemActionIcon(kind, 'move-down', 'chevron-down'),
184
+ },
185
+ collapse: {
186
+ label: t('form.collapse_item', {}, 'Collapse'),
187
+ icon: itemActionIcon(kind, 'collapse', 'chevron-up'),
188
+ },
189
+ expand: {
190
+ label: t('form.expand_item', {}, 'Expand'),
191
+ icon: itemActionIcon(kind, 'expand', 'chevron-down'),
192
+ },
170
193
  collapseAll: {
171
194
  label: t('form.collapse_all', {}, 'Collapse all'),
172
195
  icon: 'chevrons-down-up',
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  fieldIds,
3
+ itemActionIcon,
3
4
  moveRepeaterItem,
4
5
  reorderedItemKeys,
5
6
  resolveItemAction,
@@ -51,11 +52,11 @@ export function KeyValueField({ component }: { component: KeyValueSchema }) {
51
52
  })
52
53
  const deleteAction = resolveItemAction('delete', component, undefined, {
53
54
  label: t('form.remove_row', {}, 'Remove row'),
54
- icon: 'trash-2',
55
+ icon: itemActionIcon('key-value', 'delete', 'trash-2'),
55
56
  })
56
57
  const reorderAction = resolveItemAction('reorder', component, undefined, {
57
58
  label: t('form.reorder_item', {}, 'Reorder'),
58
- icon: 'grip-vertical',
59
+ icon: itemActionIcon('key-value', 'reorder', 'grip-vertical'),
59
60
  })
60
61
 
61
62
  const update = (next: Row[]) => setValue(next)
@@ -20,7 +20,7 @@ import {
20
20
  XIcon,
21
21
  } from 'lucide-react'
22
22
  import { useEffect, useMemo, useRef, useState } from 'react'
23
- import { useChoice, useT } from '../../context'
23
+ import { useChoice, useFormatter, useT } from '../../context'
24
24
  import { useStore } from '../../hooks/use-store'
25
25
  import { Icon } from '../../lib/icons'
26
26
  import { cn } from '../../lib/utils'
@@ -135,6 +135,7 @@ interface PickerProps {
135
135
 
136
136
  function ModalTablePicker({ component, initial, onClose, onConfirm }: PickerProps) {
137
137
  const t = useT()
138
+ const format = useFormatter()
138
139
  const choice = useChoice()
139
140
  const form = useFormStore()
140
141
  const multiple = Boolean(component.multiple)
@@ -399,8 +400,12 @@ function ModalTablePicker({ component, initial, onClose, onConfirm }: PickerProp
399
400
  {paging.total !== undefined
400
401
  ? t(
401
402
  'table.showing',
402
- { first: paging.from, last: paging.to, total: paging.total },
403
- `Showing ${paging.from}–${paging.to} of ${paging.total}`,
403
+ {
404
+ first: format.number(paging.from),
405
+ last: format.number(paging.to),
406
+ total: format.number(paging.total),
407
+ },
408
+ 'Showing :first–:last of :total',
404
409
  )
405
410
  : `${paging.from}–${paging.to}`}
406
411
  </span>
@@ -13,7 +13,7 @@ import {
13
13
  removeIndexedState,
14
14
  removeRepeaterItem,
15
15
  reorderedItemKeys,
16
- replacePlaceholder,
16
+ templateForIndex,
17
17
  virtualizeOptions,
18
18
  } from '@alfakit/core'
19
19
  import { useState } from 'react'
@@ -125,8 +125,7 @@ function RepeaterFieldContent({ component }: { component: RepeaterSchema }) {
125
125
  )
126
126
 
127
127
  const schemaFor = (index: number): SchemaComponent[] =>
128
- component.items[index]?.schema ??
129
- replacePlaceholder(component.template, '__INDEX__', String(index))
128
+ component.items[index]?.schema ?? templateForIndex(component.template, index)
130
129
 
131
130
  const canAdd =
132
131
  component.addable &&
@@ -9,6 +9,7 @@ import {
9
9
  attachToolbarKeyboard,
10
10
  commandLabel,
11
11
  detailsNodeConfigs,
12
+ documentNonce,
12
13
  EditorAttachments,
13
14
  fieldIds,
14
15
  fileAttachmentError,
@@ -403,6 +404,8 @@ function RichEditorInput({
403
404
  useEffect(() => () => attachments.destroy(), [attachments])
404
405
 
405
406
  const editor = useEditor({
407
+ // TipTap injects its base CSS; with the page's CSP nonce a nonce-based policy allows it.
408
+ injectNonce: documentNonce(),
406
409
  extensions: [
407
410
  StarterKit.configure({ link: false }),
408
411
  Link.configure(linkExtensionOptions(component.linkProtocols)),
@@ -128,7 +128,8 @@ export function FormBlock({ block }: { block: FormBlockSchema }) {
128
128
  submitting.current = true
129
129
  submittedBlock.current = latestBlock.current
130
130
  store.setProcessing(true)
131
- runner.execute(action, ctx, store.state.values)
131
+ // A click that blurred a live field started a refresh: send the state it returns.
132
+ store.whenSettled(() => runner.execute(action, ctx, store.state.values))
132
133
  },
133
134
  [runner, store],
134
135
  )
@@ -1,5 +1,5 @@
1
1
  import type { BaseField, FieldAriaAttributes, FormState, FormStore } from '@alfakit/core'
2
- import { fieldAria, fieldDomId } from '@alfakit/core'
2
+ import { fieldAria, fieldDomId, fieldError } from '@alfakit/core'
3
3
  import { createContext, useCallback, useContext, useMemo, type ReactNode } from 'react'
4
4
  import { useStore, useStoreSelector } from '../../hooks/use-store'
5
5
 
@@ -60,7 +60,7 @@ export function useField<T = unknown>(field: BaseField): FieldBinding<T> {
60
60
  return current as T
61
61
  })
62
62
 
63
- const error = useStoreSelector(store.store, (state) => state.errors[path]?.[0])
63
+ const error = useStoreSelector(store.store, (state) => fieldError(state.errors, path))
64
64
 
65
65
  const setValue = useCallback((next: T) => store.setValue(path, next), [store, path])
66
66
  const blur = useCallback(() => store.blur(path), [store, path])
@@ -28,7 +28,14 @@ import {
28
28
  SearchIcon,
29
29
  } from 'lucide-react'
30
30
  import { Fragment, useEffect, useId, useRef, useState, type KeyboardEvent } from 'react'
31
- import { useActionRunner, useDirection, useFormatter, usePanel, useT } from '../../context'
31
+ import {
32
+ useActionRunner,
33
+ useChoice,
34
+ useDirection,
35
+ useFormatter,
36
+ usePanel,
37
+ useT,
38
+ } from '../../context'
32
39
  import { useStore } from '../../hooks/use-store'
33
40
  import { badgeColorClasses, colorClass, cssColor } from '../../lib/colors'
34
41
  import { Icon } from '../../lib/icons'
@@ -424,6 +431,7 @@ function ColumnHeader({
424
431
  onCreate: () => void
425
432
  t: Translate
426
433
  }) {
434
+ const choice = useChoice()
427
435
  const wip = kanbanWipState(column)
428
436
  const shown =
429
437
  column.wipLimit !== undefined ? column.count : filtered ? column.total : column.count
@@ -436,7 +444,7 @@ function ColumnHeader({
436
444
  { shown: column.total, count: column.count },
437
445
  ':shown of :count cards',
438
446
  )
439
- : t('kanban.count', { count: column.count }, ':count cards')
447
+ : choice('kanban.count', column.count, {}, '{1} :count card|[0,*] :count cards')
440
448
  const style = column.color ? { borderTopColor: cssColor(column.color) } : undefined
441
449
  const toggleLabel = collapsed
442
450
  ? t('kanban.expand', { column: column.label }, 'Expand :column')
@@ -532,7 +540,12 @@ function ColumnHeader({
532
540
  )}
533
541
  {wip === 'over' && (
534
542
  <p className="text-destructive text-xs">
535
- {t('kanban.wip_over', { limit: column.wipLimit ?? 0 }, 'Over the limit of :limit cards')}
543
+ {choice(
544
+ 'kanban.wip_over',
545
+ column.wipLimit ?? 0,
546
+ { limit: column.wipLimit ?? 0 },
547
+ '{1} Over the limit of :limit card|[0,*] Over the limit of :limit cards',
548
+ )}
536
549
  </p>
537
550
  )}
538
551
  {wip === 'full' && column.wipStrict && (
@@ -76,11 +76,11 @@ export function SectionComponent({ component }: { component: SectionSchema }) {
76
76
  {component.heading &&
77
77
  createElement(
78
78
  headingTag(component.headingLevel),
79
- { className: 'text-base leading-6 font-semibold' },
79
+ { className: 'text-base leading-6 font-semibold break-words' },
80
80
  component.heading,
81
81
  )}
82
82
  {component.description && (
83
- <p className="text-muted-foreground mt-1 text-sm">{component.description}</p>
83
+ <p className="text-muted-foreground mt-1 text-sm break-words">{component.description}</p>
84
84
  )}
85
85
  </div>
86
86
  {component.headerActions && component.headerActions.length > 0 && (