@alfakit/react 0.1.1 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/actions/action-button.d.ts.map +1 -1
  3. package/dist/components/actions/action-button.js +5 -4
  4. package/dist/components/actions/action-button.js.map +1 -1
  5. package/dist/components/actions/action-modals.d.ts.map +1 -1
  6. package/dist/components/actions/action-modals.js +6 -5
  7. package/dist/components/actions/action-modals.js.map +1 -1
  8. package/dist/components/ai/copilot.d.ts +3 -1
  9. package/dist/components/ai/copilot.d.ts.map +1 -1
  10. package/dist/components/ai/copilot.js +4 -1
  11. package/dist/components/ai/copilot.js.map +1 -1
  12. package/dist/components/cms/live-preview.js +3 -2
  13. package/dist/components/cms/live-preview.js.map +1 -1
  14. package/dist/components/cms/page.js +1 -1
  15. package/dist/components/fields/builder.js +15 -6
  16. package/dist/components/fields/builder.js.map +1 -1
  17. package/dist/components/fields/color-picker.js +1 -0
  18. package/dist/components/fields/color-picker.js.map +1 -1
  19. package/dist/components/fields/file-upload.js +11 -10
  20. package/dist/components/fields/file-upload.js.map +1 -1
  21. package/dist/components/fields/image-editor-dialog.js +4 -2
  22. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  23. package/dist/components/fields/item-actions.js +10 -10
  24. package/dist/components/fields/item-actions.js.map +1 -1
  25. package/dist/components/fields/key-value.d.ts.map +1 -1
  26. package/dist/components/fields/key-value.js +3 -3
  27. package/dist/components/fields/key-value.js.map +1 -1
  28. package/dist/components/fields/modal-table-select.js +6 -5
  29. package/dist/components/fields/modal-table-select.js.map +1 -1
  30. package/dist/components/fields/one-time-code-input.js +1 -1
  31. package/dist/components/fields/one-time-code-input.js.map +1 -1
  32. package/dist/components/fields/repeater-table.js +12 -1
  33. package/dist/components/fields/repeater-table.js.map +1 -1
  34. package/dist/components/fields/repeater.js +2 -2
  35. package/dist/components/fields/repeater.js.map +1 -1
  36. package/dist/components/fields/rich-editor.js +2 -1
  37. package/dist/components/fields/rich-editor.js.map +1 -1
  38. package/dist/components/fields/select.js +3 -3
  39. package/dist/components/fields/select.js.map +1 -1
  40. package/dist/components/file-manager/file-manager.js +1 -1
  41. package/dist/components/file-manager/file-manager.js.map +1 -1
  42. package/dist/components/form/form-block.d.ts.map +1 -1
  43. package/dist/components/form/form-block.js +14 -2
  44. package/dist/components/form/form-block.js.map +1 -1
  45. package/dist/components/form/form-context.js +7 -3
  46. package/dist/components/form/form-context.js.map +1 -1
  47. package/dist/components/kanban/kanban.js +4 -3
  48. package/dist/components/kanban/kanban.js.map +1 -1
  49. package/dist/components/layout/fused-group.js +1 -0
  50. package/dist/components/layout/fused-group.js.map +1 -1
  51. package/dist/components/layout/section.js +2 -2
  52. package/dist/components/layout/section.js.map +1 -1
  53. package/dist/components/layout/tabs.d.ts.map +1 -1
  54. package/dist/components/layout/tabs.js +54 -15
  55. package/dist/components/layout/tabs.js.map +1 -1
  56. package/dist/components/panel/app-sidebar.d.ts +2 -3
  57. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  58. package/dist/components/panel/app-sidebar.js +1 -24
  59. package/dist/components/panel/app-sidebar.js.map +1 -1
  60. package/dist/components/panel/brand.d.ts +10 -0
  61. package/dist/components/panel/brand.d.ts.map +1 -0
  62. package/dist/components/panel/brand.js +37 -0
  63. package/dist/components/panel/brand.js.map +1 -0
  64. package/dist/components/panel/global-search.js +1 -0
  65. package/dist/components/panel/global-search.js.map +1 -1
  66. package/dist/components/panel/index.d.ts +2 -1
  67. package/dist/components/panel/shortcut-keys.js +1 -0
  68. package/dist/components/panel/shortcut-keys.js.map +1 -1
  69. package/dist/components/panel/tenant-menu.js +2 -2
  70. package/dist/components/panel/tenant-menu.js.map +1 -1
  71. package/dist/components/panel/topbar.js +1 -1
  72. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  73. package/dist/components/realtime/live-records-pill.js +6 -3
  74. package/dist/components/realtime/live-records-pill.js.map +1 -1
  75. package/dist/components/table/cells.d.ts +6 -0
  76. package/dist/components/table/cells.d.ts.map +1 -1
  77. package/dist/components/table/cells.js +15 -3
  78. package/dist/components/table/cells.js.map +1 -1
  79. package/dist/components/table/column-manager.js +8 -7
  80. package/dist/components/table/column-manager.js.map +1 -1
  81. package/dist/components/table/content-layout.js +2 -1
  82. package/dist/components/table/content-layout.js.map +1 -1
  83. package/dist/components/table/grouping.js +11 -1
  84. package/dist/components/table/grouping.js.map +1 -1
  85. package/dist/components/table/pagination.js +16 -13
  86. package/dist/components/table/pagination.js.map +1 -1
  87. package/dist/components/table/table-block.js +14 -4
  88. package/dist/components/table/table-block.js.map +1 -1
  89. package/dist/components/ui/dialog.d.ts.map +1 -1
  90. package/dist/components/ui/dialog.js +3 -1
  91. package/dist/components/ui/dialog.js.map +1 -1
  92. package/dist/components/ui/popover.d.ts.map +1 -1
  93. package/dist/components/ui/popover.js +3 -1
  94. package/dist/components/ui/popover.js.map +1 -1
  95. package/dist/components/ui/sheet.d.ts.map +1 -1
  96. package/dist/components/ui/sheet.js +3 -1
  97. package/dist/components/ui/sheet.js.map +1 -1
  98. package/dist/components/ui/tooltip.js +3 -1
  99. package/dist/components/ui/tooltip.js.map +1 -1
  100. package/dist/components.css +347 -0
  101. package/dist/context.d.ts +5 -0
  102. package/dist/context.d.ts.map +1 -1
  103. package/dist/context.js +5 -1
  104. package/dist/context.js.map +1 -1
  105. package/dist/create-alfakit.d.ts.map +1 -1
  106. package/dist/create-alfakit.js +8 -5
  107. package/dist/create-alfakit.js.map +1 -1
  108. package/dist/eject-manifest.json +16 -3
  109. package/dist/index.d.ts +6 -3
  110. package/dist/index.js +24 -21
  111. package/dist/layouts/app-layout.d.ts.map +1 -1
  112. package/dist/layouts/app-layout.js +2 -29
  113. package/dist/layouts/app-layout.js.map +1 -1
  114. package/dist/layouts/auth-layout.js +2 -2
  115. package/dist/layouts/auth-layout.js.map +1 -1
  116. package/dist/layouts/fullscreen-layout.js +1 -1
  117. package/dist/layouts/fullscreen-layout.js.map +1 -1
  118. package/dist/layouts/lazy-layouts.js +15 -0
  119. package/dist/layouts/lazy-layouts.js.map +1 -0
  120. package/dist/layouts/max-widths.js +34 -0
  121. package/dist/layouts/max-widths.js.map +1 -0
  122. package/dist/layouts/panel-layouts.d.ts +10 -0
  123. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  124. package/dist/layouts/panel-layouts.js +20 -0
  125. package/dist/layouts/panel-layouts.js.map +1 -0
  126. package/dist/layouts/top-nav-layout.js +2 -2
  127. package/dist/layouts/top-nav-layout.js.map +1 -1
  128. package/dist/pages/page.d.ts.map +1 -1
  129. package/dist/pages/page.js +12 -7
  130. package/dist/pages/page.js.map +1 -1
  131. package/dist/standalone/alfakit-component.js +11 -6
  132. package/dist/standalone/alfakit-component.js.map +1 -1
  133. package/dist/standalone/alfakit-toaster.d.ts.map +1 -1
  134. package/dist/standalone/alfakit-toaster.js +8 -1
  135. package/dist/standalone/alfakit-toaster.js.map +1 -1
  136. package/dist/standalone.css +54 -0
  137. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  138. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  139. package/dist/starter-kit/dashboard-widgets.js +53 -0
  140. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  141. package/dist/starter-kit/impersonation-banner.js +34 -0
  142. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  143. package/dist/starter-kit/index.d.ts +3 -2
  144. package/dist/starter-kit/index.js +3 -2
  145. package/dist/starter-kit/starter-kit.d.ts +6 -0
  146. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  147. package/dist/starter-kit/starter-kit.js +18 -7
  148. package/dist/starter-kit/starter-kit.js.map +1 -1
  149. package/dist/starter-kit/widgets-loaded.js +21 -5
  150. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  151. package/dist/starter-kit/widgets-triggers.js +35 -3
  152. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  153. package/dist/starter-kit/widgets.d.ts +13 -5
  154. package/dist/starter-kit/widgets.d.ts.map +1 -1
  155. package/dist/starter-kit/widgets.js +56 -18
  156. package/dist/starter-kit/widgets.js.map +1 -1
  157. package/dist/styles.css +15 -2
  158. package/dist/themes.css +8 -335
  159. package/package.json +2 -2
  160. package/src/components/actions/action-button.tsx +7 -4
  161. package/src/components/actions/action-modals.tsx +13 -1
  162. package/src/components/ai/copilot.tsx +14 -1
  163. package/src/components/cms/live-preview.tsx +5 -2
  164. package/src/components/fields/builder.tsx +14 -5
  165. package/src/components/fields/color-picker.tsx +1 -0
  166. package/src/components/fields/file-upload.tsx +21 -10
  167. package/src/components/fields/image-editor-dialog.tsx +7 -2
  168. package/src/components/fields/item-actions.tsx +32 -9
  169. package/src/components/fields/key-value.tsx +3 -2
  170. package/src/components/fields/modal-table-select.tsx +8 -3
  171. package/src/components/fields/one-time-code-input.tsx +1 -1
  172. package/src/components/fields/repeater-table.tsx +12 -1
  173. package/src/components/fields/repeater.tsx +2 -3
  174. package/src/components/fields/rich-editor.tsx +3 -0
  175. package/src/components/fields/select.tsx +7 -3
  176. package/src/components/file-manager/file-manager.tsx +1 -1
  177. package/src/components/form/form-block.tsx +26 -1
  178. package/src/components/form/form-context.tsx +3 -6
  179. package/src/components/kanban/kanban.tsx +16 -3
  180. package/src/components/layout/fused-group.tsx +5 -1
  181. package/src/components/layout/section.tsx +2 -2
  182. package/src/components/layout/tabs.tsx +72 -17
  183. package/src/components/panel/app-sidebar.tsx +2 -38
  184. package/src/components/panel/brand.tsx +46 -0
  185. package/src/components/panel/global-search.tsx +1 -1
  186. package/src/components/panel/shortcut-keys.tsx +4 -1
  187. package/src/components/panel/tenant-menu.tsx +2 -2
  188. package/src/components/realtime/live-records-pill.tsx +4 -3
  189. package/src/components/table/cells.tsx +27 -5
  190. package/src/components/table/column-manager.tsx +12 -10
  191. package/src/components/table/content-layout.tsx +5 -1
  192. package/src/components/table/grouping.tsx +15 -1
  193. package/src/components/table/pagination.tsx +16 -9
  194. package/src/components/table/table-block.tsx +25 -12
  195. package/src/components/ui/dialog.tsx +5 -1
  196. package/src/components/ui/popover.tsx +5 -1
  197. package/src/components/ui/sheet.tsx +5 -1
  198. package/src/components/ui/tooltip.tsx +5 -1
  199. package/src/context.tsx +10 -0
  200. package/src/create-alfakit.tsx +11 -2
  201. package/src/index.ts +2 -0
  202. package/src/layouts/app-layout.tsx +4 -28
  203. package/src/layouts/auth-layout.tsx +2 -2
  204. package/src/layouts/fullscreen-layout.tsx +1 -1
  205. package/src/layouts/lazy-layouts.ts +14 -0
  206. package/src/layouts/max-widths.ts +29 -0
  207. package/src/layouts/panel-layouts.ts +17 -0
  208. package/src/layouts/top-nav-layout.tsx +2 -2
  209. package/src/pages/page.tsx +21 -12
  210. package/src/standalone/alfakit-component.tsx +18 -7
  211. package/src/standalone/alfakit-toaster.tsx +21 -2
  212. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  213. package/src/starter-kit/impersonation-banner.tsx +48 -0
  214. package/src/starter-kit/index.ts +2 -0
  215. package/src/starter-kit/starter-kit.tsx +31 -4
  216. package/src/starter-kit/widgets-loaded.tsx +23 -1
  217. package/src/starter-kit/widgets-triggers.tsx +37 -2
  218. package/src/starter-kit/widgets.tsx +95 -20
  219. package/src/styles.css +15 -2
@@ -15,36 +15,12 @@ import { Topbar } from '../components/panel/topbar'
15
15
  import { SidebarInset, SidebarProvider, SidebarTrigger } from '../components/ui/sidebar'
16
16
  import { useAlfakitProps, useRegistry } from '../context'
17
17
  import { cn } from '../lib/utils'
18
+ import { MAX_WIDTHS } from './max-widths'
18
19
  import { TopNavLayout } from './top-nav-layout'
19
20
 
20
- export const MAX_WIDTHS: Record<string, string> = {
21
- '3xs': 'max-w-3xs',
22
- '2xs': 'max-w-2xs',
23
- xs: 'max-w-xs',
24
- sm: 'max-w-sm',
25
- md: 'max-w-md',
26
- lg: 'max-w-lg',
27
- xl: 'max-w-xl',
28
- '2xl': 'max-w-2xl',
29
- '3xl': 'max-w-3xl',
30
- '4xl': 'max-w-4xl',
31
- '5xl': 'max-w-5xl',
32
- '6xl': 'max-w-6xl',
33
- '7xl': 'max-w-7xl',
34
- none: 'max-w-none',
35
- min: 'max-w-min',
36
- max: 'max-w-max',
37
- fit: 'max-w-fit',
38
- prose: 'max-w-prose',
39
- container: 'container',
40
- 'screen-sm': 'max-w-(--breakpoint-sm)',
41
- 'screen-md': 'max-w-(--breakpoint-md)',
42
- 'screen-lg': 'max-w-(--breakpoint-lg)',
43
- 'screen-xl': 'max-w-(--breakpoint-xl)',
44
- 'screen-2xl': 'max-w-(--breakpoint-2xl)',
45
- full: 'max-w-full',
46
- screen: 'max-w-screen',
47
- }
21
+ // Re-exported for code that imported it from here; the map lives in `./max-widths` so the
22
+ // starter kit's content layout doesn't pull the panel chrome in.
23
+ export { MAX_WIDTHS } from './max-widths'
48
24
 
49
25
  export interface AppLayoutProps {
50
26
  children: ReactNode
@@ -1,12 +1,12 @@
1
1
  import type { ReactNode } from 'react'
2
- import { Brand } from '../components/panel/app-sidebar'
2
+ import { Brand } from '../components/panel/brand'
3
3
  import { LocaleSwitcher } from '../components/panel/locale-switcher'
4
4
  import { Slot } from '../components/panel/slot'
5
5
  import { ThemeToggle } from '../components/panel/theme-toggle'
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/ui/card'
7
7
  import { usePanel } from '../context'
8
8
  import { cn } from '../lib/utils'
9
- import { MAX_WIDTHS } from './app-layout'
9
+ import { MAX_WIDTHS } from './max-widths'
10
10
 
11
11
  export interface AuthLayoutProps {
12
12
  children: ReactNode
@@ -3,7 +3,7 @@ import { Link } from '@inertiajs/react'
3
3
  import { LogOutIcon } from 'lucide-react'
4
4
  import type { ReactNode } from 'react'
5
5
  import { NotificationsMenu } from '../components/notifications/notifications-menu'
6
- import { Brand } from '../components/panel/app-sidebar'
6
+ import { Brand } from '../components/panel/brand'
7
7
  import { DebugPanelSlot } from '../components/panel/debug-panel-slot'
8
8
  import { TopbarLocaleSwitcher } from '../components/panel/locale-switcher'
9
9
  import { Slot } from '../components/panel/slot'
@@ -0,0 +1,14 @@
1
+ import { lazyComponents } from '@alfakit/core'
2
+ import type { AnyComponent } from '../context'
3
+ import { discoveredComponent } from '../lib/discovery'
4
+
5
+ /**
6
+ * The fullscreen layout (`$layout = 'fullscreen'`), loaded on demand by starter kit pages:
7
+ * they render in the host's layout, so the page chunk never imports the panel's header,
8
+ * menus and notifications for a layout few pages use. Only the starter kit registers it;
9
+ * `createAlfakit()` registers the layouts themselves up front (`panelLayouts`), so a panel's
10
+ * bundle has no extra lazy entry points.
11
+ */
12
+ export const lazyPanelLayouts: Record<string, AnyComponent> = lazyComponents<AnyComponent>({
13
+ 'layout:fullscreen': () => import('./fullscreen-layout').then((m) => m.FullscreenLayout),
14
+ }).map(discoveredComponent).components
@@ -0,0 +1,29 @@
1
+ /** Tailwind classes for the `maxContentWidth` setting of the panel and its pages. */
2
+ export const MAX_WIDTHS: Record<string, string> = {
3
+ '3xs': 'max-w-3xs',
4
+ '2xs': 'max-w-2xs',
5
+ xs: 'max-w-xs',
6
+ sm: 'max-w-sm',
7
+ md: 'max-w-md',
8
+ lg: 'max-w-lg',
9
+ xl: 'max-w-xl',
10
+ '2xl': 'max-w-2xl',
11
+ '3xl': 'max-w-3xl',
12
+ '4xl': 'max-w-4xl',
13
+ '5xl': 'max-w-5xl',
14
+ '6xl': 'max-w-6xl',
15
+ '7xl': 'max-w-7xl',
16
+ none: 'max-w-none',
17
+ min: 'max-w-min',
18
+ max: 'max-w-max',
19
+ fit: 'max-w-fit',
20
+ prose: 'max-w-prose',
21
+ container: 'container',
22
+ 'screen-sm': 'max-w-(--breakpoint-sm)',
23
+ 'screen-md': 'max-w-(--breakpoint-md)',
24
+ 'screen-lg': 'max-w-(--breakpoint-lg)',
25
+ 'screen-xl': 'max-w-(--breakpoint-xl)',
26
+ 'screen-2xl': 'max-w-(--breakpoint-2xl)',
27
+ full: 'max-w-full',
28
+ screen: 'max-w-screen',
29
+ }
@@ -0,0 +1,17 @@
1
+ import type { AnyComponent } from '../context'
2
+ import { AppLayout } from './app-layout'
3
+ import { AuthLayout } from './auth-layout'
4
+ import { BlankLayout } from './blank-layout'
5
+ import { FullscreenLayout } from './fullscreen-layout'
6
+
7
+ /**
8
+ * The panel's own layouts (sidebar or top navigation, auth card, blank, fullscreen), which
9
+ * `createAlfakit()` registers up front. Starter kit pages render in the host's layout and
10
+ * never import these: `PageShell` loads one on demand if such a page asks for it.
11
+ */
12
+ export const panelLayouts: Record<string, AnyComponent> = {
13
+ 'layout:app': AppLayout,
14
+ 'layout:auth': AuthLayout,
15
+ 'layout:blank': BlankLayout,
16
+ 'layout:fullscreen': FullscreenLayout,
17
+ }
@@ -10,8 +10,8 @@ import { Link } from '@inertiajs/react'
10
10
  import { ChevronDownIcon, MenuIcon } from 'lucide-react'
11
11
  import { createElement, useState, type ReactNode } from 'react'
12
12
  import { NotificationsMenu } from '../components/notifications/notifications-menu'
13
- import { Brand } from '../components/panel/app-sidebar'
14
13
  import { BottomNav } from '../components/panel/bottom-nav'
14
+ import { Brand } from '../components/panel/brand'
15
15
  import { DebugPanelSlot } from '../components/panel/debug-panel-slot'
16
16
  import { GlobalSearch } from '../components/panel/global-search'
17
17
  import { TopbarLocaleSwitcher } from '../components/panel/locale-switcher'
@@ -34,7 +34,7 @@ import { extraProps } from '../lib/attributes'
34
34
  import { Icon } from '../lib/icons'
35
35
  import { NewTabHint } from '../lib/new-tab-hint'
36
36
  import { cn } from '../lib/utils'
37
- import { MAX_WIDTHS } from './app-layout'
37
+ import { MAX_WIDTHS } from './max-widths'
38
38
 
39
39
  export interface TopNavLayoutProps {
40
40
  children: ReactNode
@@ -47,11 +47,15 @@ import { DataTable } from '../components/table/table-block'
47
47
  import { Badge } from '../components/ui/badge'
48
48
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '../components/ui/tabs'
49
49
  import { WidgetGrid } from '../components/widgets/widgets-block'
50
- import { useActionRunner, useAlfakit, useAlfakitProps, usePanel, useT } from '../context'
51
- import { AppLayout } from '../layouts/app-layout'
52
- import { AuthLayout } from '../layouts/auth-layout'
50
+ import {
51
+ useActionRunner,
52
+ useAlfakit,
53
+ useAlfakitProps,
54
+ usePanel,
55
+ useT,
56
+ type AnyComponent,
57
+ } from '../context'
53
58
  import { BlankLayout } from '../layouts/blank-layout'
54
- import { FullscreenLayout } from '../layouts/fullscreen-layout'
55
59
  import { badgeColorClasses, colorClass } from '../lib/colors'
56
60
  import { Icon } from '../lib/icons'
57
61
  import { cn } from '../lib/utils'
@@ -138,6 +142,15 @@ export function useBodyAttributes(attributes: Record<string, string> | undefined
138
142
  )
139
143
  }
140
144
 
145
+ /**
146
+ * The registered layout: `createAlfakit()` registers the panel's (`panelLayouts`), the starter
147
+ * kit its content layout. Without one the page renders bare: importing the panel layouts here
148
+ * would put the sidebar and top bar in every starter kit page.
149
+ */
150
+ function layout(registry: { resolve(key: string): AnyComponent | undefined }, key: string) {
151
+ return registry.resolve(key) ?? BlankLayout
152
+ }
153
+
141
154
  export function PageShell({ page, children }: { page: PageSchema; children: ReactNode }) {
142
155
  const { registry } = useAlfakit()
143
156
 
@@ -146,26 +159,22 @@ export function PageShell({ page, children }: { page: PageSchema; children: Reac
146
159
  // Each layout can be replaced: createAlfakit({ components: { 'layout:app': MyLayout } }).
147
160
  if (page.layout === 'auth') {
148
161
  return createElement(
149
- registry.resolve('layout:auth') ?? AuthLayout,
162
+ layout(registry, 'layout:auth'),
150
163
  { heading: page.heading ?? page.title, subheading: page.subheading, page },
151
164
  children,
152
165
  )
153
166
  }
154
167
 
155
168
  if (page.layout === 'blank') {
156
- return createElement(registry.resolve('layout:blank') ?? BlankLayout, { page }, children)
169
+ return createElement(layout(registry, 'layout:blank'), { page }, children)
157
170
  }
158
171
 
159
172
  if (page.layout === 'fullscreen') {
160
- return createElement(
161
- registry.resolve('layout:fullscreen') ?? FullscreenLayout,
162
- { page },
163
- children,
164
- )
173
+ return createElement(layout(registry, 'layout:fullscreen'), { page }, children)
165
174
  }
166
175
 
167
176
  return createElement(
168
- registry.resolve('layout:app') ?? AppLayout,
177
+ layout(registry, 'layout:app'),
169
178
  { breadcrumbs: page.breadcrumbs, maxContentWidth: page.maxContentWidth, page },
170
179
  <>
171
180
  {/* `?` overlay, `/`, `g d` and panel command shortcuts (`keyboardShortcuts()`). */}
@@ -80,16 +80,25 @@ export function AlfakitComponent({
80
80
  const context = useMemo<AlfakitContextValue>(() => {
81
81
  const poll = (target: string, interval: string | number | null | undefined) =>
82
82
  state.poll(target, interval)
83
+ const refresh = (targets: string[]) => state.refresh(targets)
83
84
 
84
85
  if (parent) {
85
- return { ...parent, registry: createStandaloneRegistry(components, parent.registry), poll }
86
+ return {
87
+ ...parent,
88
+ registry: createStandaloneRegistry(components, parent.registry),
89
+ poll,
90
+ refresh,
91
+ }
86
92
  }
87
93
 
88
- return createStandaloneContext(
89
- { action: component.endpoints.action, modal: component.endpoints.modal },
90
- components,
91
- poll,
92
- )
94
+ return {
95
+ ...createStandaloneContext(
96
+ { action: component.endpoints.action, modal: component.endpoints.modal },
97
+ components,
98
+ poll,
99
+ ),
100
+ refresh,
101
+ }
93
102
  // eslint-disable-next-line react-hooks/exhaustive-deps
94
103
  }, [parent, state, overrideKeys])
95
104
 
@@ -122,7 +131,9 @@ export function AlfakitComponent({
122
131
  useEffect(() => registerStandaloneProp(propName ?? null), [propName])
123
132
 
124
133
  useEffect(() => {
125
- void state.loadLazy()
134
+ void state.loadLazy(component)
135
+ // The token identifies a new payload.
136
+ // eslint-disable-next-line react-hooks/exhaustive-deps
126
137
  }, [state, component.token])
127
138
 
128
139
  // Toasts: use the layout's <AlfakitToaster /> when there is one, else the first component
@@ -1,5 +1,6 @@
1
- import { resolveToastPosition, type ToastPosition } from '@alfakit/core'
1
+ import { resolveToastPosition, STARTER_KIT_TOASTER_ID, type ToastPosition } from '@alfakit/core'
2
2
  import { useEffect, useMemo } from 'react'
3
+ import { setToasterId } from '../components/notifications/flash-toasts'
3
4
  import { Toaster } from '../components/ui/sonner'
4
5
  import { useOptionalAlfakit, type AlfakitContextValue } from '../context'
5
6
  import { claimToaster, createStandaloneContext, ensureFlashToasts } from './runtime'
@@ -46,7 +47,25 @@ export function AlfakitToaster({
46
47
  ensureFlashToasts(context.registry)
47
48
  }, [context])
48
49
 
50
+ // Our own toaster gets an id, so a `<Toaster />` the app's layout already mounts (the
51
+ // starter kits have one) doesn't show every Alfakit toast a second time.
52
+ useEffect(() => {
53
+ if (!toaster) return undefined
54
+
55
+ setToasterId(STARTER_KIT_TOASTER_ID)
56
+
57
+ return () => setToasterId(undefined)
58
+ }, [toaster])
59
+
49
60
  if (!toaster) return null
50
61
 
51
- return <Toaster position={resolveToastPosition(position, dir)} dir={dir} richColors closeButton />
62
+ return (
63
+ <Toaster
64
+ id={STARTER_KIT_TOASTER_ID}
65
+ position={resolveToastPosition(position, dir)}
66
+ dir={dir}
67
+ richColors
68
+ closeButton
69
+ />
70
+ )
52
71
  }
@@ -0,0 +1,78 @@
1
+ import {
2
+ standaloneWidgetCount,
3
+ starterKitDashboardWidgets,
4
+ type StandaloneComponentSchema,
5
+ } from '@alfakit/core'
6
+ import { usePage } from '@inertiajs/react'
7
+ import type { ReactNode } from 'react'
8
+ import type { AnyComponent } from '../context'
9
+ import { lazyComponent } from '../lib/lazy'
10
+ import { cn } from '../lib/utils'
11
+
12
+ /** Props of `<AlfakitWidgets>`. */
13
+ export interface AlfakitWidgetsProps {
14
+ /** The `starterKit()` panel whose widgets to show (the first one by default). */
15
+ panel?: string
16
+ /** Frontend components of custom widgets (`{ RevenueWidget }`), as for `<AlfakitComponent>`. */
17
+ components?: Record<string, AnyComponent>
18
+ className?: string
19
+ /** Rendered instead when there are no widgets to show. */
20
+ children?: ReactNode
21
+ }
22
+
23
+ interface LoadedProps {
24
+ component: StandaloneComponentSchema
25
+ components?: Record<string, AnyComponent>
26
+ className?: string
27
+ }
28
+
29
+ const LoadedWidgets = lazyComponent<LoadedProps>(
30
+ () => import('../standalone/alfakit-component').then((module) => module.AlfakitComponent),
31
+ (props) => <WidgetsPlaceholder {...props} />,
32
+ )
33
+
34
+ /** Widget-sized boxes until the widgets' code has loaded (also during SSR). */
35
+ function WidgetsPlaceholder({ component, className }: LoadedProps) {
36
+ const count = standaloneWidgetCount(component)
37
+
38
+ return (
39
+ <div
40
+ data-slot="starter-kit-widgets-placeholder"
41
+ aria-busy="true"
42
+ className={cn('grid auto-rows-min gap-4 md:grid-cols-2', className)}
43
+ >
44
+ {Array.from({ length: Math.max(count, 1) }, (_, index) => (
45
+ <div key={index} aria-hidden="true" className="bg-muted h-28 animate-pulse rounded-xl" />
46
+ ))}
47
+ </div>
48
+ )
49
+ }
50
+
51
+ /**
52
+ * The dashboard widgets of a `starterKit()` panel on the host's own dashboard. Reads the
53
+ * `alfakitWidgets` prop (shared on the panel's `starterKitDashboard()` pages only, the
54
+ * starter kit's `/dashboard` by default) and renders the widgets with the host's theme, as
55
+ * `<AlfakitComponent>` renders standalone widgets: lazy widgets, polling and table widget
56
+ * actions go through the panel's endpoints. Its code loads in the browser; the server
57
+ * render and hydration show placeholders. Renders `children` (the kit's placeholder boxes)
58
+ * when there are no widgets to show: other pages, guests, users who may not open the panel.
59
+ *
60
+ * ```tsx
61
+ * import { AlfakitWidgets } from '@alfakit/react'
62
+ *
63
+ * <AlfakitWidgets>
64
+ * <MyEmptyState />
65
+ * </AlfakitWidgets>
66
+ * ```
67
+ */
68
+ export function AlfakitWidgets({ panel, components, className, children }: AlfakitWidgetsProps) {
69
+ const component = starterKitDashboardWidgets(usePage().props, panel)
70
+
71
+ if (!component) return <>{children}</>
72
+
73
+ return (
74
+ <div data-slot="starter-kit-widgets">
75
+ <LoadedWidgets component={component} components={components} className={className} />
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,48 @@
1
+ import type { ImpersonationSchema } from '@alfakit/core'
2
+ import { useEffect, useState, useSyncExternalStore } from 'react'
3
+ import { ImpersonationBanner } from '../components/panel/impersonation-banner'
4
+
5
+ // The header components appear in both header layouts and next to each other: the first
6
+ // one mounted shows the banner, the others render nothing.
7
+ let owner: symbol | null = null
8
+ const listeners = new Set<() => void>()
9
+
10
+ function setOwner(next: symbol | null): void {
11
+ owner = next
12
+ listeners.forEach((listener) => listener())
13
+ }
14
+
15
+ function subscribe(listener: () => void): () => void {
16
+ listeners.add(listener)
17
+
18
+ return () => listeners.delete(listener)
19
+ }
20
+
21
+ /**
22
+ * The impersonation banner (who you are signed in as, **Leave**) on the host's own pages,
23
+ * rendered by the header components. Loaded only while impersonating; AlfaKit's pages
24
+ * render their own (`AlfakitRoot`).
25
+ */
26
+ export default function StarterKitImpersonationBanner({
27
+ impersonation,
28
+ }: {
29
+ impersonation: ImpersonationSchema
30
+ }) {
31
+ const [id] = useState(() => Symbol('impersonation-banner'))
32
+ // Claimed once mounted (effects never run on the server), so SSR and hydration skip it.
33
+ const owns = useSyncExternalStore(
34
+ subscribe,
35
+ () => owner === id,
36
+ () => false,
37
+ )
38
+
39
+ useEffect(() => {
40
+ if (owner === null) setOwner(id)
41
+
42
+ return () => {
43
+ if (owner === id) setOwner(null)
44
+ }
45
+ }, [id])
46
+
47
+ return owns ? <ImpersonationBanner impersonation={impersonation} /> : null
48
+ }
@@ -1,3 +1,4 @@
1
+ export { AlfakitWidgets, type AlfakitWidgetsProps } from './dashboard-widgets'
1
2
  export { useAlfakitNavigation, type AlfakitNavigation } from './navigation'
2
3
  export {
3
4
  AlfakitStarterKitRoot,
@@ -7,6 +8,7 @@ export {
7
8
  type StarterKitPageOptions,
8
9
  } from './starter-kit'
9
10
  export {
11
+ AlfakitCopilot,
10
12
  AlfakitNotifications,
11
13
  AlfakitSearch,
12
14
  AlfakitTenantSwitcher,
@@ -8,6 +8,7 @@ import {
8
8
  resolveRequestErrorNotification,
9
9
  starterKitBreadcrumbsFromProps,
10
10
  syncStarterKitPage,
11
+ usesStarterKitHostLayout,
11
12
  type AlfakitOptions,
12
13
  type AlfakitPageProps,
13
14
  type StarterKitBreadcrumb,
@@ -23,7 +24,11 @@ import {
23
24
  type AnyComponent,
24
25
  } from '../context'
25
26
  import { AlfakitRoot } from '../create-alfakit'
26
- import { MAX_WIDTHS, type AppLayoutProps } from '../layouts/app-layout'
27
+ import type { AppLayoutProps } from '../layouts/app-layout'
28
+ import { AuthLayout } from '../layouts/auth-layout'
29
+ import { lazyPanelLayouts } from '../layouts/lazy-layouts'
30
+ import { MAX_WIDTHS } from '../layouts/max-widths'
31
+ import { discoverComponents } from '../lib/discovery'
27
32
  import { preloadIcons } from '../lib/icons'
28
33
  import { cn } from '../lib/utils'
29
34
  import { AlfakitPage } from '../pages/page'
@@ -45,6 +50,12 @@ export interface StarterKitPageOptions extends Pick<
45
50
  * `null` renders the pages without a host layout.
46
51
  */
47
52
  layout?: StarterKitLayout | null
53
+ /**
54
+ * Auto-registered custom components, as for `createAlfakit({ discover })`: pass
55
+ * `import.meta.glob('../../alfakit/{fields,columns,entries,widgets,pages,components,blocks}/**\/*.tsx')`
56
+ * from `resources/js/pages/Alfakit/Page.tsx` and every file there registers under its name.
57
+ */
58
+ discover?: AlfakitOptions<AnyComponent>['discover']
48
59
  }
49
60
 
50
61
  /**
@@ -78,14 +89,21 @@ export function StarterKitContentLayout({ children, maxContentWidth }: AppLayout
78
89
  let browserContext: AlfakitContextValue | null = null
79
90
 
80
91
  function createStarterKitContext(options: StarterKitPageOptions): AlfakitContextValue {
92
+ // The host's layout replaces the panel's. Auth pages (invitations, tenant registration)
93
+ // keep the panel's card; the fullscreen layout loads on demand.
94
+ // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.
95
+ const discovered = discoverComponents(options.discover)
81
96
  const registry = createDefaultRegistry()
97
+ .registerMany(lazyPanelLayouts)
98
+ .register('layout:auth', AuthLayout)
82
99
  .register('layout:app', StarterKitContentLayout)
100
+ .registerMany(discovered.components)
83
101
  .registerMany(options.components ?? {})
84
102
  const translator = new Translator()
85
103
 
86
104
  return {
87
105
  registry,
88
- pages: { 'Alfakit/Page': AlfakitPage, ...options.pages },
106
+ pages: { 'Alfakit/Page': AlfakitPage, ...discovered.pages, ...options.pages },
89
107
  translator,
90
108
  actions: new ActionRunner({
91
109
  endpoints: { action: '', modal: '' },
@@ -155,7 +173,16 @@ export function AlfakitStarterKitRoot({
155
173
 
156
174
  return (
157
175
  <AlfakitProvider value={context}>
158
- <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>{children}</AlfakitRoot>
176
+ {/* The reading direction and language of Alfakit's content only: the host layout around
177
+ it (sidebar, header) and the app's own pages keep the document's. */}
178
+ <div
179
+ data-slot="starter-kit-root"
180
+ dir={panel?.localization?.direction ?? 'ltr'}
181
+ lang={panel?.localization?.tag}
182
+ className="contents"
183
+ >
184
+ <AlfakitRoot toasterId={STARTER_KIT_TOASTER_ID}>{children}</AlfakitRoot>
185
+ </div>
159
186
  </AlfakitProvider>
160
187
  )
161
188
  }
@@ -186,7 +213,7 @@ export function createStarterKitPage(options: StarterKitPageOptions = {}) {
186
213
 
187
214
  // A layout callback: the host layout (with the page's breadcrumbs), then Alfakit's root.
188
215
  AlfakitStarterKitPage.layout = (props: Record<string, unknown>) => [
189
- ...(options.layout
216
+ ...(options.layout && usesStarterKitHostLayout(props)
190
217
  ? [[options.layout, { breadcrumbs: starterKitBreadcrumbsFromProps(props) }] as const]
191
218
  : []),
192
219
  [AlfakitStarterKitRoot, { options }] as const,
@@ -7,8 +7,10 @@ import {
7
7
  loadStarterKitPanel,
8
8
  loadedStarterKitPanel,
9
9
  openRequestedStarterKitSearch,
10
+ starterKitCopilot,
10
11
  starterKitWidgetsPage,
11
12
  starterKitWidgetsPanel,
13
+ takeRequestedStarterKitCopilot,
12
14
  tenantNameText,
13
15
  type PanelSchema,
14
16
  type StarterKitWidgets,
@@ -17,6 +19,7 @@ import { router, usePage } from '@inertiajs/react'
17
19
  import { ChevronsUpDownIcon, PlusIcon } from 'lucide-react'
18
20
  import { Direction } from 'radix-ui'
19
21
  import { useEffect, useMemo, useState, type ReactNode } from 'react'
22
+ import { AiCopilot } from '../components/ai/copilot'
20
23
  import { NotificationsMenu } from '../components/notifications/notifications-menu'
21
24
  import { RealtimeNotifications } from '../components/notifications/realtime-notifications'
22
25
  import { GlobalSearch } from '../components/panel/global-search'
@@ -41,6 +44,7 @@ import {
41
44
  } from '../context'
42
45
  import { Icon } from '../lib/icons'
43
46
  import {
47
+ CopilotTrigger,
44
48
  NotificationsTrigger,
45
49
  SearchTrigger,
46
50
  TenantSwitcherTrigger,
@@ -162,6 +166,24 @@ export function LoadedTenantSwitcher({ source }: WidgetProps) {
162
166
  )
163
167
  }
164
168
 
169
+ /** The AI copilot (the panel pages' `topbar.end` slot component). */
170
+ export function LoadedCopilot({ source }: WidgetProps) {
171
+ const panel = useWidgetsPanel(source)
172
+ // A click or the key bindings before the copilot loaded open it at once.
173
+ const [autoOpen] = useState(() => takeRequestedStarterKitCopilot())
174
+ const component = useMemo(() => (panel ? starterKitCopilot(panel) : null), [panel])
175
+
176
+ if (!panel) return <CopilotTrigger source={source} />
177
+
178
+ if (!component) return null
179
+
180
+ return (
181
+ <WidgetShell panel={panel}>
182
+ <AiCopilot component={component} autoOpen={autoOpen || takeRequestedStarterKitCopilot()} />
183
+ </WidgetShell>
184
+ )
185
+ }
186
+
165
187
  /** The user's tenants on a page outside the panel (no current tenant). */
166
188
  function TenantChooser({ panel }: { panel: PanelSchema }) {
167
189
  const t = useT()
@@ -207,7 +229,7 @@ function TenantChooser({ panel }: { panel: PanelSchema }) {
207
229
  {tenancy.tenants.length > 0 && <DropdownMenuSeparator />}
208
230
  <DropdownMenuItem onSelect={() => router.visit(tenancy.registrationUrl!)}>
209
231
  <Icon name="alfakit::panels.tenant-menu.registration-button" fallback={PlusIcon} />
210
- {t('tenancy.register', {}, 'New')}
232
+ {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}
211
233
  </DropdownMenuItem>
212
234
  </>
213
235
  )}
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  formatKeyBinding,
3
3
  initials,
4
+ registerStarterKitCopilotShortcut,
4
5
  registerStarterKitSearchShortcut,
6
+ requestStarterKitCopilot,
5
7
  requestStarterKitSearch,
6
8
  tenantNameText,
7
9
  type StarterKitWidgets,
8
10
  } from '@alfakit/core'
9
- import { BellIcon, ChevronsUpDownIcon, SearchIcon } from 'lucide-react'
11
+ import { BellIcon, ChevronsUpDownIcon, SearchIcon, SparklesIcon } from 'lucide-react'
10
12
  import { useEffect } from 'react'
11
13
  import { useHydrated } from '../hooks/use-hydrated'
12
14
 
@@ -49,7 +51,7 @@ export function SearchTrigger({ source, onLoad }: WidgetProps & { onLoad?: () =>
49
51
  <SearchIcon className="size-4" aria-hidden="true" />
50
52
  <span className="sr-only sm:not-sr-only sm:flex-1 sm:text-start">{placeholder}</span>
51
53
  {hydrated && binding && (
52
- <kbd className="bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline">
54
+ <kbd dir="ltr" className="bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline">
53
55
  {formatKeyBinding(binding)}
54
56
  </kbd>
55
57
  )}
@@ -101,3 +103,36 @@ export function TenantSwitcherTrigger({ source }: WidgetProps) {
101
103
  </button>
102
104
  )
103
105
  }
106
+
107
+ /**
108
+ * The AI copilot's button until its code has loaded: the same look, and it remembers a
109
+ * click or its key bindings (`mod+j`), so the copilot opens once loaded.
110
+ */
111
+ export function CopilotTrigger({ source, onLoad }: WidgetProps & { onLoad?: () => void }) {
112
+ const copilot = source.widgets.copilot
113
+ const keys = JSON.stringify(copilot?.keyBindings ?? [])
114
+ const label = copilot?.label ?? ''
115
+ // `onLoad` is a stable function (it starts loading the copilot's code).
116
+ useEffect(
117
+ () => registerStarterKitCopilotShortcut(JSON.parse(keys) as string[], label, () => onLoad?.()),
118
+ [keys, label, onLoad],
119
+ )
120
+
121
+ return (
122
+ <button
123
+ type="button"
124
+ data-slot="ai-copilot-trigger"
125
+ aria-label={label}
126
+ aria-haspopup="dialog"
127
+ title={label}
128
+ className="hover:bg-accent hover:text-accent-foreground inline-flex size-9 items-center justify-center rounded-md"
129
+ onPointerEnter={() => onLoad?.()}
130
+ onClick={() => {
131
+ requestStarterKitCopilot()
132
+ onLoad?.()
133
+ }}
134
+ >
135
+ <SparklesIcon className="size-4" aria-hidden="true" />
136
+ </button>
137
+ )
138
+ }