@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.
- package/CHANGELOG.md +8 -0
- package/dist/components/actions/action-modals.js +4 -0
- package/dist/components/actions/action-modals.js.map +1 -1
- package/dist/components/ai/copilot.d.ts +3 -1
- package/dist/components/ai/copilot.d.ts.map +1 -1
- package/dist/components/ai/copilot.js +4 -1
- package/dist/components/ai/copilot.js.map +1 -1
- package/dist/components/cms/live-preview.js +3 -2
- package/dist/components/cms/live-preview.js.map +1 -1
- package/dist/components/cms/page.js +1 -1
- package/dist/components/fields/builder.js +15 -6
- package/dist/components/fields/builder.js.map +1 -1
- package/dist/components/fields/image-editor-dialog.js +4 -2
- package/dist/components/fields/image-editor-dialog.js.map +1 -1
- package/dist/components/fields/item-actions.js +10 -10
- package/dist/components/fields/item-actions.js.map +1 -1
- package/dist/components/fields/key-value.d.ts.map +1 -1
- package/dist/components/fields/key-value.js +3 -3
- package/dist/components/fields/key-value.js.map +1 -1
- package/dist/components/fields/modal-table-select.js +6 -5
- package/dist/components/fields/modal-table-select.js.map +1 -1
- package/dist/components/fields/repeater.js +2 -2
- package/dist/components/fields/repeater.js.map +1 -1
- package/dist/components/fields/rich-editor.js +2 -1
- package/dist/components/fields/rich-editor.js.map +1 -1
- package/dist/components/form/form-block.js +1 -1
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/form/form-context.js +2 -2
- package/dist/components/form/form-context.js.map +1 -1
- package/dist/components/kanban/kanban.js +4 -3
- package/dist/components/kanban/kanban.js.map +1 -1
- package/dist/components/layout/section.js +2 -2
- package/dist/components/layout/section.js.map +1 -1
- package/dist/components/panel/app-sidebar.d.ts +2 -3
- package/dist/components/panel/app-sidebar.d.ts.map +1 -1
- package/dist/components/panel/app-sidebar.js +1 -24
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/brand.d.ts +10 -0
- package/dist/components/panel/brand.d.ts.map +1 -0
- package/dist/components/panel/brand.js +37 -0
- package/dist/components/panel/brand.js.map +1 -0
- package/dist/components/panel/index.d.ts +2 -1
- package/dist/components/panel/topbar.js +1 -1
- package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
- package/dist/components/realtime/live-records-pill.js +6 -3
- package/dist/components/realtime/live-records-pill.js.map +1 -1
- package/dist/components/table/cells.d.ts +6 -0
- package/dist/components/table/cells.d.ts.map +1 -1
- package/dist/components/table/cells.js +6 -2
- package/dist/components/table/cells.js.map +1 -1
- package/dist/components/table/column-manager.js +8 -7
- package/dist/components/table/column-manager.js.map +1 -1
- package/dist/components/table/content-layout.js +2 -1
- package/dist/components/table/content-layout.js.map +1 -1
- package/dist/components/table/pagination.js +16 -13
- package/dist/components/table/pagination.js.map +1 -1
- package/dist/components/table/table-block.js +14 -4
- package/dist/components/table/table-block.js.map +1 -1
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +3 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/popover.js +3 -1
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +3 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/tooltip.js +3 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -1
- package/dist/context.js.map +1 -1
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +8 -5
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +16 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +24 -21
- package/dist/layouts/app-layout.d.ts.map +1 -1
- package/dist/layouts/app-layout.js +2 -29
- package/dist/layouts/app-layout.js.map +1 -1
- package/dist/layouts/auth-layout.js +2 -2
- package/dist/layouts/auth-layout.js.map +1 -1
- package/dist/layouts/fullscreen-layout.js +1 -1
- package/dist/layouts/fullscreen-layout.js.map +1 -1
- package/dist/layouts/lazy-layouts.js +15 -0
- package/dist/layouts/lazy-layouts.js.map +1 -0
- package/dist/layouts/max-widths.js +34 -0
- package/dist/layouts/max-widths.js.map +1 -0
- package/dist/layouts/panel-layouts.d.ts +10 -0
- package/dist/layouts/panel-layouts.d.ts.map +1 -0
- package/dist/layouts/panel-layouts.js +20 -0
- package/dist/layouts/panel-layouts.js.map +1 -0
- package/dist/layouts/top-nav-layout.js +2 -2
- package/dist/layouts/top-nav-layout.js.map +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +12 -7
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +11 -6
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone.css +7 -0
- package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
- package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
- package/dist/starter-kit/dashboard-widgets.js +53 -0
- package/dist/starter-kit/dashboard-widgets.js.map +1 -0
- package/dist/starter-kit/impersonation-banner.js +34 -0
- package/dist/starter-kit/impersonation-banner.js.map +1 -0
- package/dist/starter-kit/index.d.ts +3 -2
- package/dist/starter-kit/index.js +3 -2
- package/dist/starter-kit/starter-kit.d.ts +6 -0
- package/dist/starter-kit/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit/starter-kit.js +18 -7
- package/dist/starter-kit/starter-kit.js.map +1 -1
- package/dist/starter-kit/widgets-loaded.js +20 -4
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +34 -3
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/starter-kit/widgets.d.ts +13 -5
- package/dist/starter-kit/widgets.d.ts.map +1 -1
- package/dist/starter-kit/widgets.js +56 -18
- package/dist/starter-kit/widgets.js.map +1 -1
- package/dist/styles.css +15 -2
- package/dist/themes.css +6 -1
- package/package.json +2 -2
- package/src/components/actions/action-modals.tsx +10 -0
- package/src/components/ai/copilot.tsx +14 -1
- package/src/components/cms/live-preview.tsx +5 -2
- package/src/components/fields/builder.tsx +14 -5
- package/src/components/fields/image-editor-dialog.tsx +7 -2
- package/src/components/fields/item-actions.tsx +32 -9
- package/src/components/fields/key-value.tsx +3 -2
- package/src/components/fields/modal-table-select.tsx +8 -3
- package/src/components/fields/repeater.tsx +2 -3
- package/src/components/fields/rich-editor.tsx +3 -0
- package/src/components/form/form-block.tsx +2 -1
- package/src/components/form/form-context.tsx +2 -2
- package/src/components/kanban/kanban.tsx +16 -3
- package/src/components/layout/section.tsx +2 -2
- package/src/components/panel/app-sidebar.tsx +2 -38
- package/src/components/panel/brand.tsx +46 -0
- package/src/components/realtime/live-records-pill.tsx +4 -3
- package/src/components/table/cells.tsx +9 -2
- package/src/components/table/column-manager.tsx +12 -10
- package/src/components/table/content-layout.tsx +5 -1
- package/src/components/table/pagination.tsx +16 -9
- package/src/components/table/table-block.tsx +25 -12
- package/src/components/ui/dialog.tsx +5 -1
- package/src/components/ui/popover.tsx +5 -1
- package/src/components/ui/sheet.tsx +5 -1
- package/src/components/ui/tooltip.tsx +5 -1
- package/src/context.tsx +10 -0
- package/src/create-alfakit.tsx +11 -2
- package/src/index.ts +2 -0
- package/src/layouts/app-layout.tsx +4 -28
- package/src/layouts/auth-layout.tsx +2 -2
- package/src/layouts/fullscreen-layout.tsx +1 -1
- package/src/layouts/lazy-layouts.ts +14 -0
- package/src/layouts/max-widths.ts +29 -0
- package/src/layouts/panel-layouts.ts +17 -0
- package/src/layouts/top-nav-layout.tsx +2 -2
- package/src/pages/page.tsx +21 -12
- package/src/standalone/alfakit-component.tsx +18 -7
- package/src/starter-kit/dashboard-widgets.tsx +78 -0
- package/src/starter-kit/impersonation-banner.tsx +48 -0
- package/src/starter-kit/index.ts +2 -0
- package/src/starter-kit/starter-kit.tsx +31 -4
- package/src/starter-kit/widgets-loaded.tsx +22 -0
- package/src/starter-kit/widgets-triggers.tsx +36 -1
- package/src/starter-kit/widgets.tsx +95 -20
- 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 './
|
|
37
|
+
import { MAX_WIDTHS } from './max-widths'
|
|
38
38
|
|
|
39
39
|
export interface TopNavLayoutProps {
|
|
40
40
|
children: ReactNode
|
package/src/pages/page.tsx
CHANGED
|
@@ -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 {
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
|
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
|
|
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 {
|
|
86
|
+
return {
|
|
87
|
+
...parent,
|
|
88
|
+
registry: createStandaloneRegistry(components, parent.registry),
|
|
89
|
+
poll,
|
|
90
|
+
refresh,
|
|
91
|
+
}
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
return
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+
}
|
package/src/starter-kit/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
100
|
+
const banner = <StarterKitImpersonationBanner source={source} />
|
|
101
|
+
|
|
102
|
+
if (!source?.widgets.search) return banner
|
|
64
103
|
|
|
65
104
|
return (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
124
|
+
if (!source?.widgets.notifications) return banner
|
|
81
125
|
|
|
82
126
|
return (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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.
|
|
168
|
+
if (!source?.widgets.copilot) return banner
|
|
97
169
|
|
|
98
170
|
return (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
171
|
+
<>
|
|
172
|
+
{banner}
|
|
173
|
+
<span data-slot="starter-kit-copilot" className="inline-flex">
|
|
174
|
+
<LoadedCopilot source={source} />
|
|
175
|
+
</span>
|
|
176
|
+
</>
|
|
102
177
|
)
|
|
103
178
|
}
|