@alfakit/react 0.1.12 → 0.1.13

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @alfakit/react
2
2
 
3
+ ## 0.1.13
4
+
5
+ ### Patch Changes
6
+
7
+ - Starter kit header components bring a toaster to the app's own pages, so push and PWA messages show there; AlfaKit pages keep a single toaster.
8
+
3
9
  ## 0.1.12
4
10
 
5
11
  ### Patch Changes
@@ -1 +1 @@
1
- {"version":3,"file":"create-alfakit.d.ts","names":[],"sources":["../src/create-alfakit.tsx"],"mappings":";;;;;iBA8HiB,6BAA6B,eAAe;;EAE3D,WAAW,KAAK,cAAc;EAC9B;;;;;;;;;;;;;;wBAsLoB,cAAc,UAAS,uBAA4B"}
1
+ {"version":3,"file":"create-alfakit.d.ts","names":[],"sources":["../src/create-alfakit.tsx"],"mappings":";;;;;iBAoIiB,6BAA6B,eAAe;;EAE3D,WAAW,KAAK,cAAc;EAC9B;;;;;;;;;;;;;;wBAsLoB,cAAc,UAAS,uBAA4B"}
@@ -3,6 +3,7 @@ import { preloadIcons } from "./lib/icons.js";
3
3
  import { TooltipProvider } from "./components/ui/tooltip.js";
4
4
  import { discoverComponents } from "./lib/discovery.js";
5
5
  import { builtInComponents, createDefaultRegistry } from "./registry.js";
6
+ import { claimToaster } from "./standalone/toaster-claims.js";
6
7
  import { FlashToasts, showToast } from "./components/notifications/flash-toasts.js";
7
8
  import { AlfakitPage } from "./pages/page.js";
8
9
  import { ActionModals } from "./components/actions/action-modals.js";
@@ -15,7 +16,7 @@ import { Toaster } from "./components/ui/sonner.js";
15
16
  import { panelLayouts } from "./layouts/panel-layouts.js";
16
17
  import { ActionRunner, PANEL_TOAST_OFFSET, Translator, applyDocumentLocale, applyPanelTheme, configureCharts, configureRealtime, debugRecorder, documentNonce, effectiveErrorNotifications, initializeAppearance, installErrorNotifications, installPrefetchInvalidation, installSpaUrlExceptions, overlayPlacementDirection, registerRichEditorExtensions, reloadOnExpiredPage, resolveRequestErrorNotification, resolveToastPosition, setIconAliases, setRequestErrorReporter, whenIdle } from "@alfakit/core";
17
18
  import { createInertiaApp, router, usePage } from "@inertiajs/react";
18
- import { StrictMode, createElement, useLayoutEffect } from "react";
19
+ import { StrictMode, createElement, useEffect, useLayoutEffect } from "react";
19
20
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
20
21
  import { Direction } from "radix-ui";
21
22
  //#region src/create-alfakit.tsx
@@ -40,6 +41,7 @@ function AlfakitRoot({ children, toasterId }) {
40
41
  useLayoutEffect(() => {
41
42
  applyPanelTheme(panel);
42
43
  }, [panel]);
44
+ useEffect(() => claimToaster(), []);
43
45
  return /* @__PURE__ */ jsx(Direction.Provider, {
44
46
  dir: direction,
45
47
  children: /* @__PURE__ */ jsxs(TooltipProvider, {
@@ -1 +1 @@
1
- {"version":3,"file":"create-alfakit.js","names":[],"sources":["../src/create-alfakit.tsx"],"sourcesContent":["import {\n ActionRunner,\n PANEL_TOAST_OFFSET,\n Translator,\n applyDocumentLocale,\n applyPanelTheme,\n configureCharts,\n configureRealtime,\n debugRecorder,\n documentNonce,\n effectiveErrorNotifications,\n initializeAppearance,\n installErrorNotifications,\n installPrefetchInvalidation,\n installSpaUrlExceptions,\n overlayPlacementDirection,\n registerRichEditorExtensions,\n reloadOnExpiredPage,\n resolveRequestErrorNotification,\n resolveToastPosition,\n setIconAliases,\n setRequestErrorReporter,\n whenIdle,\n type AlfakitOptions,\n type AlfakitPageProps,\n type ErrorNotificationsSchema,\n type PanelSchema,\n} from '@alfakit/core'\nimport type { InertiaAppSSRResponse, Page } from '@inertiajs/core'\nimport { createInertiaApp, router, usePage } from '@inertiajs/react'\nimport { Direction } from 'radix-ui'\nimport {\n StrictMode,\n createElement,\n useLayoutEffect,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport { ActionModals } from './components/actions/action-modals'\nimport { DevToolsSlot } from './components/dev-tools/dev-tools-slot'\nimport { FlashToasts, showToast } from './components/notifications/flash-toasts'\nimport { RealtimeNotifications } from './components/notifications/realtime-notifications'\nimport { ImpersonationBanner } from './components/panel/impersonation-banner'\nimport { PwaManager } from './components/panel/pwa'\nimport { SchemaContractGuard } from './components/panel/schema-contract-guard'\nimport { Toaster } from './components/ui/sonner'\nimport { TooltipProvider } from './components/ui/tooltip'\nimport { AlfakitProvider, useAlfakit, type AlfakitContextValue, type AnyComponent } from './context'\nimport { panelLayouts } from './layouts/panel-layouts'\nimport { discoverComponents } from './lib/discovery'\nimport { preloadIcons } from './lib/icons'\nimport { AlfakitPage } from './pages/page'\nimport { builtInComponents, createDefaultRegistry } from './registry'\n\n/**\n * Rendered inside Inertia's page tree, so it follows the current page: the reading\n * direction for Radix primitives, modals and toasts.\n */\nexport function AlfakitRoot({\n children,\n toasterId,\n}: {\n children: ReactNode\n /** Starter kit mode: Alfakit's toaster gets an id so the host's id-less one never doubles its toasts. */\n toasterId?: string\n}) {\n const panel = usePage<AlfakitPageProps>().props.alfakit\n const { translator, registry } = useAlfakit()\n const localization = panel?.localization\n const direction = localization?.direction ?? 'ltr'\n\n // Every visit (including the redirect after a language switch) may carry new strings,\n // so the translator is updated before children render.\n if (panel) {\n translator.setTranslations(panel.translations ?? {})\n translator.setLocale(localization?.tag ?? 'en')\n // Chrome icons replaced with `Icons::register()`, also in a server render, so the server\n // and the browser draw the same glyph (W-V3-28).\n setIconAliases(panel.theme?.icons)\n }\n\n // Starter kit mode: the host app owns `<html lang dir>` (its own pages and layout are not\n // mirrored); `AlfakitStarterKitRoot` puts the direction on Alfakit's own root element.\n const ownsDocument = panel?.starterKit !== true\n\n useLayoutEffect(() => {\n if (ownsDocument) applyDocumentLocale(localization)\n }, [localization, ownsDocument])\n\n useLayoutEffect(() => {\n applyPanelTheme(panel)\n }, [panel])\n\n return (\n <Direction.Provider dir={direction}>\n <TooltipProvider delayDuration={0}>\n <SchemaContractGuard panel={panel}>{children}</SchemaContractGuard>\n <ActionModals />\n <FlashToasts />\n <RealtimeNotifications />\n {/* `Panel::pwa()`: service worker and the \"new version\" toast. */}\n <PwaManager />\n {/* Schema inspector: renders (and loads its code) only with `alfakitDevTools`. */}\n <DevToolsSlot />\n {/* Replace with createAlfakit({ components: { 'panel:impersonation-banner': … } }). */}\n {panel?.impersonation &&\n createElement(registry.resolve('panel:impersonation-banner') ?? ImpersonationBanner, {\n impersonation: panel.impersonation,\n })}\n <Toaster\n id={toasterId}\n position={resolveToastPosition(\n panel?.toastPosition,\n overlayPlacementDirection(direction, panel?.starterKit),\n )}\n offset={PANEL_TOAST_OFFSET}\n mobileOffset={PANEL_TOAST_OFFSET}\n dir={direction}\n richColors\n closeButton\n />\n </TooltipProvider>\n </Direction.Provider>\n )\n}\n\nexport interface CreateAlfakitOptions extends AlfakitOptions<AnyComponent> {\n /** Wrap the whole app (e.g. with your own providers). */\n withApp?: (app: ReactNode) => ReactNode\n strictMode?: boolean\n}\n\n/** Renders one Inertia page to HTML: what `createInertiaApp` returns on the server. */\nexport type AlfakitRenderFunction = (\n page: Page,\n renderToString: (element: ReactElement) => string,\n) => Promise<InertiaAppSSRResponse>\n\n/**\n * The Inertia app shared by `createAlfakit()` (browser) and `createAlfakitServer()` (Node):\n * the same registry, pages, layout and providers. In the browser it mounts, or hydrates\n * server-rendered markup; on the server (no `window`) Inertia returns a render function.\n */\nexport function bootAlfakit(\n options: CreateAlfakitOptions = {},\n): Promise<void | AlfakitRenderFunction> {\n // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.\n const discovered = discoverComponents(options.discover)\n const registry = createDefaultRegistry()\n .registerMany(panelLayouts)\n .registerMany(discovered.components)\n .registerMany(options.components ?? {})\n const pages: Record<string, AnyComponent> = {\n 'Alfakit/Page': AlfakitPage,\n ...discovered.pages,\n ...options.pages,\n }\n const isServer = typeof window === 'undefined'\n\n // Every panel page draws named icons: fetch `lucide-react/dynamic` while booting.\n preloadIcons()\n\n // `'js:name'` chart option callbacks and Chart.js plugins for every chart widget.\n configureCharts(options)\n // `createAlfakit({ echo })`: Laravel Echo for `Panel::realtime()` without `window.Echo`.\n if (options.echo !== undefined && !isServer) configureRealtime(null, { echo: options.echo })\n // Rich content plugin extensions, loaded when an editor needs them.\n registerRichEditorExtensions(options.richEditorExtensions ?? {})\n\n // The browser's current panel, read by the SPA-exception and error-notification listeners.\n let currentPanel: PanelSchema | null = null\n // The open page's error notifications (its override, else the panel's).\n let currentErrorNotifications: ErrorNotificationsSchema | false | undefined\n\n // One translator and action runner for the browser session; a server render gets fresh\n // ones per request so concurrent renders never share a locale or endpoints.\n const createContext = (): AlfakitContextValue => {\n const translator = new Translator()\n\n return {\n registry,\n pages,\n translator,\n // The runner's endpoints are refreshed on every navigation from the shared props.\n actions: new ActionRunner({\n endpoints: { action: '', modal: '' },\n // A failed action request is a toast, never a silent no-op.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n currentErrorNotifications,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n }\n }\n\n const browserContext = isServer ? null : createContext()\n\n const sync = (context: AlfakitContextValue, page: Page, applyToDocument: boolean) => {\n const props = page.props as unknown as AlfakitPageProps\n const panel = props.alfakit\n\n if (!panel) return\n\n if (context === browserContext) {\n currentPanel = panel\n currentErrorNotifications = effectiveErrorNotifications(panel, props.page)\n }\n\n context.translator.setTranslations(panel.translations ?? {})\n context.translator.setLocale(panel.localization?.tag ?? 'en')\n context.actions.setEndpoints({\n action: panel.endpoints.action ?? '',\n modal: panel.endpoints.modal ?? '',\n })\n\n if (applyToDocument) {\n applyDocumentLocale(panel.localization)\n applyPanelTheme(panel)\n // Internal requests and form stores are only recorded while the page carries the debug prop.\n debugRecorder.setEnabled(props.alfakitDebug !== undefined)\n }\n }\n\n if (browserContext) {\n router.on('navigate', (event) => sync(browserContext, event.detail.page, true))\n reloadOnExpiredPage()\n // Prefetched pages and deferred table totals are dropped after every mutation.\n installPrefetchInvalidation()\n installSpaUrlExceptions(() => currentPanel)\n installErrorNotifications(\n () => ({ errorNotifications: currentErrorNotifications }),\n (notification) => showToast(notification),\n )\n // Failed internal requests without their own handler (form live refreshes, step\n // validation) show the configured error notification too.\n setRequestErrorReporter((error) => {\n const notification = resolveRequestErrorNotification(\n error,\n currentErrorNotifications,\n (key, replacements, fallback) => browserContext.translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n })\n }\n\n return createInertiaApp<AlfakitPageProps>({\n title: (title) => title,\n progress: options.progress === undefined ? { color: 'var(--primary)' } : options.progress,\n // The page's CSP nonce, so the progress bar's injected <style> passes a nonce-based policy.\n nonce: documentNonce(),\n resolve: (name, page) => {\n const component = pages[name]\n\n if (!component) {\n throw new Error(\n `Alfakit: page component \"${name}\" is not registered. Pass it to createAlfakit({ pages }).`,\n )\n }\n\n // The page's auto-registered components load first, so the server render and the\n // hydration both show them (no flash, no mismatch).\n // Built-ins split per type (`builtInComponents`) load the same way.\n const pending = [\n builtInComponents.preloadFor(page),\n discovered.discovery.preloadFor(page),\n ].filter((promise) => promise !== undefined)\n\n return pending.length > 0 ? Promise.all(pending).then(() => component) : component\n },\n layout: () => AlfakitRoot,\n strictMode: options.strictMode ?? true,\n withApp: (app, { page, ssr }) => {\n const context = browserContext ?? createContext()\n\n sync(context, page as Page, !ssr)\n\n // Panel pages only: other pages (`pages` option) follow the application's own dark\n // mode, like standalone components do.\n const panel = (page.props as unknown as Partial<AlfakitPageProps>).alfakit\n\n if (!ssr && panel) {\n initializeAppearance(panel)\n }\n\n const tree = <AlfakitProvider value={context}>{app}</AlfakitProvider>\n\n const wrapped = options.withApp ? options.withApp(tree) : tree\n\n return options.strictMode === false ? <>{wrapped}</> : <StrictMode>{wrapped}</StrictMode>\n },\n }) as Promise<void | AlfakitRenderFunction>\n}\n\n/**\n * Boot the Alfakit panel frontend. Call once from your panel entry file. Markup rendered by\n * `createAlfakitServer()` (`@alfakit/react/server`) is hydrated instead of re-rendered.\n *\n * ```tsx\n * createAlfakit({\n * // Files in resources/js/alfakit/fields, columns, … register themselves.\n * discover: import.meta.glob('./{fields,columns,entries,widgets,pages,components,blocks}/**\\/*.tsx'),\n * components: { 'map-picker': MapPicker },\n * })\n * ```\n */\nexport async function createAlfakit(options: CreateAlfakitOptions = {}): Promise<void> {\n await bootAlfakit(options)\n\n // After the first render: the remaining split built-ins (action modals' fields).\n whenIdle(() => void builtInComponents.preloadAll())\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"create-alfakit.js","names":[],"sources":["../src/create-alfakit.tsx"],"sourcesContent":["import {\n ActionRunner,\n PANEL_TOAST_OFFSET,\n Translator,\n applyDocumentLocale,\n applyPanelTheme,\n configureCharts,\n configureRealtime,\n debugRecorder,\n documentNonce,\n effectiveErrorNotifications,\n initializeAppearance,\n installErrorNotifications,\n installPrefetchInvalidation,\n installSpaUrlExceptions,\n overlayPlacementDirection,\n registerRichEditorExtensions,\n reloadOnExpiredPage,\n resolveRequestErrorNotification,\n resolveToastPosition,\n setIconAliases,\n setRequestErrorReporter,\n whenIdle,\n type AlfakitOptions,\n type AlfakitPageProps,\n type ErrorNotificationsSchema,\n type PanelSchema,\n} from '@alfakit/core'\nimport type { InertiaAppSSRResponse, Page } from '@inertiajs/core'\nimport { createInertiaApp, router, usePage } from '@inertiajs/react'\nimport { Direction } from 'radix-ui'\nimport {\n StrictMode,\n createElement,\n useEffect,\n useLayoutEffect,\n type ReactElement,\n type ReactNode,\n} from 'react'\nimport { ActionModals } from './components/actions/action-modals'\nimport { DevToolsSlot } from './components/dev-tools/dev-tools-slot'\nimport { FlashToasts, showToast } from './components/notifications/flash-toasts'\nimport { RealtimeNotifications } from './components/notifications/realtime-notifications'\nimport { ImpersonationBanner } from './components/panel/impersonation-banner'\nimport { PwaManager } from './components/panel/pwa'\nimport { SchemaContractGuard } from './components/panel/schema-contract-guard'\nimport { Toaster } from './components/ui/sonner'\nimport { TooltipProvider } from './components/ui/tooltip'\nimport { AlfakitProvider, useAlfakit, type AlfakitContextValue, type AnyComponent } from './context'\nimport { panelLayouts } from './layouts/panel-layouts'\nimport { discoverComponents } from './lib/discovery'\nimport { preloadIcons } from './lib/icons'\nimport { AlfakitPage } from './pages/page'\nimport { builtInComponents, createDefaultRegistry } from './registry'\nimport { claimToaster } from './standalone/toaster-claims'\n\n/**\n * Rendered inside Inertia's page tree, so it follows the current page: the reading\n * direction for Radix primitives, modals and toasts.\n */\nexport function AlfakitRoot({\n children,\n toasterId,\n}: {\n children: ReactNode\n /** Starter kit mode: Alfakit's toaster gets an id so the host's id-less one never doubles its toasts. */\n toasterId?: string\n}) {\n const panel = usePage<AlfakitPageProps>().props.alfakit\n const { translator, registry } = useAlfakit()\n const localization = panel?.localization\n const direction = localization?.direction ?? 'ltr'\n\n // Every visit (including the redirect after a language switch) may carry new strings,\n // so the translator is updated before children render.\n if (panel) {\n translator.setTranslations(panel.translations ?? {})\n translator.setLocale(localization?.tag ?? 'en')\n // Chrome icons replaced with `Icons::register()`, also in a server render, so the server\n // and the browser draw the same glyph (W-V3-28).\n setIconAliases(panel.theme?.icons)\n }\n\n // Starter kit mode: the host app owns `<html lang dir>` (its own pages and layout are not\n // mirrored); `AlfakitStarterKitRoot` puts the direction on Alfakit's own root element.\n const ownsDocument = panel?.starterKit !== true\n\n useLayoutEffect(() => {\n if (ownsDocument) applyDocumentLocale(localization)\n }, [localization, ownsDocument])\n\n useLayoutEffect(() => {\n applyPanelTheme(panel)\n }, [panel])\n\n // This root renders the toaster: starter kit header components and standalone components\n // on the same page don't mount a second one.\n useEffect(() => claimToaster(), [])\n\n return (\n <Direction.Provider dir={direction}>\n <TooltipProvider delayDuration={0}>\n <SchemaContractGuard panel={panel}>{children}</SchemaContractGuard>\n <ActionModals />\n <FlashToasts />\n <RealtimeNotifications />\n {/* `Panel::pwa()`: service worker and the \"new version\" toast. */}\n <PwaManager />\n {/* Schema inspector: renders (and loads its code) only with `alfakitDevTools`. */}\n <DevToolsSlot />\n {/* Replace with createAlfakit({ components: { 'panel:impersonation-banner': … } }). */}\n {panel?.impersonation &&\n createElement(registry.resolve('panel:impersonation-banner') ?? ImpersonationBanner, {\n impersonation: panel.impersonation,\n })}\n <Toaster\n id={toasterId}\n position={resolveToastPosition(\n panel?.toastPosition,\n overlayPlacementDirection(direction, panel?.starterKit),\n )}\n offset={PANEL_TOAST_OFFSET}\n mobileOffset={PANEL_TOAST_OFFSET}\n dir={direction}\n richColors\n closeButton\n />\n </TooltipProvider>\n </Direction.Provider>\n )\n}\n\nexport interface CreateAlfakitOptions extends AlfakitOptions<AnyComponent> {\n /** Wrap the whole app (e.g. with your own providers). */\n withApp?: (app: ReactNode) => ReactNode\n strictMode?: boolean\n}\n\n/** Renders one Inertia page to HTML: what `createInertiaApp` returns on the server. */\nexport type AlfakitRenderFunction = (\n page: Page,\n renderToString: (element: ReactElement) => string,\n) => Promise<InertiaAppSSRResponse>\n\n/**\n * The Inertia app shared by `createAlfakit()` (browser) and `createAlfakitServer()` (Node):\n * the same registry, pages, layout and providers. In the browser it mounts, or hydrates\n * server-rendered markup; on the server (no `window`) Inertia returns a render function.\n */\nexport function bootAlfakit(\n options: CreateAlfakitOptions = {},\n): Promise<void | AlfakitRenderFunction> {\n // Auto-registered files sit between the built-ins and the explicit `components` / `pages`.\n const discovered = discoverComponents(options.discover)\n const registry = createDefaultRegistry()\n .registerMany(panelLayouts)\n .registerMany(discovered.components)\n .registerMany(options.components ?? {})\n const pages: Record<string, AnyComponent> = {\n 'Alfakit/Page': AlfakitPage,\n ...discovered.pages,\n ...options.pages,\n }\n const isServer = typeof window === 'undefined'\n\n // Every panel page draws named icons: fetch `lucide-react/dynamic` while booting.\n preloadIcons()\n\n // `'js:name'` chart option callbacks and Chart.js plugins for every chart widget.\n configureCharts(options)\n // `createAlfakit({ echo })`: Laravel Echo for `Panel::realtime()` without `window.Echo`.\n if (options.echo !== undefined && !isServer) configureRealtime(null, { echo: options.echo })\n // Rich content plugin extensions, loaded when an editor needs them.\n registerRichEditorExtensions(options.richEditorExtensions ?? {})\n\n // The browser's current panel, read by the SPA-exception and error-notification listeners.\n let currentPanel: PanelSchema | null = null\n // The open page's error notifications (its override, else the panel's).\n let currentErrorNotifications: ErrorNotificationsSchema | false | undefined\n\n // One translator and action runner for the browser session; a server render gets fresh\n // ones per request so concurrent renders never share a locale or endpoints.\n const createContext = (): AlfakitContextValue => {\n const translator = new Translator()\n\n return {\n registry,\n pages,\n translator,\n // The runner's endpoints are refreshed on every navigation from the shared props.\n actions: new ActionRunner({\n endpoints: { action: '', modal: '' },\n // A failed action request is a toast, never a silent no-op.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n currentErrorNotifications,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n }\n }\n\n const browserContext = isServer ? null : createContext()\n\n const sync = (context: AlfakitContextValue, page: Page, applyToDocument: boolean) => {\n const props = page.props as unknown as AlfakitPageProps\n const panel = props.alfakit\n\n if (!panel) return\n\n if (context === browserContext) {\n currentPanel = panel\n currentErrorNotifications = effectiveErrorNotifications(panel, props.page)\n }\n\n context.translator.setTranslations(panel.translations ?? {})\n context.translator.setLocale(panel.localization?.tag ?? 'en')\n context.actions.setEndpoints({\n action: panel.endpoints.action ?? '',\n modal: panel.endpoints.modal ?? '',\n })\n\n if (applyToDocument) {\n applyDocumentLocale(panel.localization)\n applyPanelTheme(panel)\n // Internal requests and form stores are only recorded while the page carries the debug prop.\n debugRecorder.setEnabled(props.alfakitDebug !== undefined)\n }\n }\n\n if (browserContext) {\n router.on('navigate', (event) => sync(browserContext, event.detail.page, true))\n reloadOnExpiredPage()\n // Prefetched pages and deferred table totals are dropped after every mutation.\n installPrefetchInvalidation()\n installSpaUrlExceptions(() => currentPanel)\n installErrorNotifications(\n () => ({ errorNotifications: currentErrorNotifications }),\n (notification) => showToast(notification),\n )\n // Failed internal requests without their own handler (form live refreshes, step\n // validation) show the configured error notification too.\n setRequestErrorReporter((error) => {\n const notification = resolveRequestErrorNotification(\n error,\n currentErrorNotifications,\n (key, replacements, fallback) => browserContext.translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n })\n }\n\n return createInertiaApp<AlfakitPageProps>({\n title: (title) => title,\n progress: options.progress === undefined ? { color: 'var(--primary)' } : options.progress,\n // The page's CSP nonce, so the progress bar's injected <style> passes a nonce-based policy.\n nonce: documentNonce(),\n resolve: (name, page) => {\n const component = pages[name]\n\n if (!component) {\n throw new Error(\n `Alfakit: page component \"${name}\" is not registered. Pass it to createAlfakit({ pages }).`,\n )\n }\n\n // The page's auto-registered components load first, so the server render and the\n // hydration both show them (no flash, no mismatch).\n // Built-ins split per type (`builtInComponents`) load the same way.\n const pending = [\n builtInComponents.preloadFor(page),\n discovered.discovery.preloadFor(page),\n ].filter((promise) => promise !== undefined)\n\n return pending.length > 0 ? Promise.all(pending).then(() => component) : component\n },\n layout: () => AlfakitRoot,\n strictMode: options.strictMode ?? true,\n withApp: (app, { page, ssr }) => {\n const context = browserContext ?? createContext()\n\n sync(context, page as Page, !ssr)\n\n // Panel pages only: other pages (`pages` option) follow the application's own dark\n // mode, like standalone components do.\n const panel = (page.props as unknown as Partial<AlfakitPageProps>).alfakit\n\n if (!ssr && panel) {\n initializeAppearance(panel)\n }\n\n const tree = <AlfakitProvider value={context}>{app}</AlfakitProvider>\n\n const wrapped = options.withApp ? options.withApp(tree) : tree\n\n return options.strictMode === false ? <>{wrapped}</> : <StrictMode>{wrapped}</StrictMode>\n },\n }) as Promise<void | AlfakitRenderFunction>\n}\n\n/**\n * Boot the Alfakit panel frontend. Call once from your panel entry file. Markup rendered by\n * `createAlfakitServer()` (`@alfakit/react/server`) is hydrated instead of re-rendered.\n *\n * ```tsx\n * createAlfakit({\n * // Files in resources/js/alfakit/fields, columns, … register themselves.\n * discover: import.meta.glob('./{fields,columns,entries,widgets,pages,components,blocks}/**\\/*.tsx'),\n * components: { 'map-picker': MapPicker },\n * })\n * ```\n */\nexport async function createAlfakit(options: CreateAlfakitOptions = {}): Promise<void> {\n await bootAlfakit(options)\n\n // After the first render: the remaining split built-ins (action modals' fields).\n whenIdle(() => void builtInComponents.preloadAll())\n}\n"],"mappings":"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"}
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "package": "@alfakit/react",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "adapter": "react",
5
- "generatedAt": "2026-10-26T09:54:26.975Z",
5
+ "generatedAt": "2026-10-26T13:11:31.580Z",
6
6
  "components": {
7
7
  "TextInput": {
8
8
  "file": "src/components/fields/text-input.tsx",
@@ -3,7 +3,8 @@ import { PageActionProvider } from "../components/actions/action-context.js";
3
3
  import { cn } from "../lib/utils.js";
4
4
  import { TooltipProvider } from "../components/ui/tooltip.js";
5
5
  import { ActionList } from "../components/actions/action-list.js";
6
- import { claimToaster, createStandaloneContext, createStandaloneRegistry, ensureStandaloneRuntime, hasToaster } from "./runtime.js";
6
+ import { claimToaster, hasToaster } from "./toaster-claims.js";
7
+ import { createStandaloneContext, createStandaloneRegistry, ensureStandaloneRuntime } from "./runtime.js";
7
8
  import { ContentBlockRenderer } from "../pages/page.js";
8
9
  import { ActionModals } from "../components/actions/action-modals.js";
9
10
  import { AlfakitToaster } from "./alfakit-toaster.js";
@@ -1 +1 @@
1
- {"version":3,"file":"alfakit-component.js","names":[],"sources":["../../src/standalone/alfakit-component.tsx"],"sourcesContent":["import {\n StandaloneComponentState,\n findStandalonePropName,\n registerStandaloneProp,\n standalonePanelSchema,\n type ContentBlock,\n type StandaloneComponentSchema,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { Direction } from 'radix-ui'\nimport { useEffect, useLayoutEffect, useMemo, useState, type ReactNode } from 'react'\nimport { PageActionProvider } from '../components/actions/action-context'\nimport { ActionList } from '../components/actions/action-list'\nimport { ActionModals } from '../components/actions/action-modals'\nimport { TooltipProvider } from '../components/ui/tooltip'\nimport {\n AlfakitProvider,\n StandaloneProvider,\n useOptionalAlfakit,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { cn } from '../lib/utils'\nimport { ContentBlockRenderer } from '../pages/page'\nimport { AlfakitToaster } from './alfakit-toaster'\nimport {\n claimToaster,\n createStandaloneContext,\n createStandaloneRegistry,\n ensureStandaloneRuntime,\n hasToaster,\n} from './runtime'\n\nexport interface AlfakitComponentProps {\n /** The payload: `ContactForm::make()` passed as an Inertia prop. */\n component: StandaloneComponentSchema\n /** Renderer overrides for this instance (`{ TextInput: MyInput }`), on top of `configureAlfakit()`. */\n components?: Record<string, AnyComponent>\n className?: string\n /** Rendered above the content blocks (after the heading and actions). */\n header?: ReactNode\n /** Rendered below the content blocks. */\n footer?: ReactNode\n /** Render each content block yourself (e.g. to lay a form and a table side by side). */\n children?: (blocks: ContentBlock[], render: (block: ContentBlock) => ReactNode) => ReactNode\n}\n\n/**\n * Renders an Alfakit component outside a panel: a form, table, infolist, widgets or any mix\n * (`AlfaSix\\Alfakit\\Standalone\\*`) in any Inertia page of the application. Needs no\n * `createAlfakit()` and no panel layout: it provides its own translator, registry, action\n * runner and modals, and inherits the application's shadcn tokens.\n *\n * ```tsx\n * <AlfakitComponent component={orders} className=\"max-w-4xl\" />\n * ```\n */\n// `useLayoutEffect` warns during a server render, where nothing registers anyway.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect\n\nexport function AlfakitComponent({\n component,\n components,\n className,\n header,\n footer,\n children,\n}: AlfakitComponentProps) {\n const parent = useOptionalAlfakit()\n const props = useOptionalPageProps()\n const [, setVersion] = useState(0)\n // The registry overrides are an object literal in JSX: compare them by their keys.\n const overrideKeys = Object.keys(components ?? {}).join(',')\n\n // Lazy and polled schemas of this component, merged into the page it renders.\n const state = useMemo(\n () => new StandaloneComponentState(component, () => setVersion((value) => value + 1)),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n )\n state.setComponent(component)\n\n const context = useMemo<AlfakitContextValue>(() => {\n const poll = (target: string, interval: string | number | null | undefined) =>\n state.poll(target, interval)\n const refresh = (targets: string[]) => state.refresh(targets)\n\n if (parent) {\n // Inside a panel page: the panel's registry and translator, but this component's own\n // action runner, so its actions post to its own endpoints with its own token (sharing\n // the panel's runner sent them to the panel's endpoint: 403).\n const own = createStandaloneContext(\n { action: component.endpoints.action, modal: component.endpoints.modal },\n components,\n poll,\n )\n\n return {\n ...parent,\n actions: own.actions,\n registry: createStandaloneRegistry(components, parent.registry),\n poll,\n refresh,\n }\n }\n\n return {\n ...createStandaloneContext(\n { action: component.endpoints.action, modal: component.endpoints.modal },\n components,\n poll,\n ),\n refresh,\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parent, state, overrideKeys])\n\n // Endpoints follow the latest payload (they change per locale or path).\n context.actions.setEndpoints({\n action: component.endpoints.action,\n modal: component.endpoints.modal,\n })\n\n // Inside `createAlfakit()` the panel feeds the shared translator; on a page without the\n // `alfakit` prop (the rest of the app) the component's own translations apply.\n const ownsTranslator = !parent || !props?.alfakit\n\n if (ownsTranslator) {\n context.translator.setTranslations(component.translations ?? {})\n context.translator.setLocale(component.localization?.tag ?? 'en')\n }\n\n const panel = useMemo(() => standalonePanelSchema(component), [component])\n const page = state.page\n const direction = component.localization?.direction ?? 'ltr'\n\n useEffect(() => {\n ensureStandaloneRuntime()\n }, [])\n\n // Partial reloads (sorting, paging, inline edits) request this component's own prop.\n const propName = findStandalonePropName(props, component)\n\n // A layout effect runs before the children's effects: a `deferLoading()` table's first\n // reload must already ask for this component's prop, not the panel `page` prop.\n useIsomorphicLayoutEffect(() => registerStandaloneProp(propName ?? null), [propName])\n\n useEffect(() => {\n void state.loadLazy(component)\n // The token identifies a new payload.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state, component.token])\n\n // Toasts: use the layout's <AlfakitToaster /> when there is one, else the first component\n // on the page renders one (checked after every component's effects ran).\n const [ownsToaster, setOwnsToaster] = useState(false)\n\n useEffect(() => {\n if (parent) return\n\n let release: (() => void) | null = null\n let cancelled = false\n\n queueMicrotask(() => {\n if (cancelled || hasToaster()) return\n\n release = claimToaster()\n setOwnsToaster(true)\n })\n\n return () => {\n cancelled = true\n release?.()\n }\n }, [parent])\n\n const render = (block: ContentBlock) => <ContentBlockRenderer key={block.key} block={block} />\n const actionContext = useMemo(\n () => ({ token: component.token, component: 'page' }),\n [component.token],\n )\n\n const body = (\n <PageActionProvider value={actionContext}>\n <div\n data-slot=\"standalone\"\n data-alfakit-component={component.class}\n dir={direction}\n className={cn('flex flex-col gap-6', className)}\n >\n {(page.heading || page.headerActions?.length) && (\n <header\n data-slot=\"page-header\"\n className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\"\n >\n <div className=\"min-w-0 sm:max-w-[50%] sm:shrink-0\">\n {page.heading && (\n <h2 className=\"text-xl font-semibold tracking-tight\">{page.heading}</h2>\n )}\n {page.subheading && (\n <p className=\"text-muted-foreground mt-1 text-sm\">{page.subheading}</p>\n )}\n </div>\n {page.headerActions && page.headerActions.length > 0 && (\n <ActionList\n actions={page.headerActions}\n context={actionContext}\n align=\"end\"\n className=\"max-w-full min-w-0\"\n />\n )}\n </header>\n )}\n {header}\n {children ? children(page.content, render) : page.content.map(render)}\n {footer}\n </div>\n <ActionModals />\n {ownsToaster && <AlfakitToaster claimed dir={direction} />}\n </PageActionProvider>\n )\n\n return (\n <AlfakitProvider value={context}>\n <StandaloneProvider value={{ panel, page }}>\n {parent ? (\n body\n ) : (\n <Direction.Provider dir={direction}>\n <TooltipProvider delayDuration={0}>{body}</TooltipProvider>\n </Direction.Provider>\n )}\n </StandaloneProvider>\n </AlfakitProvider>\n )\n}\n\n/**\n * The host page's Inertia props, or undefined when rendered outside an Inertia app (a\n * component test, Storybook, a plain server render): partial reloads then reload every prop.\n */\nfunction useOptionalPageProps(): Record<string, unknown> | undefined {\n try {\n return usePage().props as Record<string, unknown>\n } catch {\n return undefined\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"alfakit-component.js","names":[],"sources":["../../src/standalone/alfakit-component.tsx"],"sourcesContent":["import {\n StandaloneComponentState,\n findStandalonePropName,\n registerStandaloneProp,\n standalonePanelSchema,\n type ContentBlock,\n type StandaloneComponentSchema,\n} from '@alfakit/core'\nimport { usePage } from '@inertiajs/react'\nimport { Direction } from 'radix-ui'\nimport { useEffect, useLayoutEffect, useMemo, useState, type ReactNode } from 'react'\nimport { PageActionProvider } from '../components/actions/action-context'\nimport { ActionList } from '../components/actions/action-list'\nimport { ActionModals } from '../components/actions/action-modals'\nimport { TooltipProvider } from '../components/ui/tooltip'\nimport {\n AlfakitProvider,\n StandaloneProvider,\n useOptionalAlfakit,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { cn } from '../lib/utils'\nimport { ContentBlockRenderer } from '../pages/page'\nimport { AlfakitToaster } from './alfakit-toaster'\nimport {\n claimToaster,\n createStandaloneContext,\n createStandaloneRegistry,\n ensureStandaloneRuntime,\n hasToaster,\n} from './runtime'\n\nexport interface AlfakitComponentProps {\n /** The payload: `ContactForm::make()` passed as an Inertia prop. */\n component: StandaloneComponentSchema\n /** Renderer overrides for this instance (`{ TextInput: MyInput }`), on top of `configureAlfakit()`. */\n components?: Record<string, AnyComponent>\n className?: string\n /** Rendered above the content blocks (after the heading and actions). */\n header?: ReactNode\n /** Rendered below the content blocks. */\n footer?: ReactNode\n /** Render each content block yourself (e.g. to lay a form and a table side by side). */\n children?: (blocks: ContentBlock[], render: (block: ContentBlock) => ReactNode) => ReactNode\n}\n\n/**\n * Renders an Alfakit component outside a panel: a form, table, infolist, widgets or any mix\n * (`AlfaSix\\Alfakit\\Standalone\\*`) in any Inertia page of the application. Needs no\n * `createAlfakit()` and no panel layout: it provides its own translator, registry, action\n * runner and modals, and inherits the application's shadcn tokens.\n *\n * ```tsx\n * <AlfakitComponent component={orders} className=\"max-w-4xl\" />\n * ```\n */\n// `useLayoutEffect` warns during a server render, where nothing registers anyway.\nconst useIsomorphicLayoutEffect = typeof window === 'undefined' ? useEffect : useLayoutEffect\n\nexport function AlfakitComponent({\n component,\n components,\n className,\n header,\n footer,\n children,\n}: AlfakitComponentProps) {\n const parent = useOptionalAlfakit()\n const props = useOptionalPageProps()\n const [, setVersion] = useState(0)\n // The registry overrides are an object literal in JSX: compare them by their keys.\n const overrideKeys = Object.keys(components ?? {}).join(',')\n\n // Lazy and polled schemas of this component, merged into the page it renders.\n const state = useMemo(\n () => new StandaloneComponentState(component, () => setVersion((value) => value + 1)),\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [],\n )\n state.setComponent(component)\n\n const context = useMemo<AlfakitContextValue>(() => {\n const poll = (target: string, interval: string | number | null | undefined) =>\n state.poll(target, interval)\n const refresh = (targets: string[]) => state.refresh(targets)\n\n if (parent) {\n // Inside a panel page: the panel's registry and translator, but this component's own\n // action runner, so its actions post to its own endpoints with its own token (sharing\n // the panel's runner sent them to the panel's endpoint: 403).\n const own = createStandaloneContext(\n { action: component.endpoints.action, modal: component.endpoints.modal },\n components,\n poll,\n )\n\n return {\n ...parent,\n actions: own.actions,\n registry: createStandaloneRegistry(components, parent.registry),\n poll,\n refresh,\n }\n }\n\n return {\n ...createStandaloneContext(\n { action: component.endpoints.action, modal: component.endpoints.modal },\n components,\n poll,\n ),\n refresh,\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [parent, state, overrideKeys])\n\n // Endpoints follow the latest payload (they change per locale or path).\n context.actions.setEndpoints({\n action: component.endpoints.action,\n modal: component.endpoints.modal,\n })\n\n // Inside `createAlfakit()` the panel feeds the shared translator; on a page without the\n // `alfakit` prop (the rest of the app) the component's own translations apply.\n const ownsTranslator = !parent || !props?.alfakit\n\n if (ownsTranslator) {\n context.translator.setTranslations(component.translations ?? {})\n context.translator.setLocale(component.localization?.tag ?? 'en')\n }\n\n const panel = useMemo(() => standalonePanelSchema(component), [component])\n const page = state.page\n const direction = component.localization?.direction ?? 'ltr'\n\n useEffect(() => {\n ensureStandaloneRuntime()\n }, [])\n\n // Partial reloads (sorting, paging, inline edits) request this component's own prop.\n const propName = findStandalonePropName(props, component)\n\n // A layout effect runs before the children's effects: a `deferLoading()` table's first\n // reload must already ask for this component's prop, not the panel `page` prop.\n useIsomorphicLayoutEffect(() => registerStandaloneProp(propName ?? null), [propName])\n\n useEffect(() => {\n void state.loadLazy(component)\n // The token identifies a new payload.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [state, component.token])\n\n // Toasts: use the layout's <AlfakitToaster /> when there is one, else the first component\n // on the page renders one (checked after every component's effects ran).\n const [ownsToaster, setOwnsToaster] = useState(false)\n\n useEffect(() => {\n if (parent) return\n\n let release: (() => void) | null = null\n let cancelled = false\n\n queueMicrotask(() => {\n if (cancelled || hasToaster()) return\n\n release = claimToaster()\n setOwnsToaster(true)\n })\n\n return () => {\n cancelled = true\n release?.()\n }\n }, [parent])\n\n const render = (block: ContentBlock) => <ContentBlockRenderer key={block.key} block={block} />\n const actionContext = useMemo(\n () => ({ token: component.token, component: 'page' }),\n [component.token],\n )\n\n const body = (\n <PageActionProvider value={actionContext}>\n <div\n data-slot=\"standalone\"\n data-alfakit-component={component.class}\n dir={direction}\n className={cn('flex flex-col gap-6', className)}\n >\n {(page.heading || page.headerActions?.length) && (\n <header\n data-slot=\"page-header\"\n className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\"\n >\n <div className=\"min-w-0 sm:max-w-[50%] sm:shrink-0\">\n {page.heading && (\n <h2 className=\"text-xl font-semibold tracking-tight\">{page.heading}</h2>\n )}\n {page.subheading && (\n <p className=\"text-muted-foreground mt-1 text-sm\">{page.subheading}</p>\n )}\n </div>\n {page.headerActions && page.headerActions.length > 0 && (\n <ActionList\n actions={page.headerActions}\n context={actionContext}\n align=\"end\"\n className=\"max-w-full min-w-0\"\n />\n )}\n </header>\n )}\n {header}\n {children ? children(page.content, render) : page.content.map(render)}\n {footer}\n </div>\n <ActionModals />\n {ownsToaster && <AlfakitToaster claimed dir={direction} />}\n </PageActionProvider>\n )\n\n return (\n <AlfakitProvider value={context}>\n <StandaloneProvider value={{ panel, page }}>\n {parent ? (\n body\n ) : (\n <Direction.Provider dir={direction}>\n <TooltipProvider delayDuration={0}>{body}</TooltipProvider>\n </Direction.Provider>\n )}\n </StandaloneProvider>\n </AlfakitProvider>\n )\n}\n\n/**\n * The host page's Inertia props, or undefined when rendered outside an Inertia app (a\n * component test, Storybook, a plain server render): partial reloads then reload every prop.\n */\nfunction useOptionalPageProps(): Record<string, unknown> | undefined {\n try {\n return usePage().props as Record<string, unknown>\n } catch {\n return undefined\n }\n}\n"],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { useOptionalAlfakit } from "../context.js";
2
- import { claimToaster, createStandaloneContext, ensureFlashToasts } from "./runtime.js";
2
+ import { claimToaster } from "./toaster-claims.js";
3
+ import { createStandaloneContext, ensureFlashToasts } from "./runtime.js";
3
4
  import { setToasterId } from "../components/notifications/flash-toasts.js";
4
5
  import { Toaster } from "../components/ui/sonner.js";
5
6
  import { STARTER_KIT_TOASTER_ID, resolveToastPosition } from "@alfakit/core";
@@ -1 +1 @@
1
- {"version":3,"file":"alfakit-toaster.js","names":[],"sources":["../../src/standalone/alfakit-toaster.tsx"],"sourcesContent":["import { resolveToastPosition, STARTER_KIT_TOASTER_ID, type ToastPosition } from '@alfakit/core'\nimport { useEffect, useMemo } from 'react'\nimport { setToasterId } from '../components/notifications/flash-toasts'\nimport { Toaster } from '../components/ui/sonner'\nimport { useOptionalAlfakit, type AlfakitContextValue } from '../context'\nimport { claimToaster, createStandaloneContext, ensureFlashToasts } from './runtime'\n\nexport interface AlfakitToasterProps {\n /** Where toasts appear; `start`/`end` follow the reading direction. */\n position?: ToastPosition\n dir?: 'ltr' | 'rtl'\n /**\n * Render sonner's `<Toaster>` too (default). Pass `false` when your layout already mounts\n * one: flashed Alfakit notifications are then shown through it.\n */\n toaster?: boolean\n /** @internal Set by `<AlfakitComponent>` when it takes the toaster role. */\n claimed?: boolean\n}\n\n/**\n * Shows notifications flashed by standalone components (`Notification::make()->send()`\n * from `submit()` or an action) as toasts. Place it once in your app layout; a page whose\n * layout has none gets one from its first `<AlfakitComponent>`.\n *\n * ```tsx\n * <AlfakitToaster />\n * ```\n */\nexport function AlfakitToaster({\n position,\n dir = 'ltr',\n toaster = true,\n claimed = false,\n}: AlfakitToasterProps) {\n const parent = useOptionalAlfakit()\n\n // Outside <AlfakitComponent> the flash reader needs a registry for custom toast views.\n const context = useMemo<AlfakitContextValue>(\n () => parent ?? createStandaloneContext({ action: '', modal: '' }, undefined, undefined),\n [parent],\n )\n\n useEffect(() => (claimed ? undefined : claimToaster()), [claimed])\n\n useEffect(() => {\n ensureFlashToasts(context.registry)\n }, [context])\n\n // Our own toaster gets an id, so a `<Toaster />` the app's layout already mounts (the\n // starter kits have one) doesn't show every Alfakit toast a second time.\n useEffect(() => {\n if (!toaster) return undefined\n\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => setToasterId(undefined)\n }, [toaster])\n\n if (!toaster) return null\n\n return (\n <Toaster\n id={STARTER_KIT_TOASTER_ID}\n position={resolveToastPosition(position, dir)}\n dir={dir}\n richColors\n closeButton\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BA,SAAgB,eAAe,EAC7B,UACA,MAAM,OACN,UAAU,MACV,UAAU,SACY;CACtB,MAAM,SAAS,mBAAmB;CAGlC,MAAM,UAAU,cACR,UAAU,wBAAwB;EAAE,QAAQ;EAAI,OAAO;CAAG,GAAG,KAAA,GAAW,KAAA,CAAS,GACvF,CAAC,MAAM,CACT;CAEA,gBAAiB,UAAU,KAAA,IAAY,aAAa,GAAI,CAAC,OAAO,CAAC;CAEjE,gBAAgB;EACd,kBAAkB,QAAQ,QAAQ;CACpC,GAAG,CAAC,OAAO,CAAC;CAIZ,gBAAgB;EACd,IAAI,CAAC,SAAS,OAAO,KAAA;EAErB,aAAa,sBAAsB;EAEnC,aAAa,aAAa,KAAA,CAAS;CACrC,GAAG,CAAC,OAAO,CAAC;CAEZ,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,oBAAC,SAAD;EACE,IAAI;EACJ,UAAU,qBAAqB,UAAU,GAAG;EACvC;EACL,YAAA;EACA,aAAA;CACD,CAAA;AAEL"}
1
+ {"version":3,"file":"alfakit-toaster.js","names":[],"sources":["../../src/standalone/alfakit-toaster.tsx"],"sourcesContent":["import { resolveToastPosition, STARTER_KIT_TOASTER_ID, type ToastPosition } from '@alfakit/core'\nimport { useEffect, useMemo } from 'react'\nimport { setToasterId } from '../components/notifications/flash-toasts'\nimport { Toaster } from '../components/ui/sonner'\nimport { useOptionalAlfakit, type AlfakitContextValue } from '../context'\nimport { claimToaster, createStandaloneContext, ensureFlashToasts } from './runtime'\n\nexport interface AlfakitToasterProps {\n /** Where toasts appear; `start`/`end` follow the reading direction. */\n position?: ToastPosition\n dir?: 'ltr' | 'rtl'\n /**\n * Render sonner's `<Toaster>` too (default). Pass `false` when your layout already mounts\n * one: flashed Alfakit notifications are then shown through it.\n */\n toaster?: boolean\n /** @internal Set by `<AlfakitComponent>` when it takes the toaster role. */\n claimed?: boolean\n}\n\n/**\n * Shows notifications flashed by standalone components (`Notification::make()->send()`\n * from `submit()` or an action) as toasts. Place it once in your app layout; a page whose\n * layout has none gets one from its first `<AlfakitComponent>`.\n *\n * ```tsx\n * <AlfakitToaster />\n * ```\n */\nexport function AlfakitToaster({\n position,\n dir = 'ltr',\n toaster = true,\n claimed = false,\n}: AlfakitToasterProps) {\n const parent = useOptionalAlfakit()\n\n // Outside <AlfakitComponent> the flash reader needs a registry for custom toast views.\n const context = useMemo<AlfakitContextValue>(\n () => parent ?? createStandaloneContext({ action: '', modal: '' }, undefined, undefined),\n [parent],\n )\n\n useEffect(() => (claimed ? undefined : claimToaster()), [claimed])\n\n useEffect(() => {\n ensureFlashToasts(context.registry)\n }, [context])\n\n // Our own toaster gets an id, so a `<Toaster />` the app's layout already mounts (the\n // starter kits have one) doesn't show every Alfakit toast a second time.\n useEffect(() => {\n if (!toaster) return undefined\n\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => setToasterId(undefined)\n }, [toaster])\n\n if (!toaster) return null\n\n return (\n <Toaster\n id={STARTER_KIT_TOASTER_ID}\n position={resolveToastPosition(position, dir)}\n dir={dir}\n richColors\n closeButton\n />\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA6BA,SAAgB,eAAe,EAC7B,UACA,MAAM,OACN,UAAU,MACV,UAAU,SACY;CACtB,MAAM,SAAS,mBAAmB;CAGlC,MAAM,UAAU,cACR,UAAU,wBAAwB;EAAE,QAAQ;EAAI,OAAO;CAAG,GAAG,KAAA,GAAW,KAAA,CAAS,GACvF,CAAC,MAAM,CACT;CAEA,gBAAiB,UAAU,KAAA,IAAY,aAAa,GAAI,CAAC,OAAO,CAAC;CAEjE,gBAAgB;EACd,kBAAkB,QAAQ,QAAQ;CACpC,GAAG,CAAC,OAAO,CAAC;CAIZ,gBAAgB;EACd,IAAI,CAAC,SAAS,OAAO,KAAA;EAErB,aAAa,sBAAsB;EAEnC,aAAa,aAAa,KAAA,CAAS;CACrC,GAAG,CAAC,OAAO,CAAC;CAEZ,IAAI,CAAC,SAAS,OAAO;CAErB,OACE,oBAAC,SAAD;EACE,IAAI;EACJ,UAAU,qBAAqB,UAAU,GAAG;EACvC;EACL,YAAA;EACA,aAAA;CACD,CAAA;AAEL"}
@@ -1,5 +1,6 @@
1
1
  import { createDefaultRegistry } from "../registry.js";
2
2
  import { getStandaloneDefaults } from "./configure.js";
3
+ import { claimToaster, hasToaster } from "./toaster-claims.js";
3
4
  import { showToast } from "../components/notifications/flash-toasts.js";
4
5
  import { ActionRunner, ComponentRegistry, FlashNotificationTracker, Translator, installPrefetchInvalidation, reloadOnExpiredPage, resolveRequestErrorNotification } from "@alfakit/core";
5
6
  import { router } from "@inertiajs/react";
@@ -48,17 +49,6 @@ function createStandaloneContext(endpoints, overrides, poll) {
48
49
  poll
49
50
  };
50
51
  }
51
- let toasters = 0;
52
- /** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
53
- function claimToaster() {
54
- toasters++;
55
- return () => {
56
- toasters--;
57
- };
58
- }
59
- function hasToaster() {
60
- return toasters > 0;
61
- }
62
52
  const flashTracker = new FlashNotificationTracker();
63
53
  let flashRegistry = null;
64
54
  let flashListening = false;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","names":[],"sources":["../../src/standalone/runtime.ts"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n FlashNotificationTracker,\n Translator,\n installPrefetchInvalidation,\n reloadOnExpiredPage,\n resolveRequestErrorNotification,\n type NotificationSchema,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { showToast } from '../components/notifications/flash-toasts'\nimport type { AlfakitContextValue, AnyComponent } from '../context'\nimport { createDefaultRegistry } from '../registry'\nimport { getStandaloneDefaults } from './configure'\n\nconst isServer = typeof window === 'undefined'\n\n// Translations are the same for every component of a page, so the browser shares one\n// translator; a server render gets its own so concurrent requests never share a locale.\nconst browserTranslator = isServer ? null : new Translator()\n\nlet browserReady = false\n\n/** Browser-wide listeners every panel app installs; once per page load. */\nexport function ensureStandaloneRuntime(): void {\n if (isServer || browserReady) return\n\n browserReady = true\n reloadOnExpiredPage()\n // Pages restored by Back after a change are reloaded; shared with a surrounding panel.\n installPrefetchInvalidation()\n}\n\n/**\n * The registry of a standalone component: the built-in renderers, the `configureAlfakit()`\n * defaults (or the surrounding panel's registry), then the component's own overrides.\n */\nexport function createStandaloneRegistry(\n overrides?: Record<string, AnyComponent>,\n base?: ComponentRegistry<AnyComponent>,\n): ComponentRegistry<AnyComponent> {\n if (!base) {\n return createDefaultRegistry()\n .registerMany(getStandaloneDefaults().components ?? {})\n .registerMany(overrides ?? {})\n }\n\n if (!overrides || Object.keys(overrides).length === 0) {\n return base\n }\n\n const registry = new ComponentRegistry<AnyComponent>()\n\n for (const type of base.types()) {\n const component = base.resolve(type)\n\n if (component) registry.register(type, component)\n }\n\n return registry.registerMany(overrides)\n}\n\n/**\n * The context value of one `<AlfakitComponent>` rendered outside `createAlfakit()`: its\n * own registry and action runner (so its modals are its own), the shared translator.\n */\nexport function createStandaloneContext(\n endpoints: { action: string; modal: string },\n overrides: Record<string, AnyComponent> | undefined,\n poll: AlfakitContextValue['poll'],\n): AlfakitContextValue {\n const translator = browserTranslator ?? new Translator()\n\n return {\n registry: createStandaloneRegistry(overrides),\n pages: {},\n translator,\n actions: new ActionRunner({\n endpoints: { ...endpoints },\n // Outside a panel there is no `errorNotifications` config: a generic toast.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n null,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n poll,\n }\n}\n\n// ---------------------------------------------------------------- Toaster ownership\n\nlet toasters = 0\n\n/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */\nexport function claimToaster(): () => void {\n toasters++\n\n return () => {\n toasters--\n }\n}\n\nexport function hasToaster(): boolean {\n return toasters > 0\n}\n\n// ---------------------------------------------------------------- Flashed notifications\n\nconst flashTracker = new FlashNotificationTracker()\nlet flashRegistry: ComponentRegistry<AnyComponent> | null = null\nlet flashListening = false\n\n/**\n * Show the notifications the server flashed (`Notification::make()->send()`), from the\n * `flash` Inertia event after each visit and from the initial page (whose event fired\n * before the app mounted). One listener per page load, so it survives layouts that remount\n * between visits; the registry of the last mounted toaster renders custom toast views.\n */\nexport function ensureFlashToasts(registry: ComponentRegistry<AnyComponent>): void {\n flashRegistry = registry\n\n if (isServer || flashListening) return\n\n flashListening = true\n\n const show = (flash: unknown) => {\n const flashed = (flash as { alfakitNotifications?: NotificationSchema[] } | undefined)\n ?.alfakitNotifications\n\n for (const notification of flashTracker.fresh(flashed)) {\n showToast(notification, flashRegistry?.resolve(notification.view ?? ''))\n }\n }\n\n show(initialFlash())\n router.on('flash', (event) => show(event.detail.flash))\n}\n\n/** The initial page's flash data (fired as an event before the app mounted). */\nexport function initialFlash(): unknown {\n if (isServer) return undefined\n\n try {\n const page = document.querySelector<HTMLElement>('[data-page]')?.dataset.page\n\n return page ? (JSON.parse(page) as { flash?: unknown }).flash : undefined\n } catch {\n return undefined\n }\n}\n"],"mappings":";;;;;;AAgBA,MAAM,WAAW,OAAO,WAAW;AAInC,MAAM,oBAAoB,WAAW,OAAO,IAAI,WAAW;AAE3D,IAAI,eAAe;;AAGnB,SAAgB,0BAAgC;CAC9C,IAAI,YAAY,cAAc;CAE9B,eAAe;CACf,oBAAoB;CAEpB,4BAA4B;AAC9B;;;;;AAMA,SAAgB,yBACd,WACA,MACiC;CACjC,IAAI,CAAC,MACH,OAAO,sBAAsB,CAAC,CAC3B,aAAa,sBAAsB,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CACtD,aAAa,aAAa,CAAC,CAAC;CAGjC,IAAI,CAAC,aAAa,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,GAClD,OAAO;CAGT,MAAM,WAAW,IAAI,kBAAgC;CAErD,KAAK,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC/B,MAAM,YAAY,KAAK,QAAQ,IAAI;EAEnC,IAAI,WAAW,SAAS,SAAS,MAAM,SAAS;CAClD;CAEA,OAAO,SAAS,aAAa,SAAS;AACxC;;;;;AAMA,SAAgB,wBACd,WACA,WACA,MACqB;CACrB,MAAM,aAAa,qBAAqB,IAAI,WAAW;CAEvD,OAAO;EACL,UAAU,yBAAyB,SAAS;EAC5C,OAAO,CAAC;EACR;EACA,SAAS,IAAI,aAAa;GACxB,WAAW,EAAE,GAAG,UAAU;GAE1B,UAAU,UAAU;IAClB,MAAM,eAAe,gCACnB,OACA,OACC,KAAK,cAAc,aAAa,WAAW,EAAE,KAAK,cAAc,QAAQ,CAC3E;IAEA,IAAI,cAAc,UAAU,YAAY;GAC1C;EACF,CAAC;EACD;CACF;AACF;AAIA,IAAI,WAAW;;AAGf,SAAgB,eAA2B;CACzC;CAEA,aAAa;EACX;CACF;AACF;AAEA,SAAgB,aAAsB;CACpC,OAAO,WAAW;AACpB;AAIA,MAAM,eAAe,IAAI,yBAAyB;AAClD,IAAI,gBAAwD;AAC5D,IAAI,iBAAiB;;;;;;;AAQrB,SAAgB,kBAAkB,UAAiD;CACjF,gBAAgB;CAEhB,IAAI,YAAY,gBAAgB;CAEhC,iBAAiB;CAEjB,MAAM,QAAQ,UAAmB;EAC/B,MAAM,UAAW,OACb;EAEJ,KAAK,MAAM,gBAAgB,aAAa,MAAM,OAAO,GACnD,UAAU,cAAc,eAAe,QAAQ,aAAa,QAAQ,EAAE,CAAC;CAE3E;CAEA,KAAK,aAAa,CAAC;CACnB,OAAO,GAAG,UAAU,UAAU,KAAK,MAAM,OAAO,KAAK,CAAC;AACxD;;AAGA,SAAgB,eAAwB;CACtC,IAAI,UAAU,OAAO,KAAA;CAErB,IAAI;EACF,MAAM,OAAO,SAAS,cAA2B,aAAa,CAAC,EAAE,QAAQ;EAEzE,OAAO,OAAQ,KAAK,MAAM,IAAI,CAAC,CAAyB,QAAQ,KAAA;CAClE,QAAQ;EACN;CACF;AACF"}
1
+ {"version":3,"file":"runtime.js","names":[],"sources":["../../src/standalone/runtime.ts"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n FlashNotificationTracker,\n Translator,\n installPrefetchInvalidation,\n reloadOnExpiredPage,\n resolveRequestErrorNotification,\n type NotificationSchema,\n} from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { showToast } from '../components/notifications/flash-toasts'\nimport type { AlfakitContextValue, AnyComponent } from '../context'\nimport { createDefaultRegistry } from '../registry'\nimport { getStandaloneDefaults } from './configure'\n\nconst isServer = typeof window === 'undefined'\n\n// Translations are the same for every component of a page, so the browser shares one\n// translator; a server render gets its own so concurrent requests never share a locale.\nconst browserTranslator = isServer ? null : new Translator()\n\nlet browserReady = false\n\n/** Browser-wide listeners every panel app installs; once per page load. */\nexport function ensureStandaloneRuntime(): void {\n if (isServer || browserReady) return\n\n browserReady = true\n reloadOnExpiredPage()\n // Pages restored by Back after a change are reloaded; shared with a surrounding panel.\n installPrefetchInvalidation()\n}\n\n/**\n * The registry of a standalone component: the built-in renderers, the `configureAlfakit()`\n * defaults (or the surrounding panel's registry), then the component's own overrides.\n */\nexport function createStandaloneRegistry(\n overrides?: Record<string, AnyComponent>,\n base?: ComponentRegistry<AnyComponent>,\n): ComponentRegistry<AnyComponent> {\n if (!base) {\n return createDefaultRegistry()\n .registerMany(getStandaloneDefaults().components ?? {})\n .registerMany(overrides ?? {})\n }\n\n if (!overrides || Object.keys(overrides).length === 0) {\n return base\n }\n\n const registry = new ComponentRegistry<AnyComponent>()\n\n for (const type of base.types()) {\n const component = base.resolve(type)\n\n if (component) registry.register(type, component)\n }\n\n return registry.registerMany(overrides)\n}\n\n/**\n * The context value of one `<AlfakitComponent>` rendered outside `createAlfakit()`: its\n * own registry and action runner (so its modals are its own), the shared translator.\n */\nexport function createStandaloneContext(\n endpoints: { action: string; modal: string },\n overrides: Record<string, AnyComponent> | undefined,\n poll: AlfakitContextValue['poll'],\n): AlfakitContextValue {\n const translator = browserTranslator ?? new Translator()\n\n return {\n registry: createStandaloneRegistry(overrides),\n pages: {},\n translator,\n actions: new ActionRunner({\n endpoints: { ...endpoints },\n // Outside a panel there is no `errorNotifications` config: a generic toast.\n onError: (error) => {\n const notification = resolveRequestErrorNotification(\n error,\n null,\n (key, replacements, fallback) => translator.t(key, replacements, fallback),\n )\n\n if (notification) showToast(notification)\n },\n }),\n poll,\n }\n}\n\n// ---------------------------------------------------------------- Toaster ownership\n\n// In a module of its own: the starter kit header components check it without loading this one.\nexport { claimToaster, hasToaster } from './toaster-claims'\n\n// ---------------------------------------------------------------- Flashed notifications\n\nconst flashTracker = new FlashNotificationTracker()\nlet flashRegistry: ComponentRegistry<AnyComponent> | null = null\nlet flashListening = false\n\n/**\n * Show the notifications the server flashed (`Notification::make()->send()`), from the\n * `flash` Inertia event after each visit and from the initial page (whose event fired\n * before the app mounted). One listener per page load, so it survives layouts that remount\n * between visits; the registry of the last mounted toaster renders custom toast views.\n */\nexport function ensureFlashToasts(registry: ComponentRegistry<AnyComponent>): void {\n flashRegistry = registry\n\n if (isServer || flashListening) return\n\n flashListening = true\n\n const show = (flash: unknown) => {\n const flashed = (flash as { alfakitNotifications?: NotificationSchema[] } | undefined)\n ?.alfakitNotifications\n\n for (const notification of flashTracker.fresh(flashed)) {\n showToast(notification, flashRegistry?.resolve(notification.view ?? ''))\n }\n }\n\n show(initialFlash())\n router.on('flash', (event) => show(event.detail.flash))\n}\n\n/** The initial page's flash data (fired as an event before the app mounted). */\nexport function initialFlash(): unknown {\n if (isServer) return undefined\n\n try {\n const page = document.querySelector<HTMLElement>('[data-page]')?.dataset.page\n\n return page ? (JSON.parse(page) as { flash?: unknown }).flash : undefined\n } catch {\n return undefined\n }\n}\n"],"mappings":";;;;;;;AAgBA,MAAM,WAAW,OAAO,WAAW;AAInC,MAAM,oBAAoB,WAAW,OAAO,IAAI,WAAW;AAE3D,IAAI,eAAe;;AAGnB,SAAgB,0BAAgC;CAC9C,IAAI,YAAY,cAAc;CAE9B,eAAe;CACf,oBAAoB;CAEpB,4BAA4B;AAC9B;;;;;AAMA,SAAgB,yBACd,WACA,MACiC;CACjC,IAAI,CAAC,MACH,OAAO,sBAAsB,CAAC,CAC3B,aAAa,sBAAsB,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CACtD,aAAa,aAAa,CAAC,CAAC;CAGjC,IAAI,CAAC,aAAa,OAAO,KAAK,SAAS,CAAC,CAAC,WAAW,GAClD,OAAO;CAGT,MAAM,WAAW,IAAI,kBAAgC;CAErD,KAAK,MAAM,QAAQ,KAAK,MAAM,GAAG;EAC/B,MAAM,YAAY,KAAK,QAAQ,IAAI;EAEnC,IAAI,WAAW,SAAS,SAAS,MAAM,SAAS;CAClD;CAEA,OAAO,SAAS,aAAa,SAAS;AACxC;;;;;AAMA,SAAgB,wBACd,WACA,WACA,MACqB;CACrB,MAAM,aAAa,qBAAqB,IAAI,WAAW;CAEvD,OAAO;EACL,UAAU,yBAAyB,SAAS;EAC5C,OAAO,CAAC;EACR;EACA,SAAS,IAAI,aAAa;GACxB,WAAW,EAAE,GAAG,UAAU;GAE1B,UAAU,UAAU;IAClB,MAAM,eAAe,gCACnB,OACA,OACC,KAAK,cAAc,aAAa,WAAW,EAAE,KAAK,cAAc,QAAQ,CAC3E;IAEA,IAAI,cAAc,UAAU,YAAY;GAC1C;EACF,CAAC;EACD;CACF;AACF;AASA,MAAM,eAAe,IAAI,yBAAyB;AAClD,IAAI,gBAAwD;AAC5D,IAAI,iBAAiB;;;;;;;AAQrB,SAAgB,kBAAkB,UAAiD;CACjF,gBAAgB;CAEhB,IAAI,YAAY,gBAAgB;CAEhC,iBAAiB;CAEjB,MAAM,QAAQ,UAAmB;EAC/B,MAAM,UAAW,OACb;EAEJ,KAAK,MAAM,gBAAgB,aAAa,MAAM,OAAO,GACnD,UAAU,cAAc,eAAe,QAAQ,aAAa,QAAQ,EAAE,CAAC;CAE3E;CAEA,KAAK,aAAa,CAAC;CACnB,OAAO,GAAG,UAAU,UAAU,KAAK,MAAM,OAAO,KAAK,CAAC;AACxD;;AAGA,SAAgB,eAAwB;CACtC,IAAI,UAAU,OAAO,KAAA;CAErB,IAAI;EACF,MAAM,OAAO,SAAS,cAA2B,aAAa,CAAC,EAAE,QAAQ;EAEzE,OAAO,OAAQ,KAAK,MAAM,IAAI,CAAC,CAAyB,QAAQ,KAAA;CAClE,QAAQ;EACN;CACF;AACF"}
@@ -0,0 +1,16 @@
1
+ //#region src/standalone/toaster-claims.ts
2
+ let toasters = 0;
3
+ /** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */
4
+ function claimToaster() {
5
+ toasters++;
6
+ return () => {
7
+ toasters--;
8
+ };
9
+ }
10
+ function hasToaster() {
11
+ return toasters > 0;
12
+ }
13
+ //#endregion
14
+ export { claimToaster, hasToaster };
15
+
16
+ //# sourceMappingURL=toaster-claims.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toaster-claims.js","names":[],"sources":["../../src/standalone/toaster-claims.ts"],"sourcesContent":["// Who renders AlfaKit's toaster on the page: `<AlfakitToaster>`, `AlfakitRoot`, the first\n// standalone component or the starter kit header components on the host's own pages.\nlet toasters = 0\n\n/** An `<AlfakitToaster>` (or the component that took the role) is mounted; returns a release. */\nexport function claimToaster(): () => void {\n toasters++\n\n return () => {\n toasters--\n }\n}\n\nexport function hasToaster(): boolean {\n return toasters > 0\n}\n"],"mappings":";AAEA,IAAI,WAAW;;AAGf,SAAgB,eAA2B;CACzC;CAEA,aAAa;EACX;CACF;AACF;AAEA,SAAgB,aAAsB;CACpC,OAAO,WAAW;AACpB"}
@@ -1,8 +1,11 @@
1
- import { AlfakitProvider, StandaloneProvider, useLocalization, useT } from "../context.js";
1
+ import { AlfakitProvider, StandaloneProvider, useLocalization, useRegistry, useT } from "../context.js";
2
2
  import { Icon } from "../lib/icons.js";
3
3
  import { DropdownMenu as DropdownMenu$1, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../components/ui/dropdown-menu.js";
4
4
  import { TooltipProvider } from "../components/ui/tooltip.js";
5
5
  import { AiCopilot } from "../components/ai/copilot.js";
6
+ import { claimToaster, hasToaster } from "../standalone/toaster-claims.js";
7
+ import { ensureFlashToasts } from "../standalone/runtime.js";
8
+ import { setToasterId } from "../components/notifications/flash-toasts.js";
6
9
  import { TenantName } from "../components/panel/nav-label.js";
7
10
  import { Avatar as Avatar$1, AvatarFallback, AvatarImage } from "../components/ui/avatar.js";
8
11
  import { NotificationsMenu } from "../components/notifications/notifications-menu.js";
@@ -10,13 +13,15 @@ import { RealtimeNotifications } from "../components/notifications/realtime-noti
10
13
  import { GlobalSearch } from "../components/panel/global-search.js";
11
14
  import { TenantMenu } from "../components/panel/tenant-menu.js";
12
15
  import { LocaleOptions } from "../components/panel/locale-switcher.js";
16
+ import { Toaster } from "../components/ui/sonner.js";
13
17
  import { CopilotTrigger, LanguageSwitcherTrigger, NotificationsTrigger, SearchTrigger, TenantSwitcherTrigger, languageTriggerClasses } from "./widgets-triggers.js";
14
- import { ActionRunner, ComponentRegistry, Translator, initials, isAlfakitPage, loadStarterKitPanel, loadedStarterKitPanel, openRequestedStarterKitSearch, showsLanguageSwitcher, starterKitCopilot, starterKitPanelLocale, starterKitWidgetsPage, starterKitWidgetsPanel, takeRequestedStarterKitCopilot, takeRequestedStarterKitLanguages, tenantNameText } from "@alfakit/core";
18
+ import { ActionRunner, ComponentRegistry, STARTER_KIT_TOASTER_ID, Translator, initials, isAlfakitPage, loadStarterKitPanel, loadedStarterKitPanel, openRequestedStarterKitSearch, overlayPlacementDirection, resolveToastPosition, showsLanguageSwitcher, starterKitCopilot, starterKitPanelLocale, starterKitWidgetsPage, starterKitWidgetsPanel, takeRequestedStarterKitCopilot, takeRequestedStarterKitLanguages, tenantNameText } from "@alfakit/core";
15
19
  import { router, usePage } from "@inertiajs/react";
16
20
  import { useEffect, useMemo, useRef, useState } from "react";
17
21
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
22
  import { Direction } from "radix-ui";
19
23
  import { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from "lucide-react";
24
+ import { createPortal } from "react-dom";
20
25
  //#region src/starter-kit/widgets-loaded.tsx
21
26
  /**
22
27
  * The loaded header components of starter kit mode: the panel's own search palette,
@@ -66,23 +71,75 @@ function useWidgetsPanel(source) {
66
71
  widgets
67
72
  ]);
68
73
  }
74
+ /**
75
+ * AlfaKit's toaster on a host page (as `<AlfakitToaster>`, without its standalone context):
76
+ * Alfakit's toasts go to it, and so do flashed notifications.
77
+ */
78
+ function HostToaster({ position, dir }) {
79
+ const registry = useRegistry();
80
+ useEffect(() => {
81
+ ensureFlashToasts(registry);
82
+ }, [registry]);
83
+ useEffect(() => {
84
+ setToasterId(STARTER_KIT_TOASTER_ID);
85
+ return () => setToasterId(void 0);
86
+ }, []);
87
+ return /* @__PURE__ */ jsx(Toaster, {
88
+ id: STARTER_KIT_TOASTER_ID,
89
+ position: resolveToastPosition(position, dir),
90
+ dir,
91
+ richColors: true,
92
+ closeButton: true
93
+ });
94
+ }
95
+ /**
96
+ * On the host's own pages no AlfaKit toaster is mounted, so the first loaded header
97
+ * component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).
98
+ * AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check
99
+ * runs after every component's effects, and again after each visit.
100
+ */
101
+ function useOwnsHostToaster() {
102
+ const alfakitPage = isAlfakitPage(usePage().props);
103
+ const [owns, setOwns] = useState(false);
104
+ useEffect(() => {
105
+ if (alfakitPage) return void 0;
106
+ let release = null;
107
+ let cancelled = false;
108
+ queueMicrotask(() => {
109
+ if (cancelled || hasToaster()) return;
110
+ release = claimToaster();
111
+ setOwns(true);
112
+ });
113
+ return () => {
114
+ cancelled = true;
115
+ release?.();
116
+ setOwns(false);
117
+ };
118
+ }, [alfakitPage]);
119
+ return owns && !alfakitPage;
120
+ }
69
121
  function WidgetShell({ panel, children }) {
70
122
  const context = useWidgetsContext(panel);
71
123
  const page = useMemo(() => starterKitWidgetsPage(), []);
124
+ const ownsToaster = useOwnsHostToaster();
125
+ const direction = panel.localization?.direction ?? "ltr";
72
126
  return /* @__PURE__ */ jsx(AlfakitProvider, {
73
127
  value: context,
74
- children: /* @__PURE__ */ jsx(StandaloneProvider, {
128
+ children: /* @__PURE__ */ jsxs(StandaloneProvider, {
75
129
  value: {
76
130
  panel,
77
131
  page
78
132
  },
79
- children: /* @__PURE__ */ jsx(Direction.Provider, {
80
- dir: panel.localization?.direction ?? "ltr",
133
+ children: [/* @__PURE__ */ jsx(Direction.Provider, {
134
+ dir: direction,
81
135
  children: /* @__PURE__ */ jsx(TooltipProvider, {
82
136
  delayDuration: 0,
83
137
  children
84
138
  })
85
- })
139
+ }), ownsToaster && createPortal(/* @__PURE__ */ jsx(HostToaster, {
140
+ position: panel.toastPosition,
141
+ dir: overlayPlacementDirection(direction, true)
142
+ }), document.body)]
86
143
  })
87
144
  });
88
145
  }
@@ -1 +1 @@
1
- {"version":3,"file":"widgets-loaded.js","names":["DropdownMenu","Avatar"],"sources":["../../src/starter-kit/widgets-loaded.tsx"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n Translator,\n initials,\n isAlfakitPage,\n loadStarterKitPanel,\n loadedStarterKitPanel,\n openRequestedStarterKitSearch,\n showsLanguageSwitcher,\n starterKitCopilot,\n starterKitPanelLocale,\n starterKitWidgetsPage,\n starterKitWidgetsPanel,\n takeRequestedStarterKitCopilot,\n takeRequestedStarterKitLanguages,\n tenantNameText,\n type PanelSchema,\n type StarterKitWidgets,\n} from '@alfakit/core'\nimport { router, usePage } from '@inertiajs/react'\nimport { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from 'lucide-react'\nimport { Direction } from 'radix-ui'\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { AiCopilot } from '../components/ai/copilot'\nimport { NotificationsMenu } from '../components/notifications/notifications-menu'\nimport { RealtimeNotifications } from '../components/notifications/realtime-notifications'\nimport { GlobalSearch } from '../components/panel/global-search'\nimport { LocaleOptions } from '../components/panel/locale-switcher'\nimport { TenantName } from '../components/panel/nav-label'\nimport { TenantMenu } from '../components/panel/tenant-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../components/ui/dropdown-menu'\nimport { TooltipProvider } from '../components/ui/tooltip'\nimport {\n AlfakitProvider,\n StandaloneProvider,\n useLocalization,\n useT,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { Icon } from '../lib/icons'\nimport {\n CopilotTrigger,\n LanguageSwitcherTrigger,\n NotificationsTrigger,\n SearchTrigger,\n TenantSwitcherTrigger,\n languageTriggerClasses,\n type WidgetProps,\n} from './widgets-triggers'\n\n/**\n * The loaded header components of starter kit mode: the panel's own search palette,\n * notifications menu, tenant menu and language menu, rendered outside Alfakit pages with\n * the panel data from `widgets.endpoint`. This module only loads in the browser.\n */\n\n// One context for the browser session: its own translator (the panel's strings), an empty\n// registry (only `panel:icon` overrides are looked up here) and an idle action runner.\nlet widgetsContext: AlfakitContextValue | null = null\n\nfunction useWidgetsContext(panel: PanelSchema): AlfakitContextValue {\n widgetsContext ??= {\n registry: new ComponentRegistry<AnyComponent>(),\n pages: {},\n translator: new Translator(),\n actions: new ActionRunner({ endpoints: { action: '', modal: '' } }),\n }\n\n widgetsContext.translator.setTranslations(panel.translations ?? {})\n widgetsContext.translator.setLocale(panel.localization?.tag ?? 'en')\n\n return widgetsContext\n}\n\n/**\n * The panel the components render with, once its data has loaded (null until then). It is\n * loaded per endpoint (a tenant switch) and language (a language switch), so the components\n * follow a switch instead of keeping the old language's strings.\n */\nfunction useWidgetsPanel(source: StarterKitWidgets): PanelSchema | null {\n const endpoint = source.widgets.endpoint\n const locale = starterKitPanelLocale(source)\n const [loaded, setLoaded] = useState(() => loadedStarterKitPanel(endpoint, locale))\n // Until the new language's data arrives, not the old language's.\n const current =\n loaded && (locale === undefined || loaded.localization?.code === locale) ? loaded : null\n const base = loadedStarterKitPanel(endpoint, locale) ?? current\n\n useEffect(() => {\n let active = true\n\n void loadStarterKitPanel(endpoint, locale).then(\n (panel) => active && setLoaded(panel),\n () => {},\n )\n\n return () => {\n active = false\n }\n }, [endpoint, locale])\n\n const { navigation, widgets } = source\n\n return useMemo(\n () => (base ? starterKitWidgetsPanel(base, { navigation, widgets }) : null),\n [base, navigation, widgets],\n )\n}\n\nfunction WidgetShell({ panel, children }: { panel: PanelSchema; children: ReactNode }) {\n const context = useWidgetsContext(panel)\n const page = useMemo(() => starterKitWidgetsPage(), [])\n\n return (\n <AlfakitProvider value={context}>\n <StandaloneProvider value={{ panel, page }}>\n <Direction.Provider dir={panel.localization?.direction ?? 'ltr'}>\n <TooltipProvider delayDuration={0}>{children}</TooltipProvider>\n </Direction.Provider>\n </StandaloneProvider>\n </AlfakitProvider>\n )\n}\n\n/** The panel's command palette and global search. */\nexport function LoadedSearch({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n // A click or the key bindings before the palette loaded open it now (after its own\n // effects registered the shortcut).\n useEffect(() => {\n if (panel) openRequestedStarterKitSearch()\n }, [panel])\n\n if (!panel) return <SearchTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <GlobalSearch />\n </WidgetShell>\n )\n}\n\n/** The notifications bell, with realtime notifications on the host's own pages. */\nexport function LoadedNotifications({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // Alfakit pages subscribe to realtime notifications themselves.\n const alfakitPage = isAlfakitPage(usePage().props)\n\n if (!panel) return <NotificationsTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <NotificationsMenu />\n {!alfakitPage && <RealtimeNotifications />}\n </WidgetShell>\n )\n}\n\n/** The panel's tenant menu inside a tenant, a menu to choose one elsewhere. */\nexport function LoadedTenantSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <TenantSwitcherTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n {panel.tenancy?.current ? <TenantMenu variant=\"topbar\" /> : <TenantChooser panel={panel} />}\n </WidgetShell>\n )\n}\n\n/** The AI copilot (the panel pages' `topbar.end` slot component). */\nexport function LoadedCopilot({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // A click or the key bindings before the copilot loaded open it at once.\n const [autoOpen] = useState(() => takeRequestedStarterKitCopilot())\n const component = useMemo(() => (panel ? starterKitCopilot(panel) : null), [panel])\n\n if (!panel) return <CopilotTrigger source={source} />\n\n if (!component) return null\n\n return (\n <WidgetShell panel={panel}>\n <AiCopilot component={component} autoOpen={autoOpen || takeRequestedStarterKitCopilot()} />\n </WidgetShell>\n )\n}\n\n/**\n * The panel's language menu (its languages in their own names, `LocaleOptions`): picking\n * one posts to the panel's locale endpoint and reloads the page in it. Nothing without a\n * language switcher with two or more languages.\n */\nexport function LoadedLanguageSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <LanguageSwitcherTrigger source={source} />\n\n if (!showsLanguageSwitcher(panel.localization, 'auth')) return null\n\n return (\n <WidgetShell panel={panel}>\n <LanguageMenu />\n </WidgetShell>\n )\n}\n\nfunction LanguageMenu() {\n const t = useT()\n const localization = useLocalization()\n const [open, setOpen] = useState(false)\n // Taken once per mount (effects run twice in StrictMode).\n const requested = useRef<boolean | null>(null)\n\n // A click on the trigger before the menu loaded opens it once mounted: a frame later, as\n // the menu closes again when it opens while focus is still on the replaced button.\n useEffect(() => {\n requested.current ??= takeRequestedStarterKitLanguages()\n\n if (!requested.current) return\n\n const frame = requestAnimationFrame(() => setOpen(true))\n\n return () => cancelAnimationFrame(frame)\n }, [])\n const label = t('change_language', {}, 'Change language')\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"language-switcher-trigger\"\n aria-label={label}\n title={label}\n className={languageTriggerClasses}\n >\n <LanguagesIcon className=\"size-4\" aria-hidden=\"true\" />\n <span lang={localization.tag} dir={localization.direction} className=\"hidden sm:inline\">\n {localization.label}\n </span>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-40\">\n <LocaleOptions />\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** The user's tenants on a page outside the panel (no current tenant). */\nfunction TenantChooser({ panel }: { panel: PanelSchema }) {\n const t = useT()\n const tenancy = panel.tenancy\n\n if (!tenancy || (tenancy.tenants.length === 0 && !tenancy.registrationUrl)) return null\n\n const label = t('starter_kit.choose_tenant', {}, 'Choose a tenant')\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n className=\"hover:bg-accent data-[state=open]:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <span className=\"sr-only lg:not-sr-only lg:truncate\">{label}</span>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"text-muted-foreground size-4 shrink-0\"\n />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-56 rounded-lg\" side=\"bottom\" align=\"end\" sideOffset={4}>\n {tenancy.tenants.length > 0 && (\n <DropdownMenuLabel className=\"text-muted-foreground text-xs\">{label}</DropdownMenuLabel>\n )}\n {tenancy.tenants.map((tenant) => (\n <DropdownMenuItem key={String(tenant.id)} onSelect={() => router.visit(tenant.url)}>\n <Avatar className=\"size-6 rounded-sm\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt=\"\" />}\n <AvatarFallback className=\"rounded-sm text-[10px]\">\n {initials(tenantNameText(tenant))}\n </AvatarFallback>\n </Avatar>\n <TenantName tenant={tenant} className=\"flex-1 truncate\" />\n </DropdownMenuItem>\n ))}\n {tenancy.registrationUrl && (\n <>\n {tenancy.tenants.length > 0 && <DropdownMenuSeparator />}\n <DropdownMenuItem onSelect={() => router.visit(tenancy.registrationUrl!)}>\n <Icon name=\"alfakit::panels.tenant-menu.registration-button\" fallback={PlusIcon} />\n {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"widgets-loaded.js","names":["DropdownMenu","Avatar"],"sources":["../../src/starter-kit/widgets-loaded.tsx"],"sourcesContent":["import {\n ActionRunner,\n ComponentRegistry,\n STARTER_KIT_TOASTER_ID,\n Translator,\n initials,\n isAlfakitPage,\n loadStarterKitPanel,\n loadedStarterKitPanel,\n openRequestedStarterKitSearch,\n overlayPlacementDirection,\n resolveToastPosition,\n showsLanguageSwitcher,\n starterKitCopilot,\n starterKitPanelLocale,\n starterKitWidgetsPage,\n starterKitWidgetsPanel,\n takeRequestedStarterKitCopilot,\n takeRequestedStarterKitLanguages,\n tenantNameText,\n type PanelSchema,\n type StarterKitWidgets,\n type ToastPosition,\n} from '@alfakit/core'\nimport { router, usePage } from '@inertiajs/react'\nimport { ChevronsUpDownIcon, LanguagesIcon, PlusIcon } from 'lucide-react'\nimport { Direction } from 'radix-ui'\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { createPortal } from 'react-dom'\nimport { AiCopilot } from '../components/ai/copilot'\nimport { setToasterId } from '../components/notifications/flash-toasts'\nimport { NotificationsMenu } from '../components/notifications/notifications-menu'\nimport { RealtimeNotifications } from '../components/notifications/realtime-notifications'\nimport { GlobalSearch } from '../components/panel/global-search'\nimport { LocaleOptions } from '../components/panel/locale-switcher'\nimport { TenantName } from '../components/panel/nav-label'\nimport { TenantMenu } from '../components/panel/tenant-menu'\nimport { Avatar, AvatarFallback, AvatarImage } from '../components/ui/avatar'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuLabel,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../components/ui/dropdown-menu'\nimport { Toaster } from '../components/ui/sonner'\nimport { TooltipProvider } from '../components/ui/tooltip'\nimport {\n AlfakitProvider,\n StandaloneProvider,\n useLocalization,\n useRegistry,\n useT,\n type AlfakitContextValue,\n type AnyComponent,\n} from '../context'\nimport { Icon } from '../lib/icons'\nimport { ensureFlashToasts } from '../standalone/runtime'\nimport { claimToaster, hasToaster } from '../standalone/toaster-claims'\nimport {\n CopilotTrigger,\n LanguageSwitcherTrigger,\n NotificationsTrigger,\n SearchTrigger,\n TenantSwitcherTrigger,\n languageTriggerClasses,\n type WidgetProps,\n} from './widgets-triggers'\n\n/**\n * The loaded header components of starter kit mode: the panel's own search palette,\n * notifications menu, tenant menu and language menu, rendered outside Alfakit pages with\n * the panel data from `widgets.endpoint`. This module only loads in the browser.\n */\n\n// One context for the browser session: its own translator (the panel's strings), an empty\n// registry (only `panel:icon` overrides are looked up here) and an idle action runner.\nlet widgetsContext: AlfakitContextValue | null = null\n\nfunction useWidgetsContext(panel: PanelSchema): AlfakitContextValue {\n widgetsContext ??= {\n registry: new ComponentRegistry<AnyComponent>(),\n pages: {},\n translator: new Translator(),\n actions: new ActionRunner({ endpoints: { action: '', modal: '' } }),\n }\n\n widgetsContext.translator.setTranslations(panel.translations ?? {})\n widgetsContext.translator.setLocale(panel.localization?.tag ?? 'en')\n\n return widgetsContext\n}\n\n/**\n * The panel the components render with, once its data has loaded (null until then). It is\n * loaded per endpoint (a tenant switch) and language (a language switch), so the components\n * follow a switch instead of keeping the old language's strings.\n */\nfunction useWidgetsPanel(source: StarterKitWidgets): PanelSchema | null {\n const endpoint = source.widgets.endpoint\n const locale = starterKitPanelLocale(source)\n const [loaded, setLoaded] = useState(() => loadedStarterKitPanel(endpoint, locale))\n // Until the new language's data arrives, not the old language's.\n const current =\n loaded && (locale === undefined || loaded.localization?.code === locale) ? loaded : null\n const base = loadedStarterKitPanel(endpoint, locale) ?? current\n\n useEffect(() => {\n let active = true\n\n void loadStarterKitPanel(endpoint, locale).then(\n (panel) => active && setLoaded(panel),\n () => {},\n )\n\n return () => {\n active = false\n }\n }, [endpoint, locale])\n\n const { navigation, widgets } = source\n\n return useMemo(\n () => (base ? starterKitWidgetsPanel(base, { navigation, widgets }) : null),\n [base, navigation, widgets],\n )\n}\n\n/**\n * AlfaKit's toaster on a host page (as `<AlfakitToaster>`, without its standalone context):\n * Alfakit's toasts go to it, and so do flashed notifications.\n */\nfunction HostToaster({ position, dir }: { position?: ToastPosition; dir: 'ltr' | 'rtl' }) {\n const registry = useRegistry()\n\n useEffect(() => {\n ensureFlashToasts(registry)\n }, [registry])\n\n useEffect(() => {\n setToasterId(STARTER_KIT_TOASTER_ID)\n\n return () => setToasterId(undefined)\n }, [])\n\n return (\n <Toaster\n id={STARTER_KIT_TOASTER_ID}\n position={resolveToastPosition(position, dir)}\n dir={dir}\n richColors\n closeButton\n />\n )\n}\n\n/**\n * On the host's own pages no AlfaKit toaster is mounted, so the first loaded header\n * component renders one: the push toggle's and the PWA's messages show there too (W-S1-03).\n * AlfaKit pages (`AlfakitRoot`) and standalone components claim it themselves; the check\n * runs after every component's effects, and again after each visit.\n */\nfunction useOwnsHostToaster(): boolean {\n const alfakitPage = isAlfakitPage(usePage().props)\n const [owns, setOwns] = useState(false)\n\n useEffect(() => {\n if (alfakitPage) return undefined\n\n let release: (() => void) | null = null\n let cancelled = false\n\n queueMicrotask(() => {\n if (cancelled || hasToaster()) return\n\n release = claimToaster()\n setOwns(true)\n })\n\n return () => {\n cancelled = true\n release?.()\n setOwns(false)\n }\n }, [alfakitPage])\n\n return owns && !alfakitPage\n}\n\nfunction WidgetShell({ panel, children }: { panel: PanelSchema; children: ReactNode }) {\n const context = useWidgetsContext(panel)\n const page = useMemo(() => starterKitWidgetsPage(), [])\n const ownsToaster = useOwnsHostToaster()\n const direction = panel.localization?.direction ?? 'ltr'\n\n return (\n <AlfakitProvider value={context}>\n <StandaloneProvider value={{ panel, page }}>\n <Direction.Provider dir={direction}>\n <TooltipProvider delayDuration={0}>{children}</TooltipProvider>\n </Direction.Provider>\n {/* In the body, not in the header (a header with a backdrop filter would hold its\n fixed position), placed in the document's direction as on AlfaKit pages. */}\n {ownsToaster &&\n createPortal(\n <HostToaster\n position={panel.toastPosition}\n dir={overlayPlacementDirection(direction, true)}\n />,\n document.body,\n )}\n </StandaloneProvider>\n </AlfakitProvider>\n )\n}\n\n/** The panel's command palette and global search. */\nexport function LoadedSearch({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n // A click or the key bindings before the palette loaded open it now (after its own\n // effects registered the shortcut).\n useEffect(() => {\n if (panel) openRequestedStarterKitSearch()\n }, [panel])\n\n if (!panel) return <SearchTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <GlobalSearch />\n </WidgetShell>\n )\n}\n\n/** The notifications bell, with realtime notifications on the host's own pages. */\nexport function LoadedNotifications({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // Alfakit pages subscribe to realtime notifications themselves.\n const alfakitPage = isAlfakitPage(usePage().props)\n\n if (!panel) return <NotificationsTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n <NotificationsMenu />\n {!alfakitPage && <RealtimeNotifications />}\n </WidgetShell>\n )\n}\n\n/** The panel's tenant menu inside a tenant, a menu to choose one elsewhere. */\nexport function LoadedTenantSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <TenantSwitcherTrigger source={source} />\n\n return (\n <WidgetShell panel={panel}>\n {panel.tenancy?.current ? <TenantMenu variant=\"topbar\" /> : <TenantChooser panel={panel} />}\n </WidgetShell>\n )\n}\n\n/** The AI copilot (the panel pages' `topbar.end` slot component). */\nexport function LoadedCopilot({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n // A click or the key bindings before the copilot loaded open it at once.\n const [autoOpen] = useState(() => takeRequestedStarterKitCopilot())\n const component = useMemo(() => (panel ? starterKitCopilot(panel) : null), [panel])\n\n if (!panel) return <CopilotTrigger source={source} />\n\n if (!component) return null\n\n return (\n <WidgetShell panel={panel}>\n <AiCopilot component={component} autoOpen={autoOpen || takeRequestedStarterKitCopilot()} />\n </WidgetShell>\n )\n}\n\n/**\n * The panel's language menu (its languages in their own names, `LocaleOptions`): picking\n * one posts to the panel's locale endpoint and reloads the page in it. Nothing without a\n * language switcher with two or more languages.\n */\nexport function LoadedLanguageSwitcher({ source }: WidgetProps) {\n const panel = useWidgetsPanel(source)\n\n if (!panel) return <LanguageSwitcherTrigger source={source} />\n\n if (!showsLanguageSwitcher(panel.localization, 'auth')) return null\n\n return (\n <WidgetShell panel={panel}>\n <LanguageMenu />\n </WidgetShell>\n )\n}\n\nfunction LanguageMenu() {\n const t = useT()\n const localization = useLocalization()\n const [open, setOpen] = useState(false)\n // Taken once per mount (effects run twice in StrictMode).\n const requested = useRef<boolean | null>(null)\n\n // A click on the trigger before the menu loaded opens it once mounted: a frame later, as\n // the menu closes again when it opens while focus is still on the replaced button.\n useEffect(() => {\n requested.current ??= takeRequestedStarterKitLanguages()\n\n if (!requested.current) return\n\n const frame = requestAnimationFrame(() => setOpen(true))\n\n return () => cancelAnimationFrame(frame)\n }, [])\n const label = t('change_language', {}, 'Change language')\n\n return (\n <DropdownMenu open={open} onOpenChange={setOpen}>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"language-switcher-trigger\"\n aria-label={label}\n title={label}\n className={languageTriggerClasses}\n >\n <LanguagesIcon className=\"size-4\" aria-hidden=\"true\" />\n <span lang={localization.tag} dir={localization.direction} className=\"hidden sm:inline\">\n {localization.label}\n </span>\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-40\">\n <LocaleOptions />\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** The user's tenants on a page outside the panel (no current tenant). */\nfunction TenantChooser({ panel }: { panel: PanelSchema }) {\n const t = useT()\n const tenancy = panel.tenancy\n\n if (!tenancy || (tenancy.tenants.length === 0 && !tenancy.registrationUrl)) return null\n\n const label = t('starter_kit.choose_tenant', {}, 'Choose a tenant')\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <button\n type=\"button\"\n data-slot=\"tenant-menu\"\n className=\"hover:bg-accent data-[state=open]:bg-accent inline-flex h-9 max-w-60 shrink-0 items-center gap-2 rounded-md px-2 text-sm font-medium\"\n >\n <span className=\"sr-only lg:not-sr-only lg:truncate\">{label}</span>\n <Icon\n name=\"alfakit::panels.tenant-menu.toggle-button\"\n fallback={ChevronsUpDownIcon}\n className=\"text-muted-foreground size-4 shrink-0\"\n />\n </button>\n </DropdownMenuTrigger>\n <DropdownMenuContent className=\"min-w-56 rounded-lg\" side=\"bottom\" align=\"end\" sideOffset={4}>\n {tenancy.tenants.length > 0 && (\n <DropdownMenuLabel className=\"text-muted-foreground text-xs\">{label}</DropdownMenuLabel>\n )}\n {tenancy.tenants.map((tenant) => (\n <DropdownMenuItem key={String(tenant.id)} onSelect={() => router.visit(tenant.url)}>\n <Avatar className=\"size-6 rounded-sm\">\n {tenant.avatar && <AvatarImage src={tenant.avatar} alt=\"\" />}\n <AvatarFallback className=\"rounded-sm text-[10px]\">\n {initials(tenantNameText(tenant))}\n </AvatarFallback>\n </Avatar>\n <TenantName tenant={tenant} className=\"flex-1 truncate\" />\n </DropdownMenuItem>\n ))}\n {tenancy.registrationUrl && (\n <>\n {tenancy.tenants.length > 0 && <DropdownMenuSeparator />}\n <DropdownMenuItem onSelect={() => router.visit(tenancy.registrationUrl!)}>\n <Icon name=\"alfakit::panels.tenant-menu.registration-button\" fallback={PlusIcon} />\n {tenancy.registrationLabel ?? t('tenancy.register', {}, 'New')}\n </DropdownMenuItem>\n </>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alfakit/react",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "React + shadcn/ui renderer for Alfakit, the Laravel admin panel builder by Alfa Six.",
5
5
  "license": "MIT",
6
6
  "author": "Alfa Six (https://alfasixbh.com)",
@@ -96,7 +96,7 @@
96
96
  "react-day-picker": "^10.0.2",
97
97
  "sonner": "^2.0.8",
98
98
  "tailwind-merge": "^3.7.0",
99
- "@alfakit/core": "0.1.12"
99
+ "@alfakit/core": "0.1.13"
100
100
  },
101
101
  "peerDependencies": {
102
102
  "@inertiajs/react": "^3.0.0",
@@ -32,6 +32,7 @@ import { Direction } from 'radix-ui'
32
32
  import {
33
33
  StrictMode,
34
34
  createElement,
35
+ useEffect,
35
36
  useLayoutEffect,
36
37
  type ReactElement,
37
38
  type ReactNode,
@@ -51,6 +52,7 @@ import { discoverComponents } from './lib/discovery'
51
52
  import { preloadIcons } from './lib/icons'
52
53
  import { AlfakitPage } from './pages/page'
53
54
  import { builtInComponents, createDefaultRegistry } from './registry'
55
+ import { claimToaster } from './standalone/toaster-claims'
54
56
 
55
57
  /**
56
58
  * Rendered inside Inertia's page tree, so it follows the current page: the reading
@@ -91,6 +93,10 @@ export function AlfakitRoot({
91
93
  applyPanelTheme(panel)
92
94
  }, [panel])
93
95
 
96
+ // This root renders the toaster: starter kit header components and standalone components
97
+ // on the same page don't mount a second one.
98
+ useEffect(() => claimToaster(), [])
99
+
94
100
  return (
95
101
  <Direction.Provider dir={direction}>
96
102
  <TooltipProvider delayDuration={0}>
@@ -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
  )