@alfakit/react 0.1.9 → 0.1.11

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 (107) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/actions/action-button.js +13 -1
  3. package/dist/components/actions/action-button.js.map +1 -1
  4. package/dist/components/dev-tools/dev-tools-inspector.js +3 -3
  5. package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
  6. package/dist/components/fields/checkbox-list.js +2 -2
  7. package/dist/components/fields/checkbox-list.js.map +1 -1
  8. package/dist/components/fields/money-input.d.ts +5 -0
  9. package/dist/components/fields/money-input.d.ts.map +1 -1
  10. package/dist/components/fields/money-input.js +2 -5
  11. package/dist/components/fields/money-input.js.map +1 -1
  12. package/dist/components/fields/rich-editor-extensions.js +14 -4
  13. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  14. package/dist/components/file-manager/file-manager.js +2 -0
  15. package/dist/components/file-manager/file-manager.js.map +1 -1
  16. package/dist/components/layout/elements.d.ts.map +1 -1
  17. package/dist/components/layout/elements.js +16 -5
  18. package/dist/components/layout/elements.js.map +1 -1
  19. package/dist/components/layout/mixed-content.d.ts +6 -4
  20. package/dist/components/layout/mixed-content.d.ts.map +1 -1
  21. package/dist/components/layout/mixed-content.js +6 -4
  22. package/dist/components/layout/mixed-content.js.map +1 -1
  23. package/dist/components/layout/section.js +4 -2
  24. package/dist/components/layout/section.js.map +1 -1
  25. package/dist/components/notifications/notifications-menu.d.ts.map +1 -1
  26. package/dist/components/notifications/notifications-menu.js +45 -17
  27. package/dist/components/notifications/notifications-menu.js.map +1 -1
  28. package/dist/components/panel/lazy-placeholder.d.ts +1 -0
  29. package/dist/components/panel/lazy-placeholder.d.ts.map +1 -1
  30. package/dist/components/panel/lazy-placeholder.js +38 -4
  31. package/dist/components/panel/lazy-placeholder.js.map +1 -1
  32. package/dist/components/panel/shortcuts-overlay.js +13 -10
  33. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  34. package/dist/components/table/cells.js +1 -1
  35. package/dist/components/table/cells.js.map +1 -1
  36. package/dist/components/table/column-manager.js +4 -4
  37. package/dist/components/table/column-manager.js.map +1 -1
  38. package/dist/components/table/spreadsheet.js +8 -1
  39. package/dist/components/table/spreadsheet.js.map +1 -1
  40. package/dist/components/translation-manager/translation-manager.js +1 -1
  41. package/dist/components/translation-manager/translation-manager.js.map +1 -1
  42. package/dist/components/ui/calendar.d.ts.map +1 -1
  43. package/dist/components/ui/calendar.js +7 -0
  44. package/dist/components/ui/calendar.js.map +1 -1
  45. package/dist/components/widgets/dashboard-customizer.js +2 -2
  46. package/dist/components/widgets/dashboard-customizer.js.map +1 -1
  47. package/dist/components/widgets/stats-overview.js +3 -3
  48. package/dist/components/widgets/stats-overview.js.map +1 -1
  49. package/dist/components/widgets/widgets-block.js +2 -2
  50. package/dist/components/widgets/widgets-block.js.map +1 -1
  51. package/dist/create-alfakit.d.ts.map +1 -1
  52. package/dist/create-alfakit.js +3 -2
  53. package/dist/create-alfakit.js.map +1 -1
  54. package/dist/eject-manifest.json +4 -2
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +2 -2
  57. package/dist/pages/page.d.ts.map +1 -1
  58. package/dist/pages/page.js +4 -7
  59. package/dist/pages/page.js.map +1 -1
  60. package/dist/standalone/alfakit-component.d.ts +0 -10
  61. package/dist/standalone/alfakit-component.d.ts.map +1 -1
  62. package/dist/standalone/alfakit-component.js +5 -4
  63. package/dist/standalone/alfakit-component.js.map +1 -1
  64. package/dist/standalone/runtime.js +2 -1
  65. package/dist/standalone/runtime.js.map +1 -1
  66. package/dist/standalone.css +23 -1
  67. package/dist/starter-kit/index.d.ts +2 -2
  68. package/dist/starter-kit/index.js +2 -2
  69. package/dist/starter-kit/navigation.d.ts +6 -1
  70. package/dist/starter-kit/navigation.d.ts.map +1 -1
  71. package/dist/starter-kit/navigation.js +3 -2
  72. package/dist/starter-kit/navigation.js.map +1 -1
  73. package/dist/starter-kit/starter-kit.d.ts +9 -0
  74. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  75. package/dist/starter-kit/starter-kit.js +22 -3
  76. package/dist/starter-kit/starter-kit.js.map +1 -1
  77. package/dist/styles.css +2 -1
  78. package/dist/themes.css +4 -0
  79. package/package.json +2 -2
  80. package/src/components/actions/action-button.tsx +15 -1
  81. package/src/components/dev-tools/dev-tools-inspector.tsx +7 -2
  82. package/src/components/fields/checkbox-list.tsx +2 -2
  83. package/src/components/fields/money-input.tsx +3 -14
  84. package/src/components/fields/rich-editor-extensions.tsx +24 -2
  85. package/src/components/file-manager/file-manager.tsx +3 -1
  86. package/src/components/layout/elements.tsx +27 -4
  87. package/src/components/layout/mixed-content.tsx +8 -4
  88. package/src/components/layout/section.tsx +2 -2
  89. package/src/components/notifications/notifications-menu.tsx +42 -1
  90. package/src/components/panel/lazy-placeholder.tsx +41 -1
  91. package/src/components/panel/shortcuts-overlay.tsx +29 -24
  92. package/src/components/table/cells.tsx +3 -1
  93. package/src/components/table/column-manager.tsx +6 -4
  94. package/src/components/table/spreadsheet.tsx +18 -7
  95. package/src/components/translation-manager/translation-manager.tsx +1 -1
  96. package/src/components/ui/calendar.tsx +8 -0
  97. package/src/components/widgets/dashboard-customizer.tsx +2 -2
  98. package/src/components/widgets/stats-overview.tsx +6 -3
  99. package/src/components/widgets/widgets-block.tsx +2 -2
  100. package/src/create-alfakit.tsx +9 -1
  101. package/src/pages/page.tsx +7 -3
  102. package/src/standalone/alfakit-component.tsx +9 -4
  103. package/src/standalone/runtime.ts +3 -0
  104. package/src/starter-kit/index.ts +1 -0
  105. package/src/starter-kit/navigation.ts +12 -1
  106. package/src/starter-kit/starter-kit.tsx +25 -2
  107. package/src/styles.css +2 -1
@@ -1,10 +1,15 @@
1
- import { StarterKitNavGroup, StarterKitNavigationSchema } from "@alfakit/core";
1
+ import { StarterKitNavGroup, StarterKitNavigationSchema, StarterKitUserMenuItem } from "@alfakit/core";
2
2
  //#region src/starter-kit/navigation.d.ts
3
3
  export interface AlfakitNavigation {
4
4
  /** The panel's navigation as the server shared it, or null for guests. */
5
5
  navigation: StarterKitNavigationSchema | null;
6
6
  /** Its groups as plain items (`title`, `href`, `icon`, `isActive`, `badge`, `children`). */
7
7
  groups: StarterKitNavGroup[];
8
+ /**
9
+ * The panel's user menu links (`userMenuItems()`, Billing's entry…) for the kit's user
10
+ * menu: `<AlfakitUserMenuItems />` renders them with the app's dropdown items.
11
+ */
12
+ userMenuItems: StarterKitUserMenuItem[];
8
13
  }
9
14
  /**
10
15
  * The navigation of a `starterKit()` panel (the first one unless `panelId` is given), for the
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.d.ts","names":[],"sources":["../../src/starter-kit/navigation.ts"],"mappings":";;iBASiB;;EAEf,YAAY;;EAEZ,QAAQ;;;;;;;;;;;wBAYM,qBAAqB,mBAAmB"}
1
+ {"version":3,"file":"navigation.d.ts","names":[],"sources":["../../src/starter-kit/navigation.ts"],"mappings":";;iBAWiB;;EAEf,YAAY;;EAEZ,QAAQ;;;;;EAKR,eAAe;;;;;;;;;;;wBAYD,qBAAqB,mBAAmB"}
@@ -1,4 +1,4 @@
1
- import { starterKitNavigation, toStarterKitNavGroups } from "@alfakit/core";
1
+ import { starterKitNavigation, toStarterKitNavGroups, toStarterKitUserMenuItems } from "@alfakit/core";
2
2
  import { usePage } from "@inertiajs/react";
3
3
  import { useMemo } from "react";
4
4
  //#region src/starter-kit/navigation.ts
@@ -16,7 +16,8 @@ function useAlfakitNavigation(panelId) {
16
16
  const navigation = starterKitNavigation(props, panelId);
17
17
  return useMemo(() => ({
18
18
  navigation,
19
- groups: toStarterKitNavGroups(navigation?.groups)
19
+ groups: toStarterKitNavGroups(navigation?.groups),
20
+ userMenuItems: toStarterKitUserMenuItems(navigation?.userMenu)
20
21
  }), [navigation]);
21
22
  }
22
23
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.js","names":[],"sources":["../../src/starter-kit/navigation.ts"],"sourcesContent":["import {\n starterKitNavigation,\n toStarterKitNavGroups,\n type StarterKitNavGroup,\n type StarterKitNavigationSchema,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useMemo } from 'react'\n\nexport interface AlfakitNavigation {\n /** The panel's navigation as the server shared it, or null for guests. */\n navigation: StarterKitNavigationSchema | null\n /** Its groups as plain items (`title`, `href`, `icon`, `isActive`, `badge`, `children`). */\n groups: StarterKitNavGroup[]\n}\n\n/**\n * The navigation of a `starterKit()` panel (the first one unless `panelId` is given), for the\n * host's sidebar. Every Inertia page of the application carries it, so the host's own pages\n * list the panel's resources too.\n *\n * ```tsx\n * const { groups } = useAlfakitNavigation()\n * ```\n */\nexport function useAlfakitNavigation(panelId?: string): AlfakitNavigation {\n const props = usePage().props\n const navigation = starterKitNavigation(props, panelId)\n\n return useMemo(\n () => ({ navigation, groups: toStarterKitNavGroups(navigation?.groups) }),\n [navigation],\n )\n}\n"],"mappings":";;;;;;;;;;;;;AAyBA,SAAgB,qBAAqB,SAAqC;CACxE,MAAM,QAAQ,QAAQ,CAAC,CAAC;CACxB,MAAM,aAAa,qBAAqB,OAAO,OAAO;CAEtD,OAAO,eACE;EAAE;EAAY,QAAQ,sBAAsB,YAAY,MAAM;CAAE,IACvE,CAAC,UAAU,CACb;AACF"}
1
+ {"version":3,"file":"navigation.js","names":[],"sources":["../../src/starter-kit/navigation.ts"],"sourcesContent":["import {\n starterKitNavigation,\n toStarterKitNavGroups,\n toStarterKitUserMenuItems,\n type StarterKitNavGroup,\n type StarterKitNavigationSchema,\n type StarterKitUserMenuItem,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useMemo } from 'react'\n\nexport interface AlfakitNavigation {\n /** The panel's navigation as the server shared it, or null for guests. */\n navigation: StarterKitNavigationSchema | null\n /** Its groups as plain items (`title`, `href`, `icon`, `isActive`, `badge`, `children`). */\n groups: StarterKitNavGroup[]\n /**\n * The panel's user menu links (`userMenuItems()`, Billing's entry…) for the kit's user\n * menu: `<AlfakitUserMenuItems />` renders them with the app's dropdown items.\n */\n userMenuItems: StarterKitUserMenuItem[]\n}\n\n/**\n * The navigation of a `starterKit()` panel (the first one unless `panelId` is given), for the\n * host's sidebar. Every Inertia page of the application carries it, so the host's own pages\n * list the panel's resources too.\n *\n * ```tsx\n * const { groups } = useAlfakitNavigation()\n * ```\n */\nexport function useAlfakitNavigation(panelId?: string): AlfakitNavigation {\n const props = usePage().props\n const navigation = starterKitNavigation(props, panelId)\n\n return useMemo(\n () => ({\n navigation,\n groups: toStarterKitNavGroups(navigation?.groups),\n userMenuItems: toStarterKitUserMenuItems(navigation?.userMenu),\n }),\n [navigation],\n )\n}\n"],"mappings":";;;;;;;;;;;;;AAgCA,SAAgB,qBAAqB,SAAqC;CACxE,MAAM,QAAQ,QAAQ,CAAC,CAAC;CACxB,MAAM,aAAa,qBAAqB,OAAO,OAAO;CAEtD,OAAO,eACE;EACL;EACA,QAAQ,sBAAsB,YAAY,MAAM;EAChD,eAAe,0BAA0B,YAAY,QAAQ;CAC/D,IACA,CAAC,UAAU,CACb;AACF"}
@@ -28,6 +28,15 @@ export interface StarterKitPageOptions extends Pick<AlfakitOptions<AnyComponent>
28
28
  * `createStarterKitPage({ components: { 'layout:app': MyContent } })`.
29
29
  */
30
30
  export declare function StarterKitContentLayout({ children, maxContentWidth }: AppLayoutProps): import("react").JSX.Element;
31
+ /**
32
+ * A `$layout = 'fullscreen'` page inside the host layout: what `layout:fullscreen` renders in
33
+ * starter kit mode. The host layout is the chrome, so there is no AlfaKit header (brand, top
34
+ * bar slots, user menu, Exit); the screen gets the whole content area. Replace it with
35
+ * `{ components: { 'layout:fullscreen': MyScreen } }`.
36
+ */
37
+ export declare function StarterKitFullscreenLayout({ children }: {
38
+ children: ReactNode;
39
+ }): import("react").JSX.Element;
31
40
  /**
32
41
  * The Inertia layout around every Alfakit page in starter kit mode, inside the host layout:
33
42
  * Alfakit's context, modals, toasts and the reading direction. It stays mounted while the
@@ -1 +1 @@
1
- {"version":3,"file":"starter-kit.d.ts","names":[],"sources":["../../src/starter-kit/starter-kit.tsx"],"mappings":";;;;;;YAsCY,mBAAmB;EAC7B,cAAc;EACd,UAAU;;iBAGK,8BAA8B,KAC7C,eAAe;;;;;;EAQf,SAAS;;;;;;EAMT,WAAW,eAAe;;;;;;;wBAQZ,0BAA0B,UAAU,mBAAmB,iCAAc,IAAA;;;;;;wBAuErE,wBACd,SACA;EAEA,UAAU;EACV,WAAW;oBACZ,IAAA;;;;;;;;;;;;;;wBAmEe,qBAAqB,UAAS;;EAYL,OAAA,OAAA,qCAAuB"}
1
+ {"version":3,"file":"starter-kit.d.ts","names":[],"sources":["../../src/starter-kit/starter-kit.tsx"],"mappings":";;;;;;YAsCY,mBAAmB;EAC7B,cAAc;EACd,UAAU;;iBAGK,8BAA8B,KAC7C,eAAe;;;;;;EAQf,SAAS;;;;;;EAMT,WAAW,eAAe;;;;;;;wBAQZ,0BAA0B,UAAU,mBAAmB,iCAAc,IAAA;;;;;;;wBA2BrE,6BAA6B;EAAc,UAAU;oBAAW,IAAA;;;;;;wBAkEhE,wBACd,SACA;EAEA,UAAU;EACV,WAAW;oBACZ,IAAA;;;;;;;;;;;;;;wBAoEe,qBAAqB,UAAS;;EAYL,OAAA,OAAA,qCAAuB"}
@@ -35,10 +35,29 @@ function StarterKitContentLayout({ children, maxContentWidth }) {
35
35
  ]
36
36
  });
37
37
  }
38
+ /**
39
+ * A `$layout = 'fullscreen'` page inside the host layout: what `layout:fullscreen` renders in
40
+ * starter kit mode. The host layout is the chrome, so there is no AlfaKit header (brand, top
41
+ * bar slots, user menu, Exit); the screen gets the whole content area. Replace it with
42
+ * `{ components: { 'layout:fullscreen': MyScreen } }`.
43
+ */
44
+ function StarterKitFullscreenLayout({ children }) {
45
+ return /* @__PURE__ */ jsxs("div", {
46
+ "data-slot": "content",
47
+ "data-layout": "fullscreen",
48
+ "data-alfakit-embedded": "",
49
+ className: "flex min-h-0 w-full min-w-0 flex-1 flex-col",
50
+ children: [
51
+ /* @__PURE__ */ jsx(Slot, { name: "content.start" }),
52
+ children,
53
+ /* @__PURE__ */ jsx(Slot, { name: "content.end" })
54
+ ]
55
+ });
56
+ }
38
57
  let browserContext = null;
39
58
  function createStarterKitContext(options) {
40
59
  const discovered = discoverComponents(options.discover);
41
- const registry = createDefaultRegistry().registerMany(lazyPanelLayouts).register("layout:auth", AuthLayout).register("layout:app", StarterKitContentLayout).registerMany(discovered.components).registerMany(options.components ?? {});
60
+ const registry = createDefaultRegistry().registerMany(lazyPanelLayouts).register("layout:auth", AuthLayout).register("layout:app", StarterKitContentLayout).register("layout:fullscreen", StarterKitFullscreenLayout).registerMany(discovered.components).registerMany(options.components ?? {});
42
61
  const translator = new Translator();
43
62
  return {
44
63
  registry,
@@ -100,7 +119,7 @@ function AlfakitStarterKitRoot({ options, children }) {
100
119
  toasterId: STARTER_KIT_TOASTER_ID,
101
120
  children: [children, /* @__PURE__ */ jsx("div", {
102
121
  "data-slot": "starter-kit-debug-panel",
103
- className: "fixed end-16 bottom-4 z-40",
122
+ className: "fixed right-16 bottom-16 z-40",
104
123
  children: /* @__PURE__ */ jsx(DebugPanelSlot, {})
105
124
  })]
106
125
  })
@@ -131,6 +150,6 @@ function createStarterKitPage(options = {}) {
131
150
  return AlfakitStarterKitPage;
132
151
  }
133
152
  //#endregion
134
- export { AlfakitStarterKitRoot, StarterKitContentLayout, createStarterKitPage };
153
+ export { AlfakitStarterKitRoot, StarterKitContentLayout, StarterKitFullscreenLayout, createStarterKitPage };
135
154
 
136
155
  //# sourceMappingURL=starter-kit.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"starter-kit.js","names":[],"sources":["../../src/starter-kit/starter-kit.tsx"],"sourcesContent":["import {\n ActionRunner,\n STARTER_KIT_TOASTER_ID,\n Translator,\n configureCharts,\n installStarterKitRuntime,\n registerRichEditorExtensions,\n resolveRequestErrorNotification,\n starterKitBreadcrumbsFromProps,\n syncStarterKitPage,\n usesStarterKitHostLayout,\n type AlfakitOptions,\n type AlfakitPageProps,\n type StarterKitBreadcrumb,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useEffect, useState, type ComponentType, type ReactNode } from 'react'\nimport { setToasterId, showToast } from '../components/notifications/flash-toasts'\nimport { DebugPanelSlot } from '../components/panel/debug-panel-slot'\nimport { Slot } from '../components/panel/slot'\nimport {\n AlfakitProvider,\n useAlfakitProps,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { AlfakitRoot } from '../create-alfakit'\nimport type { AppLayoutProps } from '../layouts/app-layout'\nimport { AuthLayout } from '../layouts/auth-layout'\nimport { lazyPanelLayouts } from '../layouts/lazy-layouts'\nimport { MAX_WIDTHS } from '../layouts/max-widths'\nimport { discoverComponents } from '../lib/discovery'\nimport { preloadIcons } from '../lib/icons'\nimport { cn } from '../lib/utils'\nimport { AlfakitPage } from '../pages/page'\nimport { createDefaultRegistry } from '../registry'\n\n/** The host's layout, as the starter kits' `AppLayout` takes its props. */\nexport type StarterKitLayout = ComponentType<{\n breadcrumbs?: StarterKitBreadcrumb[]\n children: ReactNode\n}>\n\nexport interface StarterKitPageOptions extends Pick<\n AlfakitOptions<AnyComponent>,\n 'components' | 'pages' | 'chartFormatters' | 'chartPlugins' | 'richEditorExtensions'\n> {\n /**\n * The host layout Alfakit pages render in, usually the starter kit's `AppLayout`\n * (`@/layouts/app-layout`). It receives the page's breadcrumbs as `{ title, href }`.\n * `null` renders the pages without a host layout.\n */\n layout?: StarterKitLayout | null\n /**\n * Auto-registered custom components, as for `createAlfakit({ discover })`: pass\n * `import.meta.glob('../../alfakit/{fields,columns,entries,widgets,pages,components,blocks}/**\\/*.tsx')`\n * from `resources/js/pages/Alfakit/Page.tsx` and every file there registers under its name.\n */\n discover?: AlfakitOptions<AnyComponent>['discover']\n}\n\n/**\n * The content area of an Alfakit page inside the host layout: what `layout:app` renders in\n * starter kit mode instead of Alfakit's sidebar and top bar. Replace it with\n * `createStarterKitPage({ components: { 'layout:app': MyContent } })`.\n */\nexport function StarterKitContentLayout({ children, maxContentWidth }: AppLayoutProps) {\n const props = useAlfakitProps()\n // The host's pages fill the content area, so starter kit pages default to the full width.\n const width = maxContentWidth ?? props.alfakit.maxContentWidth ?? 'full'\n\n return (\n <div\n data-slot=\"content\"\n data-alfakit-embedded=\"\"\n className={cn(\n 'mx-auto flex w-full min-w-0 flex-1 flex-col gap-6 p-4 md:p-6',\n MAX_WIDTHS[width] ?? 'max-w-full',\n )}\n >\n <Slot name=\"content.start\" />\n {children}\n <Slot name=\"content.end\" />\n </div>\n )\n}\n\n// One context for the browser session (it survives navigations between host and Alfakit\n// pages); a server render builds one per request so concurrent renders never share a locale.\nlet browserContext: AlfakitContextValue | null = null\n\nfunction createStarterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n // The host's layout replaces the panel's. Auth pages (invitations, tenant registration)\n // keep the panel's card; the fullscreen layout loads on demand.\n // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.\n const discovered = discoverComponents(options.discover)\n const registry = createDefaultRegistry()\n .registerMany(lazyPanelLayouts)\n .register('layout:auth', AuthLayout)\n .register('layout:app', StarterKitContentLayout)\n .registerMany(discovered.components)\n .registerMany(options.components ?? {})\n const translator = new Translator()\n\n return {\n registry,\n pages: { 'Alfakit/Page': AlfakitPage, ...discovered.pages, ...options.pages },\n translator,\n actions: new ActionRunner({\n endpoints: { action: '', modal: '' },\n // A failed action request is a toast, never a silent no-op.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n null,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n }\n}\n\nfunction starterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n if (typeof window === 'undefined') return createStarterKitContext(options)\n\n browserContext ??= createStarterKitContext(options)\n\n return browserContext\n}\n\n/**\n * The Inertia layout around every Alfakit page in starter kit mode, inside the host layout:\n * Alfakit's context, modals, toasts and the reading direction. It stays mounted while the\n * user moves between Alfakit pages.\n */\nexport function AlfakitStarterKitRoot({\n options,\n children,\n}: {\n options?: StarterKitPageOptions\n children?: ReactNode\n}) {\n const [context] = useState(() => starterKitContext(options ?? {}))\n const props = usePage<AlfakitPageProps>().props\n const panel = props.alfakit\n\n // Endpoints and error notifications follow the open page.\n if (panel) {\n context.actions.setEndpoints({\n action: panel.endpoints.action ?? '',\n modal: panel.endpoints.modal ?? '',\n })\n }\n\n useEffect(() => {\n syncStarterKitPage(props)\n }, [props])\n\n useEffect(() => {\n installStarterKitRuntime(\n (notification) => showToast(notification),\n (key, replacements, fallback) => context.translator.t(key, replacements, fallback),\n )\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => {\n // A host page is open: its errors and toasts are the host's again.\n syncStarterKitPage(null)\n setToasterId(undefined)\n }\n }, [context])\n\n return (\n <AlfakitProvider value={context}>\n {/* The reading direction and language of Alfakit's content only: the host layout around\n it (sidebar, header) and the app's own pages keep the document's. */}\n <div\n data-slot=\"starter-kit-root\"\n dir={panel?.localization?.direction ?? 'ltr'}\n lang={panel?.localization?.tag}\n className=\"contents\"\n >\n <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>\n {children}\n {/* The debug panel's button lives in AlfaKit's top bar, which a starter kit doesn't\n render: float it in the corner instead (nothing without `alfakitDebug`). */}\n <div data-slot=\"starter-kit-debug-panel\" className=\"fixed end-16 bottom-4 z-40\">\n <DebugPanelSlot />\n </div>\n </AlfakitRoot>\n </div>\n </AlfakitProvider>\n )\n}\n\n/**\n * The page component for `Alfakit/Page` in a starter kit app: the host's\n * `resources/js/pages/Alfakit/Page.tsx` (written by `alfakit:install --starter-kit`) exports it,\n * so Inertia's page resolution, the root view's page preload and SSR all find it like any\n * other host page.\n *\n * ```tsx\n * import { createStarterKitPage } from '@alfakit/react'\n * import AppLayout from '@/layouts/app-layout'\n *\n * export default createStarterKitPage({ layout: AppLayout })\n * ```\n */\nexport function createStarterKitPage(options: StarterKitPageOptions = {}) {\n // Panel pages draw named icons: fetch `lucide-react/dynamic` with the page.\n preloadIcons()\n // `'js:name'` chart option callbacks, Chart.js plugins and rich editor extensions.\n configureCharts(options)\n registerRichEditorExtensions(options.richEditorExtensions ?? {})\n\n function AlfakitStarterKitPage() {\n return <AlfakitPage />\n }\n\n // A layout callback: the host layout (with the page's breadcrumbs), then Alfakit's root.\n AlfakitStarterKitPage.layout = (props: Record<string, unknown>) => [\n ...(options.layout && usesStarterKitHostLayout(props)\n ? [[options.layout, { breadcrumbs: starterKitBreadcrumbsFromProps(props) }] as const]\n : []),\n [AlfakitStarterKitRoot, { options }] as const,\n ]\n\n return AlfakitStarterKitPage\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"starter-kit.js","names":[],"sources":["../../src/starter-kit/starter-kit.tsx"],"sourcesContent":["import {\n ActionRunner,\n STARTER_KIT_TOASTER_ID,\n Translator,\n configureCharts,\n installStarterKitRuntime,\n registerRichEditorExtensions,\n resolveRequestErrorNotification,\n starterKitBreadcrumbsFromProps,\n syncStarterKitPage,\n usesStarterKitHostLayout,\n type AlfakitOptions,\n type AlfakitPageProps,\n type StarterKitBreadcrumb,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { useEffect, useState, type ComponentType, type ReactNode } from 'react'\nimport { setToasterId, showToast } from '../components/notifications/flash-toasts'\nimport { DebugPanelSlot } from '../components/panel/debug-panel-slot'\nimport { Slot } from '../components/panel/slot'\nimport {\n AlfakitProvider,\n useAlfakitProps,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { AlfakitRoot } from '../create-alfakit'\nimport type { AppLayoutProps } from '../layouts/app-layout'\nimport { AuthLayout } from '../layouts/auth-layout'\nimport { lazyPanelLayouts } from '../layouts/lazy-layouts'\nimport { MAX_WIDTHS } from '../layouts/max-widths'\nimport { discoverComponents } from '../lib/discovery'\nimport { preloadIcons } from '../lib/icons'\nimport { cn } from '../lib/utils'\nimport { AlfakitPage } from '../pages/page'\nimport { createDefaultRegistry } from '../registry'\n\n/** The host's layout, as the starter kits' `AppLayout` takes its props. */\nexport type StarterKitLayout = ComponentType<{\n breadcrumbs?: StarterKitBreadcrumb[]\n children: ReactNode\n}>\n\nexport interface StarterKitPageOptions extends Pick<\n AlfakitOptions<AnyComponent>,\n 'components' | 'pages' | 'chartFormatters' | 'chartPlugins' | 'richEditorExtensions'\n> {\n /**\n * The host layout Alfakit pages render in, usually the starter kit's `AppLayout`\n * (`@/layouts/app-layout`). It receives the page's breadcrumbs as `{ title, href }`.\n * `null` renders the pages without a host layout.\n */\n layout?: StarterKitLayout | null\n /**\n * Auto-registered custom components, as for `createAlfakit({ discover })`: pass\n * `import.meta.glob('../../alfakit/{fields,columns,entries,widgets,pages,components,blocks}/**\\/*.tsx')`\n * from `resources/js/pages/Alfakit/Page.tsx` and every file there registers under its name.\n */\n discover?: AlfakitOptions<AnyComponent>['discover']\n}\n\n/**\n * The content area of an Alfakit page inside the host layout: what `layout:app` renders in\n * starter kit mode instead of Alfakit's sidebar and top bar. Replace it with\n * `createStarterKitPage({ components: { 'layout:app': MyContent } })`.\n */\nexport function StarterKitContentLayout({ children, maxContentWidth }: AppLayoutProps) {\n const props = useAlfakitProps()\n // The host's pages fill the content area, so starter kit pages default to the full width.\n const width = maxContentWidth ?? props.alfakit.maxContentWidth ?? 'full'\n\n return (\n <div\n data-slot=\"content\"\n data-alfakit-embedded=\"\"\n className={cn(\n 'mx-auto flex w-full min-w-0 flex-1 flex-col gap-6 p-4 md:p-6',\n MAX_WIDTHS[width] ?? 'max-w-full',\n )}\n >\n <Slot name=\"content.start\" />\n {children}\n <Slot name=\"content.end\" />\n </div>\n )\n}\n\n/**\n * A `$layout = 'fullscreen'` page inside the host layout: what `layout:fullscreen` renders in\n * starter kit mode. The host layout is the chrome, so there is no AlfaKit header (brand, top\n * bar slots, user menu, Exit); the screen gets the whole content area. Replace it with\n * `{ components: { 'layout:fullscreen': MyScreen } }`.\n */\nexport function StarterKitFullscreenLayout({ children }: { children: ReactNode }) {\n return (\n <div\n data-slot=\"content\"\n data-layout=\"fullscreen\"\n data-alfakit-embedded=\"\"\n className=\"flex min-h-0 w-full min-w-0 flex-1 flex-col\"\n >\n <Slot name=\"content.start\" />\n {children}\n <Slot name=\"content.end\" />\n </div>\n )\n}\n\n// One context for the browser session (it survives navigations between host and Alfakit\n// pages); a server render builds one per request so concurrent renders never share a locale.\nlet browserContext: AlfakitContextValue | null = null\n\nfunction createStarterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n // The host's layout replaces the panel's. Auth pages (invitations, tenant registration)\n // keep the panel's card; the fullscreen layout loads on demand.\n // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.\n const discovered = discoverComponents(options.discover)\n const registry = createDefaultRegistry()\n .registerMany(lazyPanelLayouts)\n .register('layout:auth', AuthLayout)\n .register('layout:app', StarterKitContentLayout)\n .register('layout:fullscreen', StarterKitFullscreenLayout)\n .registerMany(discovered.components)\n .registerMany(options.components ?? {})\n const translator = new Translator()\n\n return {\n registry,\n pages: { 'Alfakit/Page': AlfakitPage, ...discovered.pages, ...options.pages },\n translator,\n actions: new ActionRunner({\n endpoints: { action: '', modal: '' },\n // A failed action request is a toast, never a silent no-op.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n null,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n }\n}\n\nfunction starterKitContext(options: StarterKitPageOptions): AlfakitContextValue {\n if (typeof window === 'undefined') return createStarterKitContext(options)\n\n browserContext ??= createStarterKitContext(options)\n\n return browserContext\n}\n\n/**\n * The Inertia layout around every Alfakit page in starter kit mode, inside the host layout:\n * Alfakit's context, modals, toasts and the reading direction. It stays mounted while the\n * user moves between Alfakit pages.\n */\nexport function AlfakitStarterKitRoot({\n options,\n children,\n}: {\n options?: StarterKitPageOptions\n children?: ReactNode\n}) {\n const [context] = useState(() => starterKitContext(options ?? {}))\n const props = usePage<AlfakitPageProps>().props\n const panel = props.alfakit\n\n // Endpoints and error notifications follow the open page.\n if (panel) {\n context.actions.setEndpoints({\n action: panel.endpoints.action ?? '',\n modal: panel.endpoints.modal ?? '',\n })\n }\n\n useEffect(() => {\n syncStarterKitPage(props)\n }, [props])\n\n useEffect(() => {\n installStarterKitRuntime(\n (notification) => showToast(notification),\n (key, replacements, fallback) => context.translator.t(key, replacements, fallback),\n )\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => {\n // A host page is open: its errors and toasts are the host's again.\n syncStarterKitPage(null)\n setToasterId(undefined)\n }\n }, [context])\n\n return (\n <AlfakitProvider value={context}>\n {/* The reading direction and language of Alfakit's content only: the host layout around\n it (sidebar, header) and the app's own pages keep the document's. */}\n <div\n data-slot=\"starter-kit-root\"\n dir={panel?.localization?.direction ?? 'ltr'}\n lang={panel?.localization?.tag}\n className=\"contents\"\n >\n <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>\n {children}\n {/* The debug panel's button lives in AlfaKit's top bar, which a starter kit doesn't\n render: float it in the corner instead (nothing without `alfakitDebug`). The corner is the\n document's right: the kit's sidebar stays on the left in a right-to-left panel language. */}\n <div data-slot=\"starter-kit-debug-panel\" className=\"fixed right-16 bottom-16 z-40\">\n <DebugPanelSlot />\n </div>\n </AlfakitRoot>\n </div>\n </AlfakitProvider>\n )\n}\n\n/**\n * The page component for `Alfakit/Page` in a starter kit app: the host's\n * `resources/js/pages/Alfakit/Page.tsx` (written by `alfakit:install --starter-kit`) exports it,\n * so Inertia's page resolution, the root view's page preload and SSR all find it like any\n * other host page.\n *\n * ```tsx\n * import { createStarterKitPage } from '@alfakit/react'\n * import AppLayout from '@/layouts/app-layout'\n *\n * export default createStarterKitPage({ layout: AppLayout })\n * ```\n */\nexport function createStarterKitPage(options: StarterKitPageOptions = {}) {\n // Panel pages draw named icons: fetch `lucide-react/dynamic` with the page.\n preloadIcons()\n // `'js:name'` chart option callbacks, Chart.js plugins and rich editor extensions.\n configureCharts(options)\n registerRichEditorExtensions(options.richEditorExtensions ?? {})\n\n function AlfakitStarterKitPage() {\n return <AlfakitPage />\n }\n\n // A layout callback: the host layout (with the page's breadcrumbs), then Alfakit's root.\n AlfakitStarterKitPage.layout = (props: Record<string, unknown>) => [\n ...(options.layout && usesStarterKitHostLayout(props)\n ? [[options.layout, { breadcrumbs: starterKitBreadcrumbsFromProps(props) }] as const]\n : []),\n [AlfakitStarterKitRoot, { options }] as const,\n ]\n\n return AlfakitStarterKitPage\n}\n"],"mappings":"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"}
package/dist/styles.css CHANGED
@@ -289,7 +289,8 @@
289
289
  }
290
290
  /* Rich editor mentions, merge tags and custom blocks (RichEditor extensions) */
291
291
  .alfakit-prose :where(.alfakit-mention):not(:where(.alfakit-not-prose *)) {
292
- @apply bg-primary/10 text-primary rounded px-1 font-medium;
292
+ /* Full-contrast text on the tint (WCAG AA); the ring marks it as a mention. */
293
+ @apply bg-primary/10 text-foreground ring-primary/30 rounded px-1 font-medium ring-1;
293
294
  }
294
295
  .alfakit-prose :where(.alfakit-merge-tag):not(:where(.alfakit-not-prose *)) {
295
296
  @apply bg-muted text-muted-foreground rounded px-1 font-mono text-xs;
package/dist/themes.css CHANGED
@@ -742,6 +742,10 @@
742
742
  max-height: none;
743
743
  width: 100%;
744
744
  max-width: 100%;
745
+ /* The shadcn centring utilities read these variables. Resetting them survives a
746
+ minifier that drops `translate: none` in favour of the utilities' `translate`. */
747
+ --tw-translate-x: 0;
748
+ --tw-translate-y: 0;
745
749
  translate: none;
746
750
  transform: none;
747
751
  border-radius: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfakit/react",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
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.9"
99
+ "@alfakit/core": "0.1.11"
100
100
  },
101
101
  "peerDependencies": {
102
102
  "@inertiajs/react": "^3.0.0",
@@ -180,7 +180,21 @@ export function ActionButton({
180
180
  const ariaLabel = isIconOnly || labeledFrom ? accessibleName : undefined
181
181
 
182
182
  const button = isPlainLink ? (
183
- action.openUrlInNewTab ? (
183
+ action.download ? (
184
+ // `download()`: a plain link that saves the file (an Inertia visit would discard it).
185
+ <Button
186
+ asChild
187
+ variant="plain"
188
+ size={sizeProp}
189
+ disabled={action.disabled}
190
+ aria-label={ariaLabel}
191
+ {...extraProps(action.extraAttributes, classes)}
192
+ >
193
+ <a href={action.url} download="">
194
+ {content}
195
+ </a>
196
+ </Button>
197
+ ) : action.openUrlInNewTab ? (
184
198
  <Button
185
199
  asChild
186
200
  variant="plain"
@@ -6,6 +6,7 @@ import {
6
6
  jsonScalar,
7
7
  jsonSize,
8
8
  latestServerTiming,
9
+ overlayPlacementDirection,
9
10
  prettyJson,
10
11
  searchJson,
11
12
  type JsonEntry,
@@ -80,13 +81,17 @@ export function DevToolsInspector() {
80
81
  size="icon"
81
82
  aria-label={t('dev_tools.open', {}, 'Open schema inspector')}
82
83
  data-slot="dev-tools-toggle"
83
- className="fixed end-4 bottom-4 z-40 rounded-full shadow-md"
84
+ className="fixed end-4 bottom-16 z-40 rounded-full shadow-md [[data-slot=starter-kit-root]_&]:end-auto [[data-slot=starter-kit-root]_&]:right-4"
84
85
  >
85
86
  <BracesIcon />
86
87
  </Button>
87
88
  </SheetTrigger>
88
89
  <SheetContent
89
- side={direction === 'rtl' ? 'left' : 'right'}
90
+ side={
91
+ overlayPlacementDirection(direction, props.alfakit?.starterKit) === 'rtl'
92
+ ? 'left'
93
+ : 'right'
94
+ }
90
95
  className="flex w-full flex-col gap-0 p-0 sm:max-w-xl"
91
96
  >
92
97
  <div data-slot="dev-tools" className="flex min-h-0 flex-1 flex-col">
@@ -66,7 +66,7 @@ export function CheckboxListField({ component }: { component: CheckboxListSchema
66
66
  <FieldWrapper field={component} htmlFor={id} error={error}>
67
67
  <div className="flex flex-col gap-2">
68
68
  {(component.searchable || (component.bulkToggleable && !readOnly)) && (
69
- <div className="flex items-center gap-2">
69
+ <div className="flex min-w-0 flex-wrap items-center gap-2">
70
70
  {component.searchable && (
71
71
  <Input
72
72
  value={search}
@@ -77,7 +77,7 @@ export function CheckboxListField({ component }: { component: CheckboxListSchema
77
77
  />
78
78
  )}
79
79
  {component.bulkToggleable && !readOnly && (
80
- <div className="flex gap-1">
80
+ <div className="flex flex-wrap gap-1">
81
81
  {!selectAll?.hidden && (
82
82
  <Button
83
83
  type="button"
@@ -13,13 +13,6 @@ import { Input } from '../ui/input'
13
13
  * it, accepts the locale's separators and digits ("1.234,50", "١٢٫٥"), and rounds to
14
14
  * the currency's precision on blur. The form value is a plain decimal string ("1234.50").
15
15
  */
16
- export function clampAmount(amount: number, min?: number | null, max?: number | null): number {
17
- const lower = min ?? Number.NEGATIVE_INFINITY
18
- const upper = max ?? Number.POSITIVE_INFINITY
19
-
20
- return Math.min(upper, Math.max(lower, amount))
21
- }
22
-
23
16
  export function MoneyInputField({ component }: { component: MoneyInputSchema }) {
24
17
  const { value, setValue, blur, error, id, disabled, aria } = useField<string | number | null>(
25
18
  component,
@@ -46,13 +39,9 @@ export function MoneyInputField({ component }: { component: MoneyInputSchema })
46
39
 
47
40
  const parsed = format.parseNumber(text)
48
41
 
49
- // Unparseable input is kept as typed so server validation can explain the problem;
50
- // amounts are brought inside `minValue()` / `maxValue()`.
51
- setValue(
52
- parsed === null
53
- ? text
54
- : clampAmount(parsed, component.min, component.max).toFixed(component.decimals),
55
- )
42
+ // Unparseable input is kept as typed and out-of-range amounts are kept as well, so the
43
+ // server's `minValue()` / `maxValue()` rules report them (W-V3-11).
44
+ setValue(parsed === null ? text : parsed.toFixed(component.decimals))
56
45
  }
57
46
 
58
47
  const affix = (text: string, icon: string | undefined, side: 'start' | 'end') =>
@@ -11,6 +11,7 @@ import {
11
11
  customBlockNodeConfig,
12
12
  EditorSuggestions,
13
13
  fieldActionContext,
14
+ fixedContainingBlockOffset,
14
15
  groupCustomBlocks,
15
16
  groupTriggerButton,
16
17
  isCustomBlockResult,
@@ -358,6 +359,9 @@ export function ToolbarGroupButton({
358
359
  onRun: (name: string) => void
359
360
  }) {
360
361
  const buttons = group.buttons.filter((name) => commands[name])
362
+ // After a pick, typing goes on in the editor (the command focused it); the menu would
363
+ // otherwise return focus to its trigger. Closing without a pick (Escape) still does.
364
+ const picked = useRef(false)
361
365
 
362
366
  if (buttons.length === 0) return null
363
367
 
@@ -385,6 +389,13 @@ export function ToolbarGroupButton({
385
389
  align="start"
386
390
  aria-label={group.label}
387
391
  className={cn(!group.textual && 'flex min-w-0 flex-row gap-0.5')}
392
+ onCloseAutoFocus={(event) => {
393
+ if (!picked.current) return
394
+
395
+ picked.current = false
396
+ event.preventDefault()
397
+ editor.commands.focus()
398
+ }}
388
399
  >
389
400
  {buttons.map((name) => {
390
401
  const label = commandLabel(name, commands, translate)
@@ -396,7 +407,10 @@ export function ToolbarGroupButton({
396
407
  aria-label={group.textual ? undefined : label}
397
408
  title={label}
398
409
  className={cn(pressed && 'bg-accent text-accent-foreground')}
399
- onSelect={() => onRun(name)}
410
+ onSelect={() => {
411
+ picked.current = true
412
+ onRun(name)
413
+ }}
400
414
  >
401
415
  {icons(name)}
402
416
  {group.textual && <span>{label}</span>}
@@ -436,7 +450,15 @@ export function SuggestionPopup({
436
450
  data-slot="rich-editor-suggestions"
437
451
  className="bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md"
438
452
  // Runtime coordinates cannot be Tailwind classes.
439
- style={state.coords ? { left: state.coords.left, top: state.coords.bottom + 4 } : undefined}
453
+ // Inside a modal the dialog's transform is the containing block of this fixed list.
454
+ style={
455
+ state.coords
456
+ ? {
457
+ left: state.coords.left - fixedContainingBlockOffset(editor.view.dom).x,
458
+ top: state.coords.bottom + 4 - fixedContainingBlockOffset(editor.view.dom).y,
459
+ }
460
+ : undefined
461
+ }
440
462
  >
441
463
  {state.loading && state.items.length === 0 ? (
442
464
  <div className="text-muted-foreground px-2 py-1.5 text-sm">
@@ -1389,7 +1389,8 @@ function Dialogs({
1389
1389
  open={dialog?.type === 'createFolder' || dialog?.type === 'rename'}
1390
1390
  onOpenChange={(open) => !open && close()}
1391
1391
  >
1392
- <DialogContent data-slot="file-manager-dialog">
1392
+ {/* The title and the labelled field say it all: no description. */}
1393
+ <DialogContent data-slot="file-manager-dialog" aria-describedby={undefined}>
1393
1394
  <form
1394
1395
  className="flex flex-col gap-4"
1395
1396
  onSubmit={(event) => {
@@ -1763,6 +1764,7 @@ function Preview({
1763
1764
  <DialogContent
1764
1765
  className="flex max-h-[90vh] flex-col sm:max-w-4xl"
1765
1766
  data-slot="file-manager-preview"
1767
+ aria-describedby={undefined}
1766
1768
  onKeyDown={onKeyDown}
1767
1769
  >
1768
1770
  <DialogHeader>
@@ -25,6 +25,8 @@ import { Icon } from '../../lib/icons'
25
25
  import { cn } from '../../lib/utils'
26
26
  import { usePageActionContext } from '../actions/action-context'
27
27
  import { ActionList } from '../actions/action-list'
28
+ import { useOptionalFormStore } from '../form/form-context'
29
+ import { useFieldActionContext } from '../schema/field-actions'
28
30
  import { RenderComponent } from '../schema/schema-renderer'
29
31
  import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'
30
32
  import { MixedContent } from './mixed-content'
@@ -67,7 +69,12 @@ const CALLOUT_ICON_SIZES: Record<string, string> = {
67
69
  }
68
70
 
69
71
  export function CalloutComponent({ component }: { component: CalloutSchema }) {
70
- const context = usePageActionContext()
72
+ // Inside a form the actions run as field actions of this component, so `$get` / `$set`
73
+ // reach the form (like a section's header actions).
74
+ const form = useOptionalFormStore()
75
+ const pageContext = usePageActionContext()
76
+ const fieldContext = useFieldActionContext(component)
77
+ const context = form ? fieldContext : pageContext
71
78
  const tone = component.color ?? component.status
72
79
  // `iconColor()`, or the status colour once `color(null)` removed the background.
73
80
  const iconColor = component.iconColor ?? (component.color ? undefined : component.status)
@@ -113,19 +120,30 @@ export function CalloutComponent({ component }: { component: CalloutSchema }) {
113
120
  className={cn('mt-2', actionsAlignmentClass(component.footerActionsAlignment))}
114
121
  />
115
122
  )}
116
- <MixedContent items={component.footer} name="callout-footer" className="mt-2" />
123
+ <MixedContent
124
+ items={component.footer}
125
+ name="callout-footer"
126
+ className="mt-2"
127
+ context={context}
128
+ />
117
129
  </div>
118
130
  <MixedContent
119
131
  items={component.controls}
120
132
  name="callout-controls"
121
133
  className="-me-1 -mt-1 shrink-0 gap-1"
134
+ context={context}
122
135
  />
123
136
  </div>
124
137
  )
125
138
  }
126
139
 
127
140
  export function EmptyStateComponent({ component }: { component: EmptyStateComponentSchema }) {
128
- const context = usePageActionContext()
141
+ // Inside a form the actions run as field actions of this component, so `$get` / `$set`
142
+ // reach the form (like a section's header actions).
143
+ const form = useOptionalFormStore()
144
+ const pageContext = usePageActionContext()
145
+ const fieldContext = useFieldActionContext(component)
146
+ const context = form ? fieldContext : pageContext
129
147
 
130
148
  return (
131
149
  <div
@@ -158,7 +176,12 @@ export function EmptyStateComponent({ component }: { component: EmptyStateCompon
158
176
  {component.actions && component.actions.length > 0 && (
159
177
  <ActionList actions={component.actions} context={context} className="mt-2 justify-center" />
160
178
  )}
161
- <MixedContent items={component.footer} name="empty-state-footer" className="justify-center" />
179
+ <MixedContent
180
+ items={component.footer}
181
+ name="empty-state-footer"
182
+ className="justify-center"
183
+ context={context}
184
+ />
162
185
  </div>
163
186
  )
164
187
  }
@@ -1,4 +1,4 @@
1
- import type { AnyActionSchema, ContentSlotItem } from '@alfakit/core'
1
+ import type { ActionContext, AnyActionSchema, ContentSlotItem } from '@alfakit/core'
2
2
  import type { ReactNode } from 'react'
3
3
  import { cn } from '../../lib/utils'
4
4
  import { usePageActionContext } from '../actions/action-context'
@@ -7,19 +7,23 @@ import { RenderComponent } from '../schema/schema-renderer'
7
7
 
8
8
  /**
9
9
  * Actions and components placed in a layout component's fixed spot (a section's
10
- * `afterHeader()`, a callout's `controls()`). Actions run in the page's action context,
11
- * like section header actions.
10
+ * `afterHeader()`, a callout's `controls()`). Actions run in the owner's action context
11
+ * (`context`: inside a form, the owner's field context, so `$get` / `$set` work), else the
12
+ * page's.
12
13
  */
13
14
  export function MixedContent({
14
15
  items,
15
16
  name,
16
17
  className,
18
+ context: ownerContext,
17
19
  }: {
18
20
  items: ContentSlotItem[] | undefined
19
21
  name: string
20
22
  className?: string
23
+ context?: ActionContext
21
24
  }) {
22
- const context = usePageActionContext()
25
+ const pageContext = usePageActionContext()
26
+ const context = ownerContext ?? pageContext
23
27
 
24
28
  if (!items?.length) {
25
29
  return null
@@ -86,7 +86,7 @@ export function SectionComponent({ component }: { component: SectionSchema }) {
86
86
  {component.headerActions && component.headerActions.length > 0 && (
87
87
  <ActionList actions={component.headerActions} context={context} size="sm" />
88
88
  )}
89
- <MixedContent items={component.afterHeader} name="section-after-header" />
89
+ <MixedContent items={component.afterHeader} name="section-after-header" context={context} />
90
90
  {component.collapsible && (
91
91
  <CollapsibleTrigger asChild>
92
92
  <Button
@@ -123,7 +123,7 @@ export function SectionComponent({ component }: { component: SectionSchema }) {
123
123
  className={actionsAlignmentClass(component.footerActionsAlignment) || undefined}
124
124
  />
125
125
  )}
126
- <MixedContent items={component.footer} name="section-footer-content" />
126
+ <MixedContent items={component.footer} name="section-footer-content" context={context} />
127
127
  </div>
128
128
  ) : null
129
129
 
@@ -3,9 +3,13 @@ import {
3
3
  notificationTintStyles,
4
4
  onDatabaseNotificationsSent,
5
5
  onOpenDatabaseNotifications,
6
+ pwaStore,
7
+ startPwa,
8
+ togglePushNotifications,
6
9
  } from '@alfakit/core'
7
- import { BellIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'
10
+ import { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'
8
11
  import { useEffect, useMemo, useState } from 'react'
12
+ import { toast } from 'sonner'
9
13
  import { useDirection, useFormatter, usePanel, useT } from '../../context'
10
14
  import { useStore } from '../../hooks/use-store'
11
15
  import { cssColor, textColorClasses } from '../../lib/colors'
@@ -86,6 +90,29 @@ export function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps
86
90
  [center],
87
91
  )
88
92
 
93
+ // `pwa(push: true)` in a starter kit: the app's user menu has no AlfaKit entries, so Web
94
+ // Push is turned on or off from the bell's menu.
95
+ const pwa = useStore(pwaStore)
96
+ const kitPush = Boolean(panel.starterKit && panel.pwa?.push)
97
+ const pushToggle = kitPush && pwa.push !== 'unsupported'
98
+
99
+ useEffect(() => {
100
+ if (kitPush) startPwa(panel.pwa)
101
+ }, [kitPush, panel.pwa])
102
+
103
+ const togglePush = async () => {
104
+ const { message } = await togglePushNotifications()
105
+ const text = t(message.key, {}, message.fallback)
106
+
107
+ if (message.error) toast.error(text)
108
+ else toast.success(text)
109
+ }
110
+
111
+ const pushLabel =
112
+ pwa.push === 'subscribed'
113
+ ? t('pwa.notifications.disable', {}, 'Turn off notifications')
114
+ : t('pwa.notifications.enable', {}, 'Turn on notifications')
115
+
89
116
  if (!config || !center) {
90
117
  return null
91
118
  }
@@ -145,6 +172,20 @@ export function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps
145
172
  <div className="flex items-center justify-between border-b px-3 py-2">
146
173
  <span className="text-sm font-semibold">{t('notifications', {}, 'Notifications')}</span>
147
174
  <div className="flex items-center gap-1">
175
+ {pushToggle && (
176
+ <Button
177
+ type="button"
178
+ variant="ghost"
179
+ size="icon-sm"
180
+ data-slot="pwa-push"
181
+ disabled={pwa.pushBusy || pwa.push === 'denied'}
182
+ title={pushLabel}
183
+ aria-label={pushLabel}
184
+ onClick={() => void togglePush()}
185
+ >
186
+ {pwa.push === 'subscribed' ? <BellOffIcon /> : <BellRingIcon />}
187
+ </Button>
188
+ )}
148
189
  {state.unreadCount > 0 && (
149
190
  <Button
150
191
  type="button"
@@ -1,5 +1,8 @@
1
- import { useT } from '../../context'
1
+ import { widgetPollTarget, type LazyWidgetSchema } from '@alfakit/core'
2
+ import { useState } from 'react'
3
+ import { useRefreshComponents, useT } from '../../context'
2
4
  import { cn } from '../../lib/utils'
5
+ import { Button } from '../ui/button'
3
6
  import { Skeleton } from '../ui/skeleton'
4
7
 
5
8
  /**
@@ -37,3 +40,40 @@ export function LazyPlaceholder({
37
40
  </div>
38
41
  )
39
42
  }
43
+
44
+ /**
45
+ * A lazy widget's cell: the skeleton while its schema loads, or, when the request failed
46
+ * (outside panels: a refused or failed fetch), a message with a retry.
47
+ */
48
+ export function LazyWidgetPlaceholder({ widget }: { widget: LazyWidgetSchema }) {
49
+ const t = useT()
50
+ const refresh = useRefreshComponents()
51
+ const [retrying, setRetrying] = useState(false)
52
+ const variant = widget.type === 'TableWidget' ? 'table' : 'widget'
53
+
54
+ if (!widget.failed) return <LazyPlaceholder variant={variant} />
55
+
56
+ const retry = () => {
57
+ if (!refresh) return
58
+
59
+ setRetrying(true)
60
+ void Promise.resolve(refresh([widgetPollTarget(widget)])).finally(() => setRetrying(false))
61
+ }
62
+
63
+ return (
64
+ <div
65
+ data-slot="lazy-placeholder"
66
+ data-variant={variant}
67
+ data-failed=""
68
+ role="alert"
69
+ className="bg-card text-muted-foreground flex min-h-28 flex-col items-center justify-center gap-3 rounded-xl border border-dashed p-6 text-center text-sm"
70
+ >
71
+ <p>{t('widget_failed', {}, 'This widget could not be loaded.')}</p>
72
+ {refresh && (
73
+ <Button type="button" variant="outline" size="sm" disabled={retrying} onClick={retry}>
74
+ {t('retry', {}, 'Retry')}
75
+ </Button>
76
+ )}
77
+ </div>
78
+ )
79
+ }