@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.
Files changed (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/cms/blocks.d.ts.map +1 -1
  3. package/dist/components/cms/blocks.js +2 -2
  4. package/dist/components/cms/blocks.js.map +1 -1
  5. package/dist/components/cms/head.d.ts +6 -4
  6. package/dist/components/cms/head.d.ts.map +1 -1
  7. package/dist/components/cms/head.js +10 -4
  8. package/dist/components/cms/head.js.map +1 -1
  9. package/dist/components/cms/page.d.ts.map +1 -1
  10. package/dist/components/cms/page.js +16 -3
  11. package/dist/components/cms/page.js.map +1 -1
  12. package/dist/components/fields/file-upload.js +4 -4
  13. package/dist/components/fields/file-upload.js.map +1 -1
  14. package/dist/components/fields/rich-editor.js +4 -2
  15. package/dist/components/fields/rich-editor.js.map +1 -1
  16. package/dist/components/fields/slider.js +1 -1
  17. package/dist/components/fields/slider.js.map +1 -1
  18. package/dist/components/helpdesk/chat-widget.js +1 -1
  19. package/dist/components/layout/section.js +1 -1
  20. package/dist/components/layout/tabs.js +1 -1
  21. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  22. package/dist/components/panel/app-sidebar.js +27 -5
  23. package/dist/components/panel/app-sidebar.js.map +1 -1
  24. package/dist/components/panel/global-search.js +2 -2
  25. package/dist/components/panel/global-search.js.map +1 -1
  26. package/dist/components/panel/shortcut-keys.js +1 -1
  27. package/dist/components/panel/shortcut-keys.js.map +1 -1
  28. package/dist/components/table/list-tabs.js +1 -1
  29. package/dist/components/table/list-tabs.js.map +1 -1
  30. package/dist/components/table/table-views.js +1 -1
  31. package/dist/components/table/table-views.js.map +1 -1
  32. package/dist/components/versions/record-versions.js +1 -1
  33. package/dist/context.d.ts +8 -0
  34. package/dist/context.d.ts.map +1 -1
  35. package/dist/context.js +27 -2
  36. package/dist/context.js.map +1 -1
  37. package/dist/create-alfakit.d.ts.map +1 -1
  38. package/dist/create-alfakit.js +6 -3
  39. package/dist/create-alfakit.js.map +1 -1
  40. package/dist/eject-manifest.json +2 -2
  41. package/dist/index.js +1 -1
  42. package/dist/layouts/app-layout.d.ts.map +1 -1
  43. package/dist/layouts/app-layout.js +13 -3
  44. package/dist/layouts/app-layout.js.map +1 -1
  45. package/dist/lib/discovery.d.ts +4 -0
  46. package/dist/lib/discovery.d.ts.map +1 -1
  47. package/dist/lib/discovery.js +17 -7
  48. package/dist/lib/discovery.js.map +1 -1
  49. package/dist/lib/icons.d.ts +2 -1
  50. package/dist/lib/icons.d.ts.map +1 -1
  51. package/dist/lib/icons.js +55 -19
  52. package/dist/lib/icons.js.map +1 -1
  53. package/dist/pages/page.d.ts +1 -1
  54. package/dist/pages/page.d.ts.map +1 -1
  55. package/dist/pages/page.js +47 -10
  56. package/dist/pages/page.js.map +1 -1
  57. package/dist/standalone/alfakit-component.js +2 -1
  58. package/dist/standalone/alfakit-component.js.map +1 -1
  59. package/dist/standalone/alfakit-toaster.js +2 -1
  60. package/dist/standalone/alfakit-toaster.js.map +1 -1
  61. package/dist/standalone/runtime.js +1 -11
  62. package/dist/standalone/runtime.js.map +1 -1
  63. package/dist/standalone/toaster-claims.js +16 -0
  64. package/dist/standalone/toaster-claims.js.map +1 -0
  65. package/dist/starter-kit/widgets-loaded.js +63 -6
  66. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  67. package/dist/starter-kit/widgets-triggers.js +1 -1
  68. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  69. package/dist/themes.css +92 -0
  70. package/package.json +2 -2
  71. package/src/components/cms/blocks.tsx +7 -1
  72. package/src/components/cms/head.tsx +19 -4
  73. package/src/components/cms/page.tsx +17 -2
  74. package/src/components/fields/file-upload.tsx +5 -4
  75. package/src/components/fields/rich-editor.tsx +7 -2
  76. package/src/components/fields/slider.tsx +3 -1
  77. package/src/components/panel/app-sidebar.tsx +28 -7
  78. package/src/components/panel/global-search.tsx +4 -1
  79. package/src/components/panel/shortcut-keys.tsx +1 -1
  80. package/src/components/table/list-tabs.tsx +3 -1
  81. package/src/components/table/table-views.tsx +3 -1
  82. package/src/context.tsx +46 -1
  83. package/src/create-alfakit.tsx +13 -2
  84. package/src/layouts/app-layout.tsx +16 -3
  85. package/src/lib/discovery.tsx +23 -8
  86. package/src/lib/icons.tsx +90 -24
  87. package/src/pages/page.tsx +85 -27
  88. package/src/standalone/runtime.ts +2 -14
  89. package/src/standalone/toaster-claims.ts +16 -0
  90. package/src/starter-kit/widgets-loaded.tsx +84 -1
  91. package/src/starter-kit/widgets-triggers.tsx +4 -1
@@ -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
- export function PageShell({ page, children }: { page: PageSchema; children: ReactNode }) {
159
- const { registry } = useAlfakit()
161
+ type LayoutKind = 'app' | 'auth' | 'blank' | 'fullscreen'
160
162
 
161
- useBodyAttributes(page.extraBodyAttributes)
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 (page.layout === 'auth') {
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 (page.layout === 'blank') {
173
- return createElement(layout(registry, 'layout:blank'), { page }, children)
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
- if (page.layout === 'fullscreen') {
177
- return createElement(layout(registry, 'layout:fullscreen'), { page }, children)
178
- }
194
+ return createElement(layout(registry, `layout:${kind}`), { page }, children)
195
+ }
179
196
 
180
- return createElement(
181
- layout(registry, 'layout:app'),
182
- { breadcrumbs: page.breadcrumbs, maxContentWidth: page.maxContentWidth, page },
183
- <>
184
- {/* `?` overlay, `/`, `g d` and panel command shortcuts (`keyboardShortcuts()`). */}
185
- <KeyboardShortcuts />
186
- <Slot name="page.start" />
187
- <PageHeader page={page} />
188
- {/* Edit pages: somebody else saved the record (`detectConflicts()`, `Panel::realtime()`). */}
189
- {page.conflicts &&
190
- createElement(registry.resolve('page:conflict-banner') ?? ConflictBanner, { page })}
191
- {page.subNavigation ? (
192
- <SubNavigationLayout navigation={page.subNavigation}>{children}</SubNavigationLayout>
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
- let toasters = 0
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={panel.localization?.direction ?? 'ltr'}>
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 dir="ltr" className="bg-muted hidden rounded px-1 font-mono text-[10px] sm:inline">
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
  )}