@alfakit/react 0.1.12 → 0.2.0
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 +12 -0
- package/dist/components/cms/blocks.d.ts.map +1 -1
- package/dist/components/cms/blocks.js +2 -2
- package/dist/components/cms/blocks.js.map +1 -1
- package/dist/components/cms/head.d.ts +6 -4
- package/dist/components/cms/head.d.ts.map +1 -1
- package/dist/components/cms/head.js +10 -4
- package/dist/components/cms/head.js.map +1 -1
- package/dist/components/cms/page.d.ts.map +1 -1
- package/dist/components/cms/page.js +16 -3
- package/dist/components/cms/page.js.map +1 -1
- package/dist/components/fields/file-upload.js +4 -4
- package/dist/components/fields/file-upload.js.map +1 -1
- package/dist/components/fields/rich-editor.js +4 -2
- package/dist/components/fields/rich-editor.js.map +1 -1
- package/dist/components/fields/slider.js +1 -1
- package/dist/components/fields/slider.js.map +1 -1
- package/dist/components/helpdesk/chat-widget.js +1 -1
- package/dist/components/layout/section.js +1 -1
- package/dist/components/layout/tabs.js +1 -1
- package/dist/components/panel/app-sidebar.d.ts.map +1 -1
- package/dist/components/panel/app-sidebar.js +27 -5
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/global-search.js +2 -2
- package/dist/components/panel/global-search.js.map +1 -1
- package/dist/components/panel/shortcut-keys.js +1 -1
- package/dist/components/panel/shortcut-keys.js.map +1 -1
- package/dist/components/table/list-tabs.js +1 -1
- package/dist/components/table/list-tabs.js.map +1 -1
- package/dist/components/table/table-views.js +1 -1
- package/dist/components/table/table-views.js.map +1 -1
- package/dist/components/versions/record-versions.js +1 -1
- package/dist/context.d.ts +8 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +27 -2
- package/dist/context.js.map +1 -1
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +6 -3
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +2 -2
- package/dist/index.js +1 -1
- package/dist/layouts/app-layout.d.ts.map +1 -1
- package/dist/layouts/app-layout.js +13 -3
- package/dist/layouts/app-layout.js.map +1 -1
- package/dist/lib/discovery.d.ts +4 -0
- package/dist/lib/discovery.d.ts.map +1 -1
- package/dist/lib/discovery.js +17 -7
- package/dist/lib/discovery.js.map +1 -1
- package/dist/lib/icons.d.ts +2 -1
- package/dist/lib/icons.d.ts.map +1 -1
- package/dist/lib/icons.js +55 -19
- package/dist/lib/icons.js.map +1 -1
- package/dist/pages/page.d.ts +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +47 -10
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +2 -1
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone/alfakit-toaster.js +2 -1
- package/dist/standalone/alfakit-toaster.js.map +1 -1
- package/dist/standalone/runtime.js +1 -11
- package/dist/standalone/runtime.js.map +1 -1
- package/dist/standalone/toaster-claims.js +16 -0
- package/dist/standalone/toaster-claims.js.map +1 -0
- package/dist/starter-kit/widgets-loaded.js +63 -6
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +1 -1
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/themes.css +92 -0
- package/package.json +2 -2
- package/src/components/cms/blocks.tsx +7 -1
- package/src/components/cms/head.tsx +19 -4
- package/src/components/cms/page.tsx +17 -2
- package/src/components/fields/file-upload.tsx +5 -4
- package/src/components/fields/rich-editor.tsx +7 -2
- package/src/components/fields/slider.tsx +3 -1
- package/src/components/panel/app-sidebar.tsx +28 -7
- package/src/components/panel/global-search.tsx +4 -1
- package/src/components/panel/shortcut-keys.tsx +1 -1
- package/src/components/table/list-tabs.tsx +3 -1
- package/src/components/table/table-views.tsx +3 -1
- package/src/context.tsx +46 -1
- package/src/create-alfakit.tsx +13 -2
- package/src/layouts/app-layout.tsx +16 -3
- package/src/lib/discovery.tsx +23 -8
- package/src/lib/icons.tsx +90 -24
- package/src/pages/page.tsx +85 -27
- package/src/standalone/runtime.ts +2 -14
- package/src/standalone/toaster-claims.ts +16 -0
- package/src/starter-kit/widgets-loaded.tsx +84 -1
- package/src/starter-kit/widgets-triggers.tsx +4 -1
package/src/pages/page.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
AlfakitPageProps,
|
|
2
3
|
ContentBlock,
|
|
3
4
|
FormBlock as FormBlockSchema,
|
|
4
5
|
InfolistBlock,
|
|
@@ -24,8 +25,10 @@ import {
|
|
|
24
25
|
} from '@alfakit/core'
|
|
25
26
|
import { Head, Link, usePage } from '@inertiajs/react'
|
|
26
27
|
import {
|
|
28
|
+
createContext,
|
|
27
29
|
createElement,
|
|
28
30
|
Fragment,
|
|
31
|
+
useContext,
|
|
29
32
|
useEffect,
|
|
30
33
|
useId,
|
|
31
34
|
useMemo,
|
|
@@ -155,13 +158,24 @@ function layout(registry: { resolve(key: string): AnyComponent | undefined }, ke
|
|
|
155
158
|
return registry.resolve(key) ?? BlankLayout
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
|
|
159
|
-
const { registry } = useAlfakit()
|
|
161
|
+
type LayoutKind = 'app' | 'auth' | 'blank' | 'fullscreen'
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
function layoutKind(page: PageSchema): LayoutKind {
|
|
164
|
+
return page.layout === 'auth' || page.layout === 'blank' || page.layout === 'fullscreen'
|
|
165
|
+
? page.layout
|
|
166
|
+
: 'app'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The layout `PanelLayoutHost` already renders around the page, if any. */
|
|
170
|
+
const HostedLayoutContext = createContext<LayoutKind | null>(null)
|
|
171
|
+
|
|
172
|
+
/** The page's registered layout (`layout:app`, `layout:auth`…) around `children`. */
|
|
173
|
+
function PanelLayout({ page, children }: { page: PageSchema; children: ReactNode }) {
|
|
174
|
+
const { registry } = useAlfakit()
|
|
175
|
+
const kind = layoutKind(page)
|
|
162
176
|
|
|
163
177
|
// Each layout can be replaced: createAlfakit({ components: { 'layout:app': MyLayout } }).
|
|
164
|
-
if (
|
|
178
|
+
if (kind === 'auth') {
|
|
165
179
|
return createElement(
|
|
166
180
|
layout(registry, 'layout:auth'),
|
|
167
181
|
{ heading: page.heading ?? page.title, subheading: page.subheading, page },
|
|
@@ -169,33 +183,77 @@ export function PageShell({ page, children }: { page: PageSchema; children: Reac
|
|
|
169
183
|
)
|
|
170
184
|
}
|
|
171
185
|
|
|
172
|
-
if (
|
|
173
|
-
return createElement(
|
|
186
|
+
if (kind === 'app') {
|
|
187
|
+
return createElement(
|
|
188
|
+
layout(registry, 'layout:app'),
|
|
189
|
+
{ breadcrumbs: page.breadcrumbs, maxContentWidth: page.maxContentWidth, page },
|
|
190
|
+
children,
|
|
191
|
+
)
|
|
174
192
|
}
|
|
175
193
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
194
|
+
return createElement(layout(registry, `layout:${kind}`), { page }, children)
|
|
195
|
+
}
|
|
179
196
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
) : (
|
|
194
|
-
children
|
|
195
|
-
)}
|
|
196
|
-
<Slot name="page.end" />
|
|
197
|
-
</>,
|
|
197
|
+
/**
|
|
198
|
+
* Keeps the panel layout (sidebar, top bar) mounted while the user moves between Alfakit
|
|
199
|
+
* pages. Inertia remounts the page component on every visit; this sits in the persistent
|
|
200
|
+
* Inertia layout (`AlfakitRoot`) and renders the open page's layout from its props, so only
|
|
201
|
+
* the page part below it (heading, actions, content) is replaced. The `PageShell` inside
|
|
202
|
+
* the page then renders just that part. Custom page components (their own Inertia page or
|
|
203
|
+
* `page.view`) and starter kit pages render their layout through `PageShell` as before.
|
|
204
|
+
*/
|
|
205
|
+
export function PanelLayoutHost({ children }: { children: ReactNode }) {
|
|
206
|
+
const { component, props } = usePage<AlfakitPageProps>()
|
|
207
|
+
const page = useMemo(
|
|
208
|
+
() => (props.page ? resolveLazyComponents(props.page, props.alfakitLazy) : undefined),
|
|
209
|
+
[props.page, props.alfakitLazy],
|
|
198
210
|
)
|
|
211
|
+
const hosted =
|
|
212
|
+
component === 'Alfakit/Page' && page && !page.view && props.alfakit?.starterKit !== true
|
|
213
|
+
? page
|
|
214
|
+
: undefined
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<HostedLayoutContext.Provider value={hosted ? layoutKind(hosted) : null}>
|
|
218
|
+
{hosted ? <PanelLayout page={hosted}>{children}</PanelLayout> : children}
|
|
219
|
+
</HostedLayoutContext.Provider>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function PageShell({ page, children }: { page: PageSchema; children: ReactNode }) {
|
|
224
|
+
const { registry } = useAlfakit()
|
|
225
|
+
const hosted = useContext(HostedLayoutContext)
|
|
226
|
+
const kind = layoutKind(page)
|
|
227
|
+
|
|
228
|
+
useBodyAttributes(page.extraBodyAttributes)
|
|
229
|
+
|
|
230
|
+
const content =
|
|
231
|
+
kind === 'app' ? (
|
|
232
|
+
<>
|
|
233
|
+
{/* `?` overlay, `/`, `g d` and panel command shortcuts (`keyboardShortcuts()`). */}
|
|
234
|
+
<KeyboardShortcuts />
|
|
235
|
+
<Slot name="page.start" />
|
|
236
|
+
<PageHeader page={page} />
|
|
237
|
+
{/* Edit pages: somebody else saved the record (`detectConflicts()`, `Panel::realtime()`). */}
|
|
238
|
+
{page.conflicts &&
|
|
239
|
+
createElement(registry.resolve('page:conflict-banner') ?? ConflictBanner, { page })}
|
|
240
|
+
{page.subNavigation ? (
|
|
241
|
+
<SubNavigationLayout navigation={page.subNavigation}>{children}</SubNavigationLayout>
|
|
242
|
+
) : (
|
|
243
|
+
children
|
|
244
|
+
)}
|
|
245
|
+
<Slot name="page.end" />
|
|
246
|
+
</>
|
|
247
|
+
) : (
|
|
248
|
+
children
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
// The persistent layout around the page already draws this layout.
|
|
252
|
+
if (hosted === kind) {
|
|
253
|
+
return content
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return <PanelLayout page={page}>{content}</PanelLayout>
|
|
199
257
|
}
|
|
200
258
|
|
|
201
259
|
export function PageHeader({ page }: { page: PageSchema }) {
|
|
@@ -95,20 +95,8 @@ export function createStandaloneContext(
|
|
|
95
95
|
|
|
96
96
|
// ---------------------------------------------------------------- Toaster ownership
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
|
|
101
|
-
export function claimToaster(): () => void {
|
|
102
|
-
toasters++
|
|
103
|
-
|
|
104
|
-
return () => {
|
|
105
|
-
toasters--
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function hasToaster(): boolean {
|
|
110
|
-
return toasters > 0
|
|
111
|
-
}
|
|
98
|
+
// In a module of its own: the starter kit header components check it without loading this one.
|
|
99
|
+
export { claimToaster, hasToaster } from './toaster-claims'
|
|
112
100
|
|
|
113
101
|
// ---------------------------------------------------------------- Flashed notifications
|
|
114
102
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Who renders AlfaKit's toaster on the page: `<AlfakitToaster>`, `AlfakitRoot`, the first
|
|
2
|
+
// standalone component or the starter kit header components on the host's own pages.
|
|
3
|
+
let toasters = 0
|
|
4
|
+
|
|
5
|
+
/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
|
|
6
|
+
export function claimToaster(): () => void {
|
|
7
|
+
toasters++
|
|
8
|
+
|
|
9
|
+
return () => {
|
|
10
|
+
toasters--
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function hasToaster(): boolean {
|
|
15
|
+
return toasters > 0
|
|
16
|
+
}
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ActionRunner,
|
|
3
3
|
ComponentRegistry,
|
|
4
|
+
STARTER_KIT_TOASTER_ID,
|
|
4
5
|
Translator,
|
|
5
6
|
initials,
|
|
6
7
|
isAlfakitPage,
|
|
7
8
|
loadStarterKitPanel,
|
|
8
9
|
loadedStarterKitPanel,
|
|
9
10
|
openRequestedStarterKitSearch,
|
|
11
|
+
overlayPlacementDirection,
|
|
12
|
+
resolveToastPosition,
|
|
10
13
|
showsLanguageSwitcher,
|
|
11
14
|
starterKitCopilot,
|
|
12
15
|
starterKitPanelLocale,
|
|
@@ -17,12 +20,15 @@ import {
|
|
|
17
20
|
tenantNameText,
|
|
18
21
|
type PanelSchema,
|
|
19
22
|
type StarterKitWidgets,
|
|
23
|
+
type ToastPosition,
|
|
20
24
|
} from '@alfakit/core'
|
|
21
25
|
import { router, usePage } from '@inertiajs/react'
|
|
22
26
|
import { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from 'lucide-react'
|
|
23
27
|
import { Direction } from 'radix-ui'
|
|
24
28
|
import { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
29
|
+
import { createPortal } from 'react-dom'
|
|
25
30
|
import { AiCopilot } from '../components/ai/copilot'
|
|
31
|
+
import { setToasterId } from '../components/notifications/flash-toasts'
|
|
26
32
|
import { NotificationsMenu } from '../components/notifications/notifications-menu'
|
|
27
33
|
import { RealtimeNotifications } from '../components/notifications/realtime-notifications'
|
|
28
34
|
import { GlobalSearch } from '../components/panel/global-search'
|
|
@@ -38,16 +44,20 @@ import {
|
|
|
38
44
|
DropdownMenuSeparator,
|
|
39
45
|
DropdownMenuTrigger,
|
|
40
46
|
} from '../components/ui/dropdown-menu'
|
|
47
|
+
import { Toaster } from '../components/ui/sonner'
|
|
41
48
|
import { TooltipProvider } from '../components/ui/tooltip'
|
|
42
49
|
import {
|
|
43
50
|
AlfakitProvider,
|
|
44
51
|
StandaloneProvider,
|
|
45
52
|
useLocalization,
|
|
53
|
+
useRegistry,
|
|
46
54
|
useT,
|
|
47
55
|
type AlfakitContextValue,
|
|
48
56
|
type AnyComponent,
|
|
49
57
|
} from '../context'
|
|
50
58
|
import { Icon } from '../lib/icons'
|
|
59
|
+
import { ensureFlashToasts } from '../standalone/runtime'
|
|
60
|
+
import { claimToaster, hasToaster } from '../standalone/toaster-claims'
|
|
51
61
|
import {
|
|
52
62
|
CopilotTrigger,
|
|
53
63
|
LanguageSwitcherTrigger,
|
|
@@ -117,16 +127,89 @@ function useWidgetsPanel(source: StarterKitWidgets): PanelSchema | null {
|
|
|
117
127
|
)
|
|
118
128
|
}
|
|
119
129
|
|
|
130
|
+
/**
|
|
131
|
+
* AlfaKit's toaster on a host page (as `<AlfakitToaster>`, without its standalone context):
|
|
132
|
+
* Alfakit's toasts go to it, and so do flashed notifications.
|
|
133
|
+
*/
|
|
134
|
+
function HostToaster({ position, dir }: { position?: ToastPosition; dir: 'ltr' | 'rtl' }) {
|
|
135
|
+
const registry = useRegistry()
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
ensureFlashToasts(registry)
|
|
139
|
+
}, [registry])
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
setToasterId(STARTER_KIT_TOASTER_ID)
|
|
143
|
+
|
|
144
|
+
return () => setToasterId(undefined)
|
|
145
|
+
}, [])
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<Toaster
|
|
149
|
+
id={STARTER_KIT_TOASTER_ID}
|
|
150
|
+
position={resolveToastPosition(position, dir)}
|
|
151
|
+
dir={dir}
|
|
152
|
+
richColors
|
|
153
|
+
closeButton
|
|
154
|
+
/>
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* On the host's own pages no AlfaKit toaster is mounted, so the first loaded header
|
|
160
|
+
* component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).
|
|
161
|
+
* AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check
|
|
162
|
+
* runs after every component's effects, and again after each visit.
|
|
163
|
+
*/
|
|
164
|
+
function useOwnsHostToaster(): boolean {
|
|
165
|
+
const alfakitPage = isAlfakitPage(usePage().props)
|
|
166
|
+
const [owns, setOwns] = useState(false)
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (alfakitPage) return undefined
|
|
170
|
+
|
|
171
|
+
let release: (() => void) | null = null
|
|
172
|
+
let cancelled = false
|
|
173
|
+
|
|
174
|
+
queueMicrotask(() => {
|
|
175
|
+
if (cancelled || hasToaster()) return
|
|
176
|
+
|
|
177
|
+
release = claimToaster()
|
|
178
|
+
setOwns(true)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
return () => {
|
|
182
|
+
cancelled = true
|
|
183
|
+
release?.()
|
|
184
|
+
setOwns(false)
|
|
185
|
+
}
|
|
186
|
+
}, [alfakitPage])
|
|
187
|
+
|
|
188
|
+
return owns && !alfakitPage
|
|
189
|
+
}
|
|
190
|
+
|
|
120
191
|
function WidgetShell({ panel, children }: { panel: PanelSchema; children: ReactNode }) {
|
|
121
192
|
const context = useWidgetsContext(panel)
|
|
122
193
|
const page = useMemo(() => starterKitWidgetsPage(), [])
|
|
194
|
+
const ownsToaster = useOwnsHostToaster()
|
|
195
|
+
const direction = panel.localization?.direction ?? 'ltr'
|
|
123
196
|
|
|
124
197
|
return (
|
|
125
198
|
<AlfakitProvider value={context}>
|
|
126
199
|
<StandaloneProvider value={{ panel, page }}>
|
|
127
|
-
<Direction.Provider dir={
|
|
200
|
+
<Direction.Provider dir={direction}>
|
|
128
201
|
<TooltipProvider delayDuration={0}>{children}</TooltipProvider>
|
|
129
202
|
</Direction.Provider>
|
|
203
|
+
{/* In the body, not in the header (a header with a backdrop filter would hold its
|
|
204
|
+
fixed position), placed in the document's direction as on AlfaKit pages. */}
|
|
205
|
+
{ownsToaster &&
|
|
206
|
+
createPortal(
|
|
207
|
+
<HostToaster
|
|
208
|
+
position={panel.toastPosition}
|
|
209
|
+
dir={overlayPlacementDirection(direction, true)}
|
|
210
|
+
/>,
|
|
211
|
+
document.body,
|
|
212
|
+
)}
|
|
130
213
|
</StandaloneProvider>
|
|
131
214
|
</AlfakitProvider>
|
|
132
215
|
)
|
|
@@ -52,7 +52,10 @@ export function SearchTrigger({ source, onLoad }: WidgetProps & { onLoad?: () =>
|
|
|
52
52
|
<SearchIcon className="size-4" aria-hidden="true" />
|
|
53
53
|
<span className="sr-only sm:not-sr-only sm:flex-1 sm:text-start">{placeholder}</span>
|
|
54
54
|
{hydrated && binding && (
|
|
55
|
-
<kbd
|
|
55
|
+
<kbd
|
|
56
|
+
dir="ltr"
|
|
57
|
+
className="bg-muted text-foreground/70 hidden rounded px-1 font-mono text-[10px] sm:inline"
|
|
58
|
+
>
|
|
56
59
|
{formatKeyBinding(binding)}
|
|
57
60
|
</kbd>
|
|
58
61
|
)}
|