@alfakit/react 0.1.2 → 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 (171) hide show
  1. package/CHANGELOG.md +8 -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
@@ -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
@@ -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()
@@ -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
 
@@ -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
+ }
@@ -1,7 +1,14 @@
1
- import { starterKitWidgets, type StarterKitWidgets } from '@alfakit/core'
1
+ import {
2
+ isAlfakitPage,
3
+ starterKitWidgets,
4
+ type StarterKitWidgets,
5
+ type StarterKitWidgetsSchema,
6
+ } from '@alfakit/core'
2
7
  import { usePage } from '@inertiajs/react'
8
+ import { lazy, Suspense } from 'react'
3
9
  import { lazyComponent } from '../lib/lazy'
4
10
  import {
11
+ CopilotTrigger,
5
12
  NotificationsTrigger,
6
13
  SearchTrigger,
7
14
  TenantSwitcherTrigger,
@@ -16,14 +23,15 @@ import {
16
23
  * (in the browser, never during SSR).
17
24
  *
18
25
  * ```tsx
19
- * import { AlfakitNotifications, AlfakitSearch } from '@alfakit/react'
26
+ * import { AlfakitCopilot, AlfakitNotifications, AlfakitSearch } from '@alfakit/react'
20
27
  *
21
28
  * <AlfakitSearch />
22
29
  * <AlfakitNotifications />
30
+ * <AlfakitCopilot />
23
31
  * ```
24
32
  */
25
33
  export interface AlfakitHeaderWidgetProps {
26
- /** The `starterKit()` panel to use (the first one by default). */
34
+ /** The `starterKit()` panel to use (by default the open AlfaKit page's panel, else the first one that has this component). */
27
35
  panel?: string
28
36
  }
29
37
 
@@ -48,8 +56,37 @@ const LoadedTenantSwitcher = lazyComponent<WidgetProps>(
48
56
  (props) => <TenantSwitcherTrigger {...props} />,
49
57
  )
50
58
 
51
- function useWidgets(panelId: string | undefined): StarterKitWidgets | null {
52
- return starterKitWidgets(usePage().props, panelId)
59
+ const LoadedCopilot = lazyComponent<WidgetProps>(
60
+ () => load().then((module) => module.LoadedCopilot),
61
+ (props) => <CopilotTrigger {...props} onLoad={preloadCopilot} />,
62
+ )
63
+
64
+ function preloadCopilot() {
65
+ void LoadedCopilot.preload()
66
+ }
67
+
68
+ // Only needed while impersonating: its code loads then (and never on AlfaKit pages).
69
+ const LazyImpersonationBanner = lazy(() => import('./impersonation-banner'))
70
+
71
+ /** While impersonating, the first header component shows the banner on host pages. */
72
+ function StarterKitImpersonationBanner({ source }: { source: StarterKitWidgets | null }) {
73
+ const props = usePage().props
74
+ const impersonation = source?.widgets.impersonation
75
+
76
+ if (!impersonation || isAlfakitPage(props)) return null
77
+
78
+ return (
79
+ <Suspense fallback={null}>
80
+ <LazyImpersonationBanner impersonation={impersonation} />
81
+ </Suspense>
82
+ )
83
+ }
84
+
85
+ function useWidgets(
86
+ panelId: string | undefined,
87
+ part: keyof StarterKitWidgetsSchema,
88
+ ): StarterKitWidgets | null {
89
+ return starterKitWidgets(usePage().props, panelId, part)
53
90
  }
54
91
 
55
92
  /**
@@ -58,14 +95,19 @@ function useWidgets(panelId: string | undefined): StarterKitWidgets | null {
58
95
  * nothing for guests or without global search and a command palette.
59
96
  */
60
97
  export function AlfakitSearch({ panel }: AlfakitHeaderWidgetProps = {}) {
61
- const source = useWidgets(panel)
98
+ const source = useWidgets(panel, 'search')
62
99
 
63
- if (!source?.widgets.search) return null
100
+ const banner = <StarterKitImpersonationBanner source={source} />
101
+
102
+ if (!source?.widgets.search) return banner
64
103
 
65
104
  return (
66
- <span data-slot="starter-kit-search" className="inline-flex">
67
- <LoadedSearch source={source} />
68
- </span>
105
+ <>
106
+ {banner}
107
+ <span data-slot="starter-kit-search" className="inline-flex">
108
+ <LoadedSearch source={source} />
109
+ </span>
110
+ </>
69
111
  )
70
112
  }
71
113
 
@@ -75,14 +117,19 @@ export function AlfakitSearch({ panel }: AlfakitHeaderWidgetProps = {}) {
75
117
  * notifications.
76
118
  */
77
119
  export function AlfakitNotifications({ panel }: AlfakitHeaderWidgetProps = {}) {
78
- const source = useWidgets(panel)
120
+ const source = useWidgets(panel, 'notifications')
121
+
122
+ const banner = <StarterKitImpersonationBanner source={source} />
79
123
 
80
- if (!source?.widgets.notifications) return null
124
+ if (!source?.widgets.notifications) return banner
81
125
 
82
126
  return (
83
- <span data-slot="starter-kit-notifications" className="inline-flex">
84
- <LoadedNotifications source={source} />
85
- </span>
127
+ <>
128
+ {banner}
129
+ <span data-slot="starter-kit-notifications" className="inline-flex">
130
+ <LoadedNotifications source={source} />
131
+ </span>
132
+ </>
86
133
  )
87
134
  }
88
135
 
@@ -91,13 +138,41 @@ export function AlfakitNotifications({ panel }: AlfakitHeaderWidgetProps = {}) {
91
138
  * to choose one on the host's own pages. Renders nothing for guests or other panels.
92
139
  */
93
140
  export function AlfakitTenantSwitcher({ panel }: AlfakitHeaderWidgetProps = {}) {
94
- const source = useWidgets(panel)
141
+ const source = useWidgets(panel, 'tenancy')
142
+
143
+ const banner = <StarterKitImpersonationBanner source={source} />
144
+
145
+ if (!source?.widgets.tenancy) return banner
146
+
147
+ return (
148
+ <>
149
+ {banner}
150
+ <span data-slot="starter-kit-tenant-switcher" className="inline-flex">
151
+ <LoadedTenantSwitcher source={source} />
152
+ </span>
153
+ </>
154
+ )
155
+ }
156
+
157
+ /**
158
+ * The AI copilot of alfasix/alfakit-ai (`AiPlugin::copilot()`): its button and chat
159
+ * slide-over, opened with its key bindings (`mod+j`) from any page. Renders nothing for
160
+ * guests, without the plugin's copilot, for users who may not use it and (tenancy panels)
161
+ * outside a tenant.
162
+ */
163
+ export function AlfakitCopilot({ panel }: AlfakitHeaderWidgetProps = {}) {
164
+ const source = useWidgets(panel, 'copilot')
165
+
166
+ const banner = <StarterKitImpersonationBanner source={source} />
95
167
 
96
- if (!source?.widgets.tenancy) return null
168
+ if (!source?.widgets.copilot) return banner
97
169
 
98
170
  return (
99
- <span data-slot="starter-kit-tenant-switcher" className="inline-flex">
100
- <LoadedTenantSwitcher source={source} />
101
- </span>
171
+ <>
172
+ {banner}
173
+ <span data-slot="starter-kit-copilot" className="inline-flex">
174
+ <LoadedCopilot source={source} />
175
+ </span>
176
+ </>
102
177
  )
103
178
  }