@alfakit/react 0.1.11 → 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.
Files changed (64) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/entries/other-entries.d.ts.map +1 -1
  3. package/dist/components/entries/other-entries.js +1 -1
  4. package/dist/components/entries/other-entries.js.map +1 -1
  5. package/dist/components/fields/builder.d.ts.map +1 -1
  6. package/dist/components/fields/builder.js +3 -3
  7. package/dist/components/fields/builder.js.map +1 -1
  8. package/dist/components/fields/file-upload.d.ts.map +1 -1
  9. package/dist/components/fields/file-upload.js +2 -5
  10. package/dist/components/fields/file-upload.js.map +1 -1
  11. package/dist/components/fields/repeater.d.ts.map +1 -1
  12. package/dist/components/fields/repeater.js +3 -3
  13. package/dist/components/fields/repeater.js.map +1 -1
  14. package/dist/components/fields/rich-editor-extensions.js +5 -2
  15. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  16. package/dist/components/layout/wizard.d.ts.map +1 -1
  17. package/dist/components/layout/wizard.js +4 -1
  18. package/dist/components/layout/wizard.js.map +1 -1
  19. package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
  20. package/dist/components/notifications/flash-toasts.js +11 -1
  21. package/dist/components/notifications/flash-toasts.js.map +1 -1
  22. package/dist/components/notifications/notifications-menu.js +3 -3
  23. package/dist/components/notifications/notifications-menu.js.map +1 -1
  24. package/dist/components/panel/pwa.d.ts.map +1 -1
  25. package/dist/components/panel/pwa.js +8 -7
  26. package/dist/components/panel/pwa.js.map +1 -1
  27. package/dist/components/panel/shortcuts-overlay.js +1 -1
  28. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  29. package/dist/components/table/cells.js +1 -1
  30. package/dist/components/table/cells.js.map +1 -1
  31. package/dist/components.css +19 -0
  32. package/dist/create-alfakit.d.ts.map +1 -1
  33. package/dist/create-alfakit.js +3 -1
  34. package/dist/create-alfakit.js.map +1 -1
  35. package/dist/eject-manifest.json +2 -2
  36. package/dist/pages/page.js +1 -1
  37. package/dist/pages/page.js.map +1 -1
  38. package/dist/standalone/alfakit-component.js +2 -1
  39. package/dist/standalone/alfakit-component.js.map +1 -1
  40. package/dist/standalone/alfakit-toaster.js +2 -1
  41. package/dist/standalone/alfakit-toaster.js.map +1 -1
  42. package/dist/standalone/runtime.js +1 -11
  43. package/dist/standalone/runtime.js.map +1 -1
  44. package/dist/standalone/toaster-claims.js +16 -0
  45. package/dist/standalone/toaster-claims.js.map +1 -0
  46. package/dist/starter-kit/widgets-loaded.js +63 -6
  47. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  48. package/package.json +2 -2
  49. package/src/components/entries/other-entries.tsx +2 -0
  50. package/src/components/fields/builder.tsx +7 -2
  51. package/src/components/fields/file-upload.tsx +2 -7
  52. package/src/components/fields/repeater.tsx +6 -3
  53. package/src/components/fields/rich-editor-extensions.tsx +9 -5
  54. package/src/components/layout/wizard.tsx +5 -1
  55. package/src/components/notifications/flash-toasts.tsx +8 -0
  56. package/src/components/notifications/notifications-menu.tsx +3 -3
  57. package/src/components/panel/pwa.tsx +23 -9
  58. package/src/components/panel/shortcuts-overlay.tsx +1 -1
  59. package/src/components/table/cells.tsx +3 -1
  60. package/src/create-alfakit.tsx +6 -0
  61. package/src/pages/page.tsx +1 -1
  62. package/src/standalone/runtime.ts +2 -14
  63. package/src/standalone/toaster-claims.ts +16 -0
  64. package/src/starter-kit/widgets-loaded.tsx +84 -1
@@ -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.11",
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.11"
99
+ "@alfakit/core": "0.1.13"
100
100
  },
101
101
  "peerDependencies": {
102
102
  "@inertiajs/react": "^3.0.0",
@@ -115,6 +115,8 @@ export function ImageEntryComponent({ component }: { component: ImageEntrySchema
115
115
  data-slot="image-remaining"
116
116
  className={cn(
117
117
  'text-muted-foreground',
118
+ // The last image keeps the stack's negative end margin (W-S3-03).
119
+ component.stacked && usesDefaultImageStack(component) && 'ms-3',
118
120
  remainingTextSizeClass(component.limitedRemainingTextSize),
119
121
  )}
120
122
  >
@@ -17,6 +17,7 @@ import {
17
17
  insertBuilderItem,
18
18
  isDeferredPlaceholder,
19
19
  itemActionContext,
20
+ itemExtraActions,
20
21
  moveIndexedState,
21
22
  moveRepeaterItem,
22
23
  removeIndexedState,
@@ -316,7 +317,9 @@ function BuilderFieldContent({ component }: { component: BuilderSchema }) {
316
317
  index={index}
317
318
  count={items.length}
318
319
  resolve={(name) => resolve(name, serverItem)}
319
- extraActions={menu || locked ? undefined : serverItem?.actions}
320
+ extraActions={
321
+ menu || locked ? undefined : itemExtraActions(component, serverItem)
322
+ }
320
323
  buttons={buttons && !menu}
321
324
  canClone={canClone && !menu}
322
325
  canDelete={canDelete && !menu}
@@ -353,7 +356,9 @@ function BuilderFieldContent({ component }: { component: BuilderSchema }) {
353
356
  canMoveDown={buttons && index < items.length - 1}
354
357
  canClone={canClone}
355
358
  canDelete={canDelete}
356
- extraActions={locked ? undefined : serverItem?.actions}
359
+ extraActions={
360
+ locked ? undefined : itemExtraActions(component, serverItem)
361
+ }
357
362
  onAction={(name, onClient) =>
358
363
  runItemAction(
359
364
  resolve(name, serverItem),
@@ -9,6 +9,7 @@ import {
9
9
  fileManagerSize,
10
10
  filesFromClipboard,
11
11
  fileUploadAccept,
12
+ fileUploadEntries,
12
13
  fileUploadPaths,
13
14
  insertUploadedFile,
14
15
  moveRepeaterItem,
@@ -103,13 +104,7 @@ export function FileUploadField({ component }: { component: FileUploadSchema })
103
104
  }
104
105
 
105
106
  const paths = fileUploadPaths(value)
106
- const files: LocalFile[] = paths.map(
107
- (path) =>
108
- [...component.files, ...uploaded].find((file) => file.path === path) ?? {
109
- path,
110
- name: path.replace(/^tmp:/, ''),
111
- },
112
- )
107
+ const files: LocalFile[] = fileUploadEntries(paths, component.files, uploaded)
113
108
  const maxFiles = component.multiple ? (component.maxFiles ?? Infinity) : 1
114
109
  const canAddMore = files.length + uploading.length < maxFiles
115
110
 
@@ -1,6 +1,5 @@
1
1
  import type { RepeaterSchema, SchemaComponent } from '@alfakit/core'
2
2
  import {
3
- VIRTUAL_ESTIMATES,
4
3
  appendRepeaterItem,
5
4
  cloneIndexedState,
6
5
  cloneRepeaterItem,
@@ -8,12 +7,14 @@ import {
8
7
  fieldIds,
9
8
  gridColumnsClasses,
10
9
  isDeferredPlaceholder,
10
+ itemExtraActions,
11
11
  moveIndexedState,
12
12
  moveRepeaterItem,
13
13
  removeIndexedState,
14
14
  removeRepeaterItem,
15
15
  reorderedItemKeys,
16
16
  templateForIndex,
17
+ VIRTUAL_ESTIMATES,
17
18
  virtualizeOptions,
18
19
  } from '@alfakit/core'
19
20
  import { useState } from 'react'
@@ -165,7 +166,7 @@ function RepeaterFieldContent({ component }: { component: RepeaterSchema }) {
165
166
  virtual={virtual}
166
167
  context={context}
167
168
  resolve={(name, index) => resolve(name, component.items[index])}
168
- extraActionsFor={(index) => component.items[index]?.actions}
169
+ extraActionsFor={(index) => itemExtraActions(component, component.items[index])}
169
170
  />
170
171
  ) : (
171
172
  <>
@@ -245,7 +246,9 @@ function RepeaterFieldContent({ component }: { component: RepeaterSchema }) {
245
246
  index={index}
246
247
  count={items.length}
247
248
  resolve={(name) => resolve(name, serverItem)}
248
- extraActions={locked ? undefined : serverItem?.actions}
249
+ extraActions={
250
+ locked ? undefined : itemExtraActions(component, serverItem)
251
+ }
249
252
  buttons={buttons}
250
253
  canClone={Boolean(component.cloneable) && canAdd}
251
254
  canDelete={canDelete}
@@ -152,11 +152,15 @@ export function useRichEditorExtensions(
152
152
 
153
153
  if (!result || !editor || editor.isDestroyed) return
154
154
 
155
- editor
156
- .chain()
157
- .focus()
158
- .insertContent({ type: 'customBlock', attrs: { id: result.id, config: result.config } })
159
- .run()
155
+ const block = { type: 'customBlock', attrs: { id: result.id, config: result.config } }
156
+ // A block inserted just before is still selected (a node selection): insert after it
157
+ // instead of replacing it (W-S2-05).
158
+ const { selection } = editor.state
159
+ const chain = editor.chain().focus()
160
+ ;('node' in selection && selection.node
161
+ ? chain.insertContentAt(selection.to, block)
162
+ : chain.insertContent(block)
163
+ ).run()
160
164
  }
161
165
 
162
166
  return { extensions, suggestions, insertBlock, context }
@@ -21,6 +21,7 @@ import { cn } from '../../lib/utils'
21
21
  import { ActionButton } from '../actions/action-button'
22
22
  import { usePageActionContext } from '../actions/action-context'
23
23
  import { useOptionalFormStore } from '../form/form-context'
24
+ import { useFieldActionContext } from '../schema/field-actions'
24
25
  import { SchemaRenderer } from '../schema/schema-renderer'
25
26
  import { Button } from '../ui/button'
26
27
  import { DeferredSchema } from './deferred'
@@ -52,6 +53,9 @@ export function WizardComponent({ component }: { component: WizardSchema }) {
52
53
  if (stepQueryKey) persistWizardStep(current, stepQueryKey)
53
54
  }, [current, stepQueryKey])
54
55
  const store = useOptionalFormStore()
56
+ // Inside a form the submit action runs as a field action, so `$get` / `$set` work (W-S2-09).
57
+ const fieldContext = useFieldActionContext(component)
58
+ const submitContext = store ? fieldContext : actionContext
55
59
 
56
60
  // An action modal around this wizard shows its submit button on the last step only.
57
61
  useEffect(() => {
@@ -229,7 +233,7 @@ export function WizardComponent({ component }: { component: WizardSchema }) {
229
233
  </Button>
230
234
  )
231
235
  ) : component.submitAction ? (
232
- <ActionButton action={component.submitAction} context={actionContext} />
236
+ <ActionButton action={component.submitAction} context={submitContext} />
233
237
  ) : component.submitActionHtml ? (
234
238
  <div
235
239
  data-slot="wizard-submit"
@@ -43,6 +43,14 @@ export function setToasterId(id: string | undefined): void {
43
43
  toasterId = id
44
44
  }
45
45
 
46
+ /**
47
+ * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and
48
+ * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).
49
+ */
50
+ export function toastOptions<T extends object>(options?: T): T & { toasterId?: string } {
51
+ return { ...(options ?? ({} as T)), toasterId }
52
+ }
53
+
46
54
  /** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */
47
55
  export function listenForClosedNotifications(): void {
48
56
  if (closeListening || typeof window === 'undefined') return
@@ -19,7 +19,7 @@ import { Button } from '../ui/button'
19
19
  import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'
20
20
  import { ScrollArea } from '../ui/scroll-area'
21
21
  import { Spinner } from '../ui/spinner'
22
- import { NotificationActionButton } from './flash-toasts'
22
+ import { NotificationActionButton, toastOptions } from './flash-toasts'
23
23
 
24
24
  export interface NotificationsMenuProps {
25
25
  /**
@@ -104,8 +104,8 @@ export function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps
104
104
  const { message } = await togglePushNotifications()
105
105
  const text = t(message.key, {}, message.fallback)
106
106
 
107
- if (message.error) toast.error(text)
108
- else toast.success(text)
107
+ if (message.error) toast.error(text, toastOptions())
108
+ else toast.success(text, toastOptions())
109
109
  }
110
110
 
111
111
  const pushLabel =