@alfakit/react 0.1.11 → 0.1.12

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 (47) hide show
  1. package/CHANGELOG.md +6 -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/eject-manifest.json +2 -2
  33. package/dist/pages/page.js +1 -1
  34. package/dist/pages/page.js.map +1 -1
  35. package/package.json +2 -2
  36. package/src/components/entries/other-entries.tsx +2 -0
  37. package/src/components/fields/builder.tsx +7 -2
  38. package/src/components/fields/file-upload.tsx +2 -7
  39. package/src/components/fields/repeater.tsx +6 -3
  40. package/src/components/fields/rich-editor-extensions.tsx +9 -5
  41. package/src/components/layout/wizard.tsx +5 -1
  42. package/src/components/notifications/flash-toasts.tsx +8 -0
  43. package/src/components/notifications/notifications-menu.tsx +3 -3
  44. package/src/components/panel/pwa.tsx +23 -9
  45. package/src/components/panel/shortcuts-overlay.tsx +1 -1
  46. package/src/components/table/cells.tsx +3 -1
  47. package/src/pages/page.tsx +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"repeater.js","names":[],"sources":["../../../src/components/fields/repeater.tsx"],"sourcesContent":["import type { RepeaterSchema, SchemaComponent } from '@alfakit/core'\nimport {\n VIRTUAL_ESTIMATES,\n appendRepeaterItem,\n cloneIndexedState,\n cloneRepeaterItem,\n columnSpanClasses,\n fieldIds,\n gridColumnsClasses,\n isDeferredPlaceholder,\n moveIndexedState,\n moveRepeaterItem,\n removeIndexedState,\n removeRepeaterItem,\n reorderedItemKeys,\n templateForIndex,\n virtualizeOptions,\n} from '@alfakit/core'\nimport { useState } from 'react'\nimport { useFormatter } from '../../context'\nimport { useScrollToErroredItem, useVirtualList } from '../../hooks/use-virtual-list'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { DeferredSchema } from '../layout/deferred'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Collapsible, CollapsibleContent } from '../ui/collapsible'\nimport {\n AddActionRow,\n CollapseAllActions,\n ItemActionButton,\n ItemHeaderActions,\n useItemActionFallbacks,\n useItemActions,\n useItemMoves,\n useRunItemAction,\n} from './item-actions'\nimport { RepeaterTable } from './repeater-table'\n\n/**\n * Items use the per-index schema from the server when available (so closures see item\n * state) and fall back to the template for freshly added items.\n */\n/**\n * `deferLoading()`: a skeleton until the field scrolls into view, then the field with its\n * items (loaded from the server).\n */\nexport function RepeaterField({ component }: { component: RepeaterSchema }) {\n if (!isDeferredPlaceholder(component)) return <RepeaterFieldContent component={component} />\n\n return (\n <DeferredSchema component={component}>\n {(loaded) => <RepeaterFieldContent component={loaded} />}\n </DeferredSchema>\n )\n}\n\nfunction RepeaterFieldContent({ component }: { component: RepeaterSchema }) {\n const formatter = useFormatter()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<unknown[] | null>(\n component,\n )\n const context = useFieldActionContext(component)\n const runItemAction = useRunItemAction(context)\n const fallbacks = useItemActionFallbacks(component.addActionLabel)\n const resolve = useItemActions(component, fallbacks)\n // Read-only shows the items but hides add, delete, clone and reorder controls.\n const locked = disabled || readOnly\n const items = value ?? []\n const [collapsed, setCollapsed] = useState<Record<number, boolean>>({})\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n // Long lists render only the visible items (plus a few around them); a multi-column\n // `grid()` is windowed by whole rows, counting its columns from the rendered grid.\n const virtual = useVirtualList(\n items.length,\n virtualizeOptions(\n component.virtualize,\n VIRTUAL_ESTIMATES.item,\n 12,\n component.grid ? 'auto' : 1,\n ),\n )\n const { range, indexes, style, attach, item } = virtual\n useScrollToErroredItem(virtual.list, range.active, component.statePath)\n\n // Collapsed flags are keyed by index, so they move with their item.\n const moveItem = (from: number, to: number) => {\n setValue(moveRepeaterItem(items, from, to))\n setCollapsed((state) => moveIndexedState(state, from, to))\n }\n const removeItem = (index: number) => {\n setValue(removeRepeaterItem(items, index))\n setCollapsed((state) => removeIndexedState(state, index))\n }\n const cloneItem = (index: number) => {\n setValue(cloneRepeaterItem(items, index))\n setCollapsed((state) => cloneIndexedState(state, index))\n }\n // Move buttons: announced, one at a time, focus kept on the moved item.\n const {\n moveWithButton,\n region: moveRegion,\n containerRef: moveContainerRef,\n } = useItemMoves(value, moveItem, (index) => component.items[index]?.label, items.length)\n // A drop runs the reorder action: in the browser, or on the server when customised.\n const dropItem = (from: number, to: number) =>\n runItemAction(resolve('reorder'), { items: reorderedItemKeys(items.length, from, to) }, () =>\n moveItem(from, to),\n )\n const setAllCollapsed = (value: boolean) =>\n setCollapsed(Object.fromEntries(items.map((_, index) => [index, value])))\n const addItem = () =>\n setValue(\n appendRepeaterItem(\n items,\n component.simple\n ? ({ [Object.keys(component.templateState)[0] ?? 'value']: null } as Record<\n string,\n unknown\n >)\n : component.templateState,\n ),\n )\n\n const schemaFor = (index: number): SchemaComponent[] =>\n component.items[index]?.schema ?? templateForIndex(component.template, index)\n\n const canAdd =\n component.addable &&\n !locked &&\n (component.maxItems == null || items.length < component.maxItems)\n const canDelete =\n component.deletable &&\n !locked &&\n (component.minItems == null || items.length > component.minItems)\n const draggable =\n component.reorderable &&\n component.reorderableWithDragAndDrop !== false &&\n !locked &&\n resolve('reorder') !== null\n const buttons = component.reorderable && component.reorderableWithButtons !== false && !locked\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div ref={moveContainerRef} className=\"flex flex-col gap-3\">\n {moveRegion}\n {component.table?.length && !component.simple ? (\n <RepeaterTable\n id={id}\n columns={component.table}\n compact={component.compact}\n count={items.length}\n prefixFor={(index) => `${component.statePath}.${index}.`}\n schemaFor={schemaFor}\n labelledBy={component.label ? fieldIds(id).label : undefined}\n describedBy={aria['aria-describedby']}\n reorderable={buttons}\n cloneable={Boolean(component.cloneable) && canAdd}\n deletable={canDelete}\n onMove={moveItem}\n onClone={cloneItem}\n onRemove={removeItem}\n virtual={virtual}\n context={context}\n resolve={(name, index) => resolve(name, component.items[index])}\n extraActionsFor={(index) => component.items[index]?.actions}\n />\n ) : (\n <>\n {component.collapsible && !component.simple && items.length > 0 && (\n <CollapseAllActions\n context={context}\n resolve={(name) => resolve(name)}\n onCollapseAll={() => setAllCollapsed(true)}\n onExpandAll={() => setAllCollapsed(false)}\n />\n )}\n <ul\n id={id}\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n ref={attach}\n style={style}\n className={cn('grid gap-3', component.grid && gridColumnsClasses(component.grid))}\n >\n {indexes.map((index) => {\n const isCollapsed = collapsed[index] ?? component.collapsed ?? false\n const label = component.items[index]?.label ?? null\n const serverItem = component.items[index]\n\n return (\n <li\n key={index}\n ref={item}\n data-index={index}\n aria-setsize={range.active ? items.length : undefined}\n aria-posinset={range.active ? index + 1 : undefined}\n draggable={draggable}\n onDragStart={() => setDragIndex(index)}\n onDragOver={(event) => draggable && event.preventDefault()}\n onDrop={() => {\n if (dragIndex !== null && dragIndex !== index) dropItem(dragIndex, index)\n setDragIndex(null)\n }}\n onDragEnd={() => setDragIndex(null)}\n className={cn(\n 'bg-card min-w-0 rounded-lg border shadow-xs',\n dragIndex === index && 'opacity-50',\n component.simple && 'border-0 shadow-none',\n )}\n >\n <Collapsible open={!isCollapsed}>\n {!component.simple && (\n <div\n className=\"flex flex-wrap items-center gap-1 border-b px-2 py-1.5\"\n data-slot=\"item-header\"\n >\n {draggable && (\n <span\n className=\"text-muted-foreground cursor-grab\"\n title={resolve('reorder', serverItem)?.label ?? undefined}\n aria-hidden=\"true\"\n >\n <Icon\n name={resolve('reorder', serverItem)?.icon ?? 'grip-vertical'}\n />\n </span>\n )}\n {component.itemNumbers && (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n {formatter.number(index + 1)}\n </span>\n )}\n {label && (\n <span dir=\"auto\" className=\"min-w-0 truncate text-sm font-medium\">\n {label}\n </span>\n )}\n <span className=\"flex-1\" />\n <ItemHeaderActions\n context={context}\n itemKey={String(index)}\n index={index}\n count={items.length}\n resolve={(name) => resolve(name, serverItem)}\n extraActions={locked ? undefined : serverItem?.actions}\n buttons={buttons}\n canClone={Boolean(component.cloneable) && canAdd}\n canDelete={canDelete}\n collapsible={Boolean(component.collapsible)}\n collapsed={isCollapsed}\n onMove={moveWithButton}\n onClone={() => cloneItem(index)}\n onDelete={() => removeItem(index)}\n onToggle={() =>\n setCollapsed((state) => ({ ...state, [index]: !isCollapsed }))\n }\n />\n </div>\n )}\n <CollapsibleContent>\n <div className={cn('flex gap-2', component.simple ? 'items-start' : 'p-4')}>\n <div className=\"flex-1\">\n <SchemaRenderer\n components={schemaFor(index)}\n columns={\n component.simple\n ? { default: 1 }\n : (component.columns ?? { default: 1, lg: 2 })\n }\n className={cn(\n component.simple && 'gap-0',\n '[&_[data-slot=field]_[data-slot=label]]:hidden',\n !component.simple && '[&_[data-slot=field]_[data-slot=label]]:flex',\n )}\n />\n </div>\n {component.simple && canDelete && (\n <ItemActionButton\n action={resolve('delete', serverItem)}\n context={context}\n args={{ item: String(index) }}\n size=\"default\"\n className=\"text-muted-foreground\"\n onClient={() => removeItem(index)}\n />\n )}\n </div>\n </CollapsibleContent>\n </Collapsible>\n </li>\n )\n })}\n </ul>\n </>\n )}\n {canAdd && (\n <AddActionRow alignment={component.addActionAlignment}>\n <ItemActionButton\n action={resolve('add')}\n context={context}\n size=\"sm\"\n onClient={addItem}\n />\n </AddActionRow>\n )}\n </div>\n </FieldWrapper>\n )\n}\n\nexport function columnSpanOf(component: SchemaComponent): string {\n return columnSpanClasses(component.columnSpan)\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"repeater.js","names":[],"sources":["../../../src/components/fields/repeater.tsx"],"sourcesContent":["import type { RepeaterSchema, SchemaComponent } from '@alfakit/core'\nimport {\n appendRepeaterItem,\n cloneIndexedState,\n cloneRepeaterItem,\n columnSpanClasses,\n fieldIds,\n gridColumnsClasses,\n isDeferredPlaceholder,\n itemExtraActions,\n moveIndexedState,\n moveRepeaterItem,\n removeIndexedState,\n removeRepeaterItem,\n reorderedItemKeys,\n templateForIndex,\n VIRTUAL_ESTIMATES,\n virtualizeOptions,\n} from '@alfakit/core'\nimport { useState } from 'react'\nimport { useFormatter } from '../../context'\nimport { useScrollToErroredItem, useVirtualList } from '../../hooks/use-virtual-list'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { DeferredSchema } from '../layout/deferred'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Collapsible, CollapsibleContent } from '../ui/collapsible'\nimport {\n AddActionRow,\n CollapseAllActions,\n ItemActionButton,\n ItemHeaderActions,\n useItemActionFallbacks,\n useItemActions,\n useItemMoves,\n useRunItemAction,\n} from './item-actions'\nimport { RepeaterTable } from './repeater-table'\n\n/**\n * Items use the per-index schema from the server when available (so closures see item\n * state) and fall back to the template for freshly added items.\n */\n/**\n * `deferLoading()`: a skeleton until the field scrolls into view, then the field with its\n * items (loaded from the server).\n */\nexport function RepeaterField({ component }: { component: RepeaterSchema }) {\n if (!isDeferredPlaceholder(component)) return <RepeaterFieldContent component={component} />\n\n return (\n <DeferredSchema component={component}>\n {(loaded) => <RepeaterFieldContent component={loaded} />}\n </DeferredSchema>\n )\n}\n\nfunction RepeaterFieldContent({ component }: { component: RepeaterSchema }) {\n const formatter = useFormatter()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<unknown[] | null>(\n component,\n )\n const context = useFieldActionContext(component)\n const runItemAction = useRunItemAction(context)\n const fallbacks = useItemActionFallbacks(component.addActionLabel)\n const resolve = useItemActions(component, fallbacks)\n // Read-only shows the items but hides add, delete, clone and reorder controls.\n const locked = disabled || readOnly\n const items = value ?? []\n const [collapsed, setCollapsed] = useState<Record<number, boolean>>({})\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n // Long lists render only the visible items (plus a few around them); a multi-column\n // `grid()` is windowed by whole rows, counting its columns from the rendered grid.\n const virtual = useVirtualList(\n items.length,\n virtualizeOptions(\n component.virtualize,\n VIRTUAL_ESTIMATES.item,\n 12,\n component.grid ? 'auto' : 1,\n ),\n )\n const { range, indexes, style, attach, item } = virtual\n useScrollToErroredItem(virtual.list, range.active, component.statePath)\n\n // Collapsed flags are keyed by index, so they move with their item.\n const moveItem = (from: number, to: number) => {\n setValue(moveRepeaterItem(items, from, to))\n setCollapsed((state) => moveIndexedState(state, from, to))\n }\n const removeItem = (index: number) => {\n setValue(removeRepeaterItem(items, index))\n setCollapsed((state) => removeIndexedState(state, index))\n }\n const cloneItem = (index: number) => {\n setValue(cloneRepeaterItem(items, index))\n setCollapsed((state) => cloneIndexedState(state, index))\n }\n // Move buttons: announced, one at a time, focus kept on the moved item.\n const {\n moveWithButton,\n region: moveRegion,\n containerRef: moveContainerRef,\n } = useItemMoves(value, moveItem, (index) => component.items[index]?.label, items.length)\n // A drop runs the reorder action: in the browser, or on the server when customised.\n const dropItem = (from: number, to: number) =>\n runItemAction(resolve('reorder'), { items: reorderedItemKeys(items.length, from, to) }, () =>\n moveItem(from, to),\n )\n const setAllCollapsed = (value: boolean) =>\n setCollapsed(Object.fromEntries(items.map((_, index) => [index, value])))\n const addItem = () =>\n setValue(\n appendRepeaterItem(\n items,\n component.simple\n ? ({ [Object.keys(component.templateState)[0] ?? 'value']: null } as Record<\n string,\n unknown\n >)\n : component.templateState,\n ),\n )\n\n const schemaFor = (index: number): SchemaComponent[] =>\n component.items[index]?.schema ?? templateForIndex(component.template, index)\n\n const canAdd =\n component.addable &&\n !locked &&\n (component.maxItems == null || items.length < component.maxItems)\n const canDelete =\n component.deletable &&\n !locked &&\n (component.minItems == null || items.length > component.minItems)\n const draggable =\n component.reorderable &&\n component.reorderableWithDragAndDrop !== false &&\n !locked &&\n resolve('reorder') !== null\n const buttons = component.reorderable && component.reorderableWithButtons !== false && !locked\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div ref={moveContainerRef} className=\"flex flex-col gap-3\">\n {moveRegion}\n {component.table?.length && !component.simple ? (\n <RepeaterTable\n id={id}\n columns={component.table}\n compact={component.compact}\n count={items.length}\n prefixFor={(index) => `${component.statePath}.${index}.`}\n schemaFor={schemaFor}\n labelledBy={component.label ? fieldIds(id).label : undefined}\n describedBy={aria['aria-describedby']}\n reorderable={buttons}\n cloneable={Boolean(component.cloneable) && canAdd}\n deletable={canDelete}\n onMove={moveItem}\n onClone={cloneItem}\n onRemove={removeItem}\n virtual={virtual}\n context={context}\n resolve={(name, index) => resolve(name, component.items[index])}\n extraActionsFor={(index) => itemExtraActions(component, component.items[index])}\n />\n ) : (\n <>\n {component.collapsible && !component.simple && items.length > 0 && (\n <CollapseAllActions\n context={context}\n resolve={(name) => resolve(name)}\n onCollapseAll={() => setAllCollapsed(true)}\n onExpandAll={() => setAllCollapsed(false)}\n />\n )}\n <ul\n id={id}\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n ref={attach}\n style={style}\n className={cn('grid gap-3', component.grid && gridColumnsClasses(component.grid))}\n >\n {indexes.map((index) => {\n const isCollapsed = collapsed[index] ?? component.collapsed ?? false\n const label = component.items[index]?.label ?? null\n const serverItem = component.items[index]\n\n return (\n <li\n key={index}\n ref={item}\n data-index={index}\n aria-setsize={range.active ? items.length : undefined}\n aria-posinset={range.active ? index + 1 : undefined}\n draggable={draggable}\n onDragStart={() => setDragIndex(index)}\n onDragOver={(event) => draggable && event.preventDefault()}\n onDrop={() => {\n if (dragIndex !== null && dragIndex !== index) dropItem(dragIndex, index)\n setDragIndex(null)\n }}\n onDragEnd={() => setDragIndex(null)}\n className={cn(\n 'bg-card min-w-0 rounded-lg border shadow-xs',\n dragIndex === index && 'opacity-50',\n component.simple && 'border-0 shadow-none',\n )}\n >\n <Collapsible open={!isCollapsed}>\n {!component.simple && (\n <div\n className=\"flex flex-wrap items-center gap-1 border-b px-2 py-1.5\"\n data-slot=\"item-header\"\n >\n {draggable && (\n <span\n className=\"text-muted-foreground cursor-grab\"\n title={resolve('reorder', serverItem)?.label ?? undefined}\n aria-hidden=\"true\"\n >\n <Icon\n name={resolve('reorder', serverItem)?.icon ?? 'grip-vertical'}\n />\n </span>\n )}\n {component.itemNumbers && (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n {formatter.number(index + 1)}\n </span>\n )}\n {label && (\n <span dir=\"auto\" className=\"min-w-0 truncate text-sm font-medium\">\n {label}\n </span>\n )}\n <span className=\"flex-1\" />\n <ItemHeaderActions\n context={context}\n itemKey={String(index)}\n index={index}\n count={items.length}\n resolve={(name) => resolve(name, serverItem)}\n extraActions={\n locked ? undefined : itemExtraActions(component, serverItem)\n }\n buttons={buttons}\n canClone={Boolean(component.cloneable) && canAdd}\n canDelete={canDelete}\n collapsible={Boolean(component.collapsible)}\n collapsed={isCollapsed}\n onMove={moveWithButton}\n onClone={() => cloneItem(index)}\n onDelete={() => removeItem(index)}\n onToggle={() =>\n setCollapsed((state) => ({ ...state, [index]: !isCollapsed }))\n }\n />\n </div>\n )}\n <CollapsibleContent>\n <div className={cn('flex gap-2', component.simple ? 'items-start' : 'p-4')}>\n <div className=\"flex-1\">\n <SchemaRenderer\n components={schemaFor(index)}\n columns={\n component.simple\n ? { default: 1 }\n : (component.columns ?? { default: 1, lg: 2 })\n }\n className={cn(\n component.simple && 'gap-0',\n '[&_[data-slot=field]_[data-slot=label]]:hidden',\n !component.simple && '[&_[data-slot=field]_[data-slot=label]]:flex',\n )}\n />\n </div>\n {component.simple && canDelete && (\n <ItemActionButton\n action={resolve('delete', serverItem)}\n context={context}\n args={{ item: String(index) }}\n size=\"default\"\n className=\"text-muted-foreground\"\n onClient={() => removeItem(index)}\n />\n )}\n </div>\n </CollapsibleContent>\n </Collapsible>\n </li>\n )\n })}\n </ul>\n </>\n )}\n {canAdd && (\n <AddActionRow alignment={component.addActionAlignment}>\n <ItemActionButton\n action={resolve('add')}\n context={context}\n size=\"sm\"\n onClient={addItem}\n />\n </AddActionRow>\n )}\n </div>\n </FieldWrapper>\n )\n}\n\nexport function columnSpanOf(component: SchemaComponent): string {\n return columnSpanClasses(component.columnSpan)\n}\n"],"mappings":"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"}
@@ -89,13 +89,16 @@ function useRichEditorExtensions(component, editorRef) {
89
89
  const result = await runBlock(id);
90
90
  const editor = editorRef.current;
91
91
  if (!result || !editor || editor.isDestroyed) return;
92
- editor.chain().focus().insertContent({
92
+ const block = {
93
93
  type: "customBlock",
94
94
  attrs: {
95
95
  id: result.id,
96
96
  config: result.config
97
97
  }
98
- }).run();
98
+ };
99
+ const { selection } = editor.state;
100
+ const chain = editor.chain().focus();
101
+ ("node" in selection && selection.node ? chain.insertContentAt(selection.to, block) : chain.insertContent(block)).run();
99
102
  };
100
103
  return {
101
104
  extensions,
@@ -1 +1 @@
1
- {"version":3,"file":"rich-editor-extensions.js","names":[],"sources":["../../../src/components/fields/rich-editor-extensions.tsx"],"sourcesContent":["import type {\n ActionContext,\n RichEditorPanel,\n RichEditorSchema,\n RichEditorToolbarGroupSchema,\n SuggestionState,\n ToolbarCommand,\n} from '@alfakit/core'\nimport {\n commandLabel,\n customBlockNodeConfig,\n EditorSuggestions,\n fieldActionContext,\n fixedContainingBlockOffset,\n groupCustomBlocks,\n groupTriggerButton,\n isCustomBlockResult,\n mentionNodeConfig,\n mergeTagNodeConfig,\n mergeTagText,\n RichEditorRemote,\n} from '@alfakit/core'\nimport { Node, type Editor, type Extensions } from '@tiptap/react'\nimport { BlocksIcon, BracesIcon, ChevronDownIcon, XIcon } from 'lucide-react'\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type MutableRefObject,\n type ReactNode,\n} from 'react'\nimport { useActionRunner, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { Button } from '../ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { Input } from '../ui/input'\n\ntype NodeConfig = Parameters<typeof Node.create>[0]\n\nexport interface RichEditorExtensions {\n extensions: Extensions\n suggestions: EditorSuggestions\n /** Insert a custom block (opening its modal first when it has a form). */\n insertBlock: (id: string) => Promise<void>\n context: ActionContext\n}\n\n/**\n * Mentions, merge tags and custom blocks for a RichEditor. `editorRef` points at the live\n * editor, which the custom block node views and suggestions act on.\n */\nexport function useRichEditorExtensions(\n component: RichEditorSchema,\n editorRef: MutableRefObject<Editor | null>,\n): RichEditorExtensions {\n const t = useT()\n const form = useOptionalFormStore()\n const page = usePageActionContext()\n const runner = useActionRunner()\n const context = useMemo(\n () => fieldActionContext({ token: page.token, form, key: component.key }),\n [page.token, form, component.key],\n )\n const contextRef = useRef(context)\n const componentRef = useRef(component)\n\n const [remote] = useState(() => new RichEditorRemote(form, component.statePath))\n const [suggestions] = useState(\n () =>\n new EditorSuggestions({\n mentions: component.mentions ?? [],\n mergeTags: component.mergeTags ?? [],\n search: (trigger, query) => remote.searchMentions(trigger, query),\n }),\n )\n\n useEffect(() => () => suggestions.destroy(), [suggestions])\n\n const runBlock = async (id: string, config?: Record<string, unknown>) => {\n const block = componentRef.current.customBlocks?.find((candidate) => candidate.id === id)\n\n if (!block) return null\n\n const result = await runner.runField(block.action, {\n ...contextRef.current,\n arguments: config ? { config, editing: true } : {},\n })\n\n if (!isCustomBlockResult(result)) return null\n\n if (result.preview !== undefined) {\n remote.primePreview(result.id, result.config, result.preview, result.prose === true)\n }\n\n return result\n }\n\n const runBlockRef = useRef(runBlock)\n\n useEffect(() => {\n contextRef.current = context\n componentRef.current = component\n runBlockRef.current = runBlock\n })\n\n // The editor is created once; the node views read the latest props through refs when\n // they run (on edit, on preview), never during render.\n // eslint-disable-next-line react-hooks/refs\n const [extensions] = useState<Extensions>(() => [\n Node.create(mentionNodeConfig() as NodeConfig),\n Node.create(mergeTagNodeConfig() as NodeConfig),\n Node.create(\n customBlockNodeConfig({\n get blocks() {\n return componentRef.current.customBlocks ?? []\n },\n loadPreview: (id, config) => remote.loadPreview(id, config),\n onEdit: ({ id, config, pos }) => {\n void runBlockRef.current(id, config).then((result) => {\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor.view.dispatch(\n editor.state.tr.setNodeMarkup(pos, undefined, { id, config: result.config }),\n )\n })\n },\n labels: {\n edit: t('editor.edit_block', {}, 'Edit block'),\n remove: t('editor.remove_block', {}, 'Remove block'),\n },\n minimal: Boolean(component.minimalCustomBlockControls),\n }) as NodeConfig,\n ),\n ])\n\n const insertBlock = async (id: string) => {\n const result = await runBlock(id)\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor\n .chain()\n .focus()\n .insertContent({ type: 'customBlock', attrs: { id: result.id, config: result.config } })\n .run()\n }\n\n return { extensions, suggestions, insertBlock, context }\n}\n\n/** The toolbar button that opens the custom blocks or merge tags panel. */\nexport function ExtensionToolbarButton({\n name,\n component,\n editable,\n open,\n onToggle,\n}: {\n name: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n open: boolean\n onToggle: () => void\n}) {\n const t = useT()\n const count =\n name === 'customBlocks'\n ? (component.customBlocks ?? []).length\n : (component.mergeTags ?? []).length\n\n if (count === 0) return null\n\n const label =\n name === 'customBlocks'\n ? t('editor.custom_blocks', {}, 'Insert block')\n : t('editor.merge_tags', {}, 'Insert merge tag')\n\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n aria-pressed={open}\n aria-expanded={open}\n disabled={!editable}\n className={cn(open && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onToggle}\n >\n {name === 'customBlocks' ? <BlocksIcon /> : <BracesIcon />}\n </Button>\n )\n}\n\n/**\n * The custom blocks or merge tags panel: beside the content on wide editors, above it on\n * narrow ones. Blocks are grouped, optionally searchable and shown as a grid.\n */\nexport function RichEditorSidePanel({\n panel,\n component,\n editable,\n sticky,\n onInsertBlock,\n onInsertMergeTag,\n onClose,\n}: {\n panel: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n /** `top` of a sticky panel, or null. */\n sticky: string | null\n onInsertBlock: (id: string) => void\n onInsertMergeTag: (id: string) => void\n onClose: () => void\n}) {\n const t = useT()\n const [search, setSearch] = useState('')\n const heading =\n panel === 'customBlocks'\n ? t('editor.custom_blocks_heading', {}, 'Custom blocks')\n : t('editor.merge_tags_heading', {}, 'Merge tags')\n const groups =\n panel === 'customBlocks' ? groupCustomBlocks(component.customBlocks ?? [], search) : []\n const grid = Boolean(component.customBlocksGrid)\n const itemClass =\n 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50'\n\n return (\n <aside\n aria-label={heading}\n data-slot=\"rich-editor-panel\"\n data-panel={panel}\n className=\"bg-muted/20 shrink-0 border-b @2xl:w-64 @2xl:border-s @2xl:border-b-0\"\n >\n <div\n className={cn('flex flex-col gap-2 p-2', sticky !== null && '@2xl:sticky')}\n // The offset is a runtime value: no Tailwind class can hold it.\n style={sticky !== null ? { top: sticky } : undefined}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-muted-foreground text-xs font-medium\">{heading}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={t('editor.close_panel', {}, 'Close panel')}\n title={t('editor.close_panel', {}, 'Close panel')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClose}\n >\n <XIcon />\n </Button>\n </div>\n {panel === 'customBlocks' && component.searchableCustomBlocks && (\n <Input\n type=\"search\"\n value={search}\n aria-label={t('editor.search_blocks', {}, 'Search blocks')}\n placeholder={t('editor.search_blocks', {}, 'Search blocks')}\n className=\"h-8\"\n onChange={(event) => setSearch(event.target.value)}\n />\n )}\n {panel === 'customBlocks' ? (\n groups.length === 0 ? (\n <p className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </p>\n ) : (\n groups.map(({ group, blocks }) => (\n <div key={group ?? ''} role=\"group\" aria-label={group ?? heading}>\n {group && (\n <div className=\"text-muted-foreground px-2 pt-1 pb-0.5 text-xs font-medium\">\n {group}\n </div>\n )}\n <div className={cn(grid ? 'grid grid-cols-2 gap-1' : 'flex flex-col gap-0.5')}>\n {blocks.map((block) => (\n <button\n key={block.id}\n type=\"button\"\n disabled={!editable}\n className={cn(\n itemClass,\n grid && 'flex-col justify-center border py-3 text-center',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertBlock(block.id)}\n >\n {block.icon ? (\n <Icon name={block.icon} className=\"size-4 shrink-0\" />\n ) : (\n <BlocksIcon className=\"size-4 shrink-0\" />\n )}\n <span className=\"truncate\">{block.label}</span>\n </button>\n ))}\n </div>\n </div>\n ))\n )\n ) : (\n <div className=\"flex flex-col gap-0.5\">\n {(component.mergeTags ?? []).map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n disabled={!editable}\n className={itemClass}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertMergeTag(tag.id)}\n >\n <span className=\"truncate\">{tag.label}</span>\n <span className=\"text-muted-foreground ms-auto font-mono text-xs\" dir=\"ltr\">\n {mergeTagText({ id: tag.id })}\n </span>\n </button>\n ))}\n </div>\n )}\n </div>\n </aside>\n )\n}\n\n/** A `ToolbarButtonGroup`: a dropdown of buttons (icons, or labels with `textualButtons()`). */\nexport function ToolbarGroupButton({\n group,\n editor,\n editable,\n commands,\n icons,\n translate,\n onRun,\n}: {\n group: RichEditorToolbarGroupSchema\n editor: Editor\n editable: boolean\n commands: Record<string, ToolbarCommand>\n icons: (name: string) => ReactNode\n translate: (key: string, fallback: string) => string\n onRun: (name: string) => void\n}) {\n const buttons = group.buttons.filter((name) => commands[name])\n // After a pick, typing goes on in the editor (the command focused it); the menu would\n // otherwise return focus to its trigger. Closing without a pick (Escape) still does.\n const picked = useRef(false)\n\n if (buttons.length === 0) return null\n\n const trigger = groupTriggerButton(buttons, editor, commands)\n const active = buttons.some((name) => commands[name]?.active?.(editor))\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={group.label}\n title={group.label}\n disabled={!editable}\n className={cn('h-8 gap-0.5 px-1.5', active && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n >\n {group.icon ? <Icon name={group.icon} /> : trigger ? icons(trigger) : group.label}\n <ChevronDownIcon className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"start\"\n aria-label={group.label}\n className={cn(!group.textual && 'flex min-w-0 flex-row gap-0.5')}\n onCloseAutoFocus={(event) => {\n if (!picked.current) return\n\n picked.current = false\n event.preventDefault()\n editor.commands.focus()\n }}\n >\n {buttons.map((name) => {\n const label = commandLabel(name, commands, translate)\n const pressed = commands[name]?.active?.(editor)\n\n return (\n <DropdownMenuItem\n key={name}\n aria-label={group.textual ? undefined : label}\n title={label}\n className={cn(pressed && 'bg-accent text-accent-foreground')}\n onSelect={() => {\n picked.current = true\n onRun(name)\n }}\n >\n {icons(name)}\n {group.textual && <span>{label}</span>}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** Inline style of a sticky toolbar. */\nexport function stickyStyle(top: string | null): CSSProperties | undefined {\n return top === null ? undefined : { top }\n}\n\n/** The mention / merge tag listbox under the trigger. */\nexport function SuggestionPopup({\n suggestions,\n editor,\n id,\n}: {\n suggestions: EditorSuggestions\n editor: Editor | null\n id: string\n}) {\n const t = useT()\n const state: SuggestionState = useStore(suggestions.store)\n\n if (!state.open || !editor) return null\n\n return (\n <div\n id={id}\n role=\"listbox\"\n aria-label={t('editor.suggestions', {}, 'Suggestions')}\n data-slot=\"rich-editor-suggestions\"\n className=\"bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md\"\n // Runtime coordinates cannot be Tailwind classes.\n // Inside a modal the dialog's transform is the containing block of this fixed list.\n style={\n state.coords\n ? {\n left: state.coords.left - fixedContainingBlockOffset(editor.view.dom).x,\n top: state.coords.bottom + 4 - fixedContainingBlockOffset(editor.view.dom).y,\n }\n : undefined\n }\n >\n {state.loading && state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('loading', {}, 'Loading…')}\n </div>\n ) : state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </div>\n ) : (\n state.items.map((item, index) => (\n <div\n key={item.id}\n id={`${id}-${index}`}\n role=\"option\"\n aria-selected={index === state.index}\n className={cn(\n 'cursor-pointer rounded-sm px-2 py-1.5 text-sm',\n index === state.index && 'bg-accent text-accent-foreground',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => suggestions.highlight(index)}\n onClick={() => suggestions.select(index, editor)}\n >\n {state.kind === 'mention' ? `${state.trigger}${item.label}` : item.label}\n </div>\n ))\n )}\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"rich-editor-extensions.js","names":[],"sources":["../../../src/components/fields/rich-editor-extensions.tsx"],"sourcesContent":["import type {\n ActionContext,\n RichEditorPanel,\n RichEditorSchema,\n RichEditorToolbarGroupSchema,\n SuggestionState,\n ToolbarCommand,\n} from '@alfakit/core'\nimport {\n commandLabel,\n customBlockNodeConfig,\n EditorSuggestions,\n fieldActionContext,\n fixedContainingBlockOffset,\n groupCustomBlocks,\n groupTriggerButton,\n isCustomBlockResult,\n mentionNodeConfig,\n mergeTagNodeConfig,\n mergeTagText,\n RichEditorRemote,\n} from '@alfakit/core'\nimport { Node, type Editor, type Extensions } from '@tiptap/react'\nimport { BlocksIcon, BracesIcon, ChevronDownIcon, XIcon } from 'lucide-react'\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n type CSSProperties,\n type MutableRefObject,\n type ReactNode,\n} from 'react'\nimport { useActionRunner, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { Button } from '../ui/button'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { Input } from '../ui/input'\n\ntype NodeConfig = Parameters<typeof Node.create>[0]\n\nexport interface RichEditorExtensions {\n extensions: Extensions\n suggestions: EditorSuggestions\n /** Insert a custom block (opening its modal first when it has a form). */\n insertBlock: (id: string) => Promise<void>\n context: ActionContext\n}\n\n/**\n * Mentions, merge tags and custom blocks for a RichEditor. `editorRef` points at the live\n * editor, which the custom block node views and suggestions act on.\n */\nexport function useRichEditorExtensions(\n component: RichEditorSchema,\n editorRef: MutableRefObject<Editor | null>,\n): RichEditorExtensions {\n const t = useT()\n const form = useOptionalFormStore()\n const page = usePageActionContext()\n const runner = useActionRunner()\n const context = useMemo(\n () => fieldActionContext({ token: page.token, form, key: component.key }),\n [page.token, form, component.key],\n )\n const contextRef = useRef(context)\n const componentRef = useRef(component)\n\n const [remote] = useState(() => new RichEditorRemote(form, component.statePath))\n const [suggestions] = useState(\n () =>\n new EditorSuggestions({\n mentions: component.mentions ?? [],\n mergeTags: component.mergeTags ?? [],\n search: (trigger, query) => remote.searchMentions(trigger, query),\n }),\n )\n\n useEffect(() => () => suggestions.destroy(), [suggestions])\n\n const runBlock = async (id: string, config?: Record<string, unknown>) => {\n const block = componentRef.current.customBlocks?.find((candidate) => candidate.id === id)\n\n if (!block) return null\n\n const result = await runner.runField(block.action, {\n ...contextRef.current,\n arguments: config ? { config, editing: true } : {},\n })\n\n if (!isCustomBlockResult(result)) return null\n\n if (result.preview !== undefined) {\n remote.primePreview(result.id, result.config, result.preview, result.prose === true)\n }\n\n return result\n }\n\n const runBlockRef = useRef(runBlock)\n\n useEffect(() => {\n contextRef.current = context\n componentRef.current = component\n runBlockRef.current = runBlock\n })\n\n // The editor is created once; the node views read the latest props through refs when\n // they run (on edit, on preview), never during render.\n // eslint-disable-next-line react-hooks/refs\n const [extensions] = useState<Extensions>(() => [\n Node.create(mentionNodeConfig() as NodeConfig),\n Node.create(mergeTagNodeConfig() as NodeConfig),\n Node.create(\n customBlockNodeConfig({\n get blocks() {\n return componentRef.current.customBlocks ?? []\n },\n loadPreview: (id, config) => remote.loadPreview(id, config),\n onEdit: ({ id, config, pos }) => {\n void runBlockRef.current(id, config).then((result) => {\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n editor.view.dispatch(\n editor.state.tr.setNodeMarkup(pos, undefined, { id, config: result.config }),\n )\n })\n },\n labels: {\n edit: t('editor.edit_block', {}, 'Edit block'),\n remove: t('editor.remove_block', {}, 'Remove block'),\n },\n minimal: Boolean(component.minimalCustomBlockControls),\n }) as NodeConfig,\n ),\n ])\n\n const insertBlock = async (id: string) => {\n const result = await runBlock(id)\n const editor = editorRef.current\n\n if (!result || !editor || editor.isDestroyed) return\n\n const block = { type: 'customBlock', attrs: { id: result.id, config: result.config } }\n // A block inserted just before is still selected (a node selection): insert after it\n // instead of replacing it (W-S2-05).\n const { selection } = editor.state\n const chain = editor.chain().focus()\n ;('node' in selection && selection.node\n ? chain.insertContentAt(selection.to, block)\n : chain.insertContent(block)\n ).run()\n }\n\n return { extensions, suggestions, insertBlock, context }\n}\n\n/** The toolbar button that opens the custom blocks or merge tags panel. */\nexport function ExtensionToolbarButton({\n name,\n component,\n editable,\n open,\n onToggle,\n}: {\n name: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n open: boolean\n onToggle: () => void\n}) {\n const t = useT()\n const count =\n name === 'customBlocks'\n ? (component.customBlocks ?? []).length\n : (component.mergeTags ?? []).length\n\n if (count === 0) return null\n\n const label =\n name === 'customBlocks'\n ? t('editor.custom_blocks', {}, 'Insert block')\n : t('editor.merge_tags', {}, 'Insert merge tag')\n\n return (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n aria-pressed={open}\n aria-expanded={open}\n disabled={!editable}\n className={cn(open && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onToggle}\n >\n {name === 'customBlocks' ? <BlocksIcon /> : <BracesIcon />}\n </Button>\n )\n}\n\n/**\n * The custom blocks or merge tags panel: beside the content on wide editors, above it on\n * narrow ones. Blocks are grouped, optionally searchable and shown as a grid.\n */\nexport function RichEditorSidePanel({\n panel,\n component,\n editable,\n sticky,\n onInsertBlock,\n onInsertMergeTag,\n onClose,\n}: {\n panel: RichEditorPanel\n component: RichEditorSchema\n editable: boolean\n /** `top` of a sticky panel, or null. */\n sticky: string | null\n onInsertBlock: (id: string) => void\n onInsertMergeTag: (id: string) => void\n onClose: () => void\n}) {\n const t = useT()\n const [search, setSearch] = useState('')\n const heading =\n panel === 'customBlocks'\n ? t('editor.custom_blocks_heading', {}, 'Custom blocks')\n : t('editor.merge_tags_heading', {}, 'Merge tags')\n const groups =\n panel === 'customBlocks' ? groupCustomBlocks(component.customBlocks ?? [], search) : []\n const grid = Boolean(component.customBlocksGrid)\n const itemClass =\n 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring/50 flex min-w-0 items-center gap-2 rounded-md px-2 py-1.5 text-start text-sm outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50'\n\n return (\n <aside\n aria-label={heading}\n data-slot=\"rich-editor-panel\"\n data-panel={panel}\n className=\"bg-muted/20 shrink-0 border-b @2xl:w-64 @2xl:border-s @2xl:border-b-0\"\n >\n <div\n className={cn('flex flex-col gap-2 p-2', sticky !== null && '@2xl:sticky')}\n // The offset is a runtime value: no Tailwind class can hold it.\n style={sticky !== null ? { top: sticky } : undefined}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"text-muted-foreground text-xs font-medium\">{heading}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={t('editor.close_panel', {}, 'Close panel')}\n title={t('editor.close_panel', {}, 'Close panel')}\n onMouseDown={(event) => event.preventDefault()}\n onClick={onClose}\n >\n <XIcon />\n </Button>\n </div>\n {panel === 'customBlocks' && component.searchableCustomBlocks && (\n <Input\n type=\"search\"\n value={search}\n aria-label={t('editor.search_blocks', {}, 'Search blocks')}\n placeholder={t('editor.search_blocks', {}, 'Search blocks')}\n className=\"h-8\"\n onChange={(event) => setSearch(event.target.value)}\n />\n )}\n {panel === 'customBlocks' ? (\n groups.length === 0 ? (\n <p className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </p>\n ) : (\n groups.map(({ group, blocks }) => (\n <div key={group ?? ''} role=\"group\" aria-label={group ?? heading}>\n {group && (\n <div className=\"text-muted-foreground px-2 pt-1 pb-0.5 text-xs font-medium\">\n {group}\n </div>\n )}\n <div className={cn(grid ? 'grid grid-cols-2 gap-1' : 'flex flex-col gap-0.5')}>\n {blocks.map((block) => (\n <button\n key={block.id}\n type=\"button\"\n disabled={!editable}\n className={cn(\n itemClass,\n grid && 'flex-col justify-center border py-3 text-center',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertBlock(block.id)}\n >\n {block.icon ? (\n <Icon name={block.icon} className=\"size-4 shrink-0\" />\n ) : (\n <BlocksIcon className=\"size-4 shrink-0\" />\n )}\n <span className=\"truncate\">{block.label}</span>\n </button>\n ))}\n </div>\n </div>\n ))\n )\n ) : (\n <div className=\"flex flex-col gap-0.5\">\n {(component.mergeTags ?? []).map((tag) => (\n <button\n key={tag.id}\n type=\"button\"\n disabled={!editable}\n className={itemClass}\n onMouseDown={(event) => event.preventDefault()}\n onClick={() => onInsertMergeTag(tag.id)}\n >\n <span className=\"truncate\">{tag.label}</span>\n <span className=\"text-muted-foreground ms-auto font-mono text-xs\" dir=\"ltr\">\n {mergeTagText({ id: tag.id })}\n </span>\n </button>\n ))}\n </div>\n )}\n </div>\n </aside>\n )\n}\n\n/** A `ToolbarButtonGroup`: a dropdown of buttons (icons, or labels with `textualButtons()`). */\nexport function ToolbarGroupButton({\n group,\n editor,\n editable,\n commands,\n icons,\n translate,\n onRun,\n}: {\n group: RichEditorToolbarGroupSchema\n editor: Editor\n editable: boolean\n commands: Record<string, ToolbarCommand>\n icons: (name: string) => ReactNode\n translate: (key: string, fallback: string) => string\n onRun: (name: string) => void\n}) {\n const buttons = group.buttons.filter((name) => commands[name])\n // After a pick, typing goes on in the editor (the command focused it); the menu would\n // otherwise return focus to its trigger. Closing without a pick (Escape) still does.\n const picked = useRef(false)\n\n if (buttons.length === 0) return null\n\n const trigger = groupTriggerButton(buttons, editor, commands)\n const active = buttons.some((name) => commands[name]?.active?.(editor))\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n aria-label={group.label}\n title={group.label}\n disabled={!editable}\n className={cn('h-8 gap-0.5 px-1.5', active && 'bg-accent text-accent-foreground')}\n onMouseDown={(event) => event.preventDefault()}\n >\n {group.icon ? <Icon name={group.icon} /> : trigger ? icons(trigger) : group.label}\n <ChevronDownIcon className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent\n align=\"start\"\n aria-label={group.label}\n className={cn(!group.textual && 'flex min-w-0 flex-row gap-0.5')}\n onCloseAutoFocus={(event) => {\n if (!picked.current) return\n\n picked.current = false\n event.preventDefault()\n editor.commands.focus()\n }}\n >\n {buttons.map((name) => {\n const label = commandLabel(name, commands, translate)\n const pressed = commands[name]?.active?.(editor)\n\n return (\n <DropdownMenuItem\n key={name}\n aria-label={group.textual ? undefined : label}\n title={label}\n className={cn(pressed && 'bg-accent text-accent-foreground')}\n onSelect={() => {\n picked.current = true\n onRun(name)\n }}\n >\n {icons(name)}\n {group.textual && <span>{label}</span>}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\n/** Inline style of a sticky toolbar. */\nexport function stickyStyle(top: string | null): CSSProperties | undefined {\n return top === null ? undefined : { top }\n}\n\n/** The mention / merge tag listbox under the trigger. */\nexport function SuggestionPopup({\n suggestions,\n editor,\n id,\n}: {\n suggestions: EditorSuggestions\n editor: Editor | null\n id: string\n}) {\n const t = useT()\n const state: SuggestionState = useStore(suggestions.store)\n\n if (!state.open || !editor) return null\n\n return (\n <div\n id={id}\n role=\"listbox\"\n aria-label={t('editor.suggestions', {}, 'Suggestions')}\n data-slot=\"rich-editor-suggestions\"\n className=\"bg-popover text-popover-foreground fixed z-50 min-w-48 overflow-hidden rounded-md border p-1 shadow-md\"\n // Runtime coordinates cannot be Tailwind classes.\n // Inside a modal the dialog's transform is the containing block of this fixed list.\n style={\n state.coords\n ? {\n left: state.coords.left - fixedContainingBlockOffset(editor.view.dom).x,\n top: state.coords.bottom + 4 - fixedContainingBlockOffset(editor.view.dom).y,\n }\n : undefined\n }\n >\n {state.loading && state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('loading', {}, 'Loading…')}\n </div>\n ) : state.items.length === 0 ? (\n <div className=\"text-muted-foreground px-2 py-1.5 text-sm\">\n {t('no_results', {}, 'No results.')}\n </div>\n ) : (\n state.items.map((item, index) => (\n <div\n key={item.id}\n id={`${id}-${index}`}\n role=\"option\"\n aria-selected={index === state.index}\n className={cn(\n 'cursor-pointer rounded-sm px-2 py-1.5 text-sm',\n index === state.index && 'bg-accent text-accent-foreground',\n )}\n onMouseDown={(event) => event.preventDefault()}\n onMouseEnter={() => suggestions.highlight(index)}\n onClick={() => suggestions.select(index, editor)}\n >\n {state.kind === 'mention' ? `${state.trigger}${item.label}` : item.label}\n </div>\n ))\n )}\n </div>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"wizard.d.ts","names":[],"sources":["../../../src/components/layout/wizard.tsx"],"mappings":";;wBAwCgB,kBAAkB;EAAe,WAAW;oBAAc,IAAA"}
1
+ {"version":3,"file":"wizard.d.ts","names":[],"sources":["../../../src/components/layout/wizard.tsx"],"mappings":";;wBAyCgB,kBAAkB;EAAe,WAAW;oBAAc,IAAA"}
@@ -8,6 +8,7 @@ import { extraProps } from "../../lib/attributes.js";
8
8
  import { ActionButton } from "../actions/action-button.js";
9
9
  import { useOptionalFormStore } from "../form/form-context.js";
10
10
  import { DeferredSchema } from "./deferred.js";
11
+ import { useFieldActionContext } from "../schema/field-actions.js";
11
12
  import { SchemaRenderer } from "../schema/schema-renderer.js";
12
13
  import { collectFields, completedStepIcon, initialWizardStep, persistWizardStep, reportWizardProgress, schemaGapClass, wizardRequestedStepIndex, wizardStepFieldPaths, wizardStepQueryKey, wizardStepToReveal } from "@alfakit/core";
13
14
  import { useEffect, useState } from "react";
@@ -34,6 +35,8 @@ function WizardComponent({ component }) {
34
35
  if (stepQueryKey) persistWizardStep(current, stepQueryKey);
35
36
  }, [current, stepQueryKey]);
36
37
  const store = useOptionalFormStore();
38
+ const fieldContext = useFieldActionContext(component);
39
+ const submitContext = store ? fieldContext : actionContext;
37
40
  useEffect(() => {
38
41
  reportWizardProgress(store, current, steps.length);
39
42
  }, [
@@ -154,7 +157,7 @@ function WizardComponent({ component }) {
154
157
  children: component.nextActionLabel ?? t("form.next_step", {}, "Next")
155
158
  }) : component.submitAction ? /* @__PURE__ */ jsx(ActionButton, {
156
159
  action: component.submitAction,
157
- context: actionContext
160
+ context: submitContext
158
161
  }) : component.submitActionHtml ? /* @__PURE__ */ jsx("div", {
159
162
  "data-slot": "wizard-submit",
160
163
  dangerouslySetInnerHTML: { __html: component.submitActionHtml }
@@ -1 +1 @@
1
- {"version":3,"file":"wizard.js","names":[],"sources":["../../../src/components/layout/wizard.tsx"],"sourcesContent":["import type { WizardSchema } from '@alfakit/core'\nimport {\n collectFields,\n completedStepIcon,\n initialWizardStep,\n persistWizardStep,\n reportWizardProgress,\n schemaGapClass,\n wizardRequestedStepIndex,\n wizardStepFieldPaths,\n wizardStepQueryKey,\n wizardStepToReveal,\n} from '@alfakit/core'\nimport { CheckIcon } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { useT } from '../../context'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ActionButton } from '../actions/action-button'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport { DeferredSchema } from './deferred'\n\n/**\n * \"Next\" validates the current step's fields on the server before moving on; the submit\n * validates the whole form, and the wizard jumps to the first step containing an error\n * when new errors arrive (the user can still move between steps afterwards).\n */\n// Used when a wizard renders outside a form. Module-level so the snapshot stays stable;\n// a new object per read makes useSyncExternalStore re-render forever.\nconst NO_ERRORS: Record<string, string[]> = {}\nconst NO_FORM_STORE = {\n get: () => ({ errors: NO_ERRORS }),\n subscribe: () => () => {},\n}\n\nexport function WizardComponent({ component }: { component: WizardSchema }) {\n const t = useT()\n const steps = component.schema ?? []\n const [current, setCurrent] = useState(() =>\n initialWizardStep(steps.length, component.startStep, component.persistStepInQueryString),\n )\n\n const stepQueryKey = wizardStepQueryKey(component.persistStepInQueryString)\n const actionContext = usePageActionContext()\n\n useEffect(() => {\n if (stepQueryKey) persistWizardStep(current, stepQueryKey)\n }, [current, stepQueryKey])\n const store = useOptionalFormStore()\n\n // An action modal around this wizard shows its submit button on the last step only.\n useEffect(() => {\n reportWizardProgress(store, current, steps.length)\n }, [store, current, steps.length])\n const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors)\n\n const stepHasError = (index: number) => {\n const step = steps[index]\n\n return step ? collectFields(step.schema ?? []).some((field) => errors[field.statePath]) : false\n }\n\n const [seenErrors, setSeenErrors] = useState<Record<string, string[]>>({})\n\n if (seenErrors !== errors) {\n setSeenErrors(errors)\n\n const reveal = wizardStepToReveal(steps, seenErrors, errors)\n\n if (reveal !== null) setCurrent(reveal)\n }\n\n // `goToStep()` from the server: move once per request (each carries a new id).\n const [seenStepRequest, setSeenStepRequest] = useState(component.goToStep?.id)\n\n if (seenStepRequest !== component.goToStep?.id) {\n setSeenStepRequest(component.goToStep?.id)\n\n const requested = wizardRequestedStepIndex(\n steps,\n component.goToStep,\n current,\n component.skippable,\n )\n\n if (requested !== null) setCurrent(requested)\n }\n\n const activeIndex = current\n const [validating, setValidating] = useState(false)\n\n const next = async () => {\n const step = steps[activeIndex]\n\n if (store && step) {\n setValidating(true)\n const valid = await store.validateStep(\n step.key,\n step.deferred ? (step.deferredPaths ?? []) : wizardStepFieldPaths(step),\n )\n setValidating(false)\n\n if (!valid) return\n }\n\n setCurrent(activeIndex + 1)\n }\n\n return (\n <div\n data-surface=\"wizard\"\n\n {...extraProps(component.extraAttributes, 'bg-card rounded-xl border shadow-xs')}\n >\n <ol className=\"flex flex-wrap gap-2 border-b p-4\" aria-label={t('steps', {}, 'Steps')}>\n {steps.map((step, index) => {\n const done = index < activeIndex\n const active = index === activeIndex\n const clickable = component.skippable || index <= activeIndex\n\n return (\n <li key={step.key} className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={!clickable}\n onClick={() => setCurrent(index)}\n className={cn(\n 'flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors disabled:cursor-default',\n active ? 'text-foreground font-medium' : 'text-muted-foreground',\n clickable && !active && 'hover:text-foreground',\n )}\n >\n <span\n className={cn(\n 'flex size-6 items-center justify-center rounded-full border text-xs',\n active && 'border-primary bg-primary text-primary-foreground',\n done && 'border-success bg-success text-success-foreground',\n stepHasError(index) && 'border-destructive text-destructive',\n )}\n >\n {done ? (\n completedStepIcon(step, component) ? (\n <Icon name={completedStepIcon(step, component)} className=\"size-3.5\" />\n ) : (\n <Icon\n name=\"alfakit::schemas.wizard.completed-step\"\n fallback={CheckIcon}\n className=\"size-3.5\"\n />\n )\n ) : step.icon ? (\n <Icon name={step.icon} className=\"size-3.5\" />\n ) : (\n index + 1\n )}\n </span>\n <span className=\"flex flex-col items-start leading-tight\">\n {step.label}\n {step.description && (\n <span className=\"text-muted-foreground text-xs font-normal\">\n {step.description}\n </span>\n )}\n </span>\n </button>\n {index < steps.length - 1 && <span className=\"bg-border hidden h-px w-6 sm:block\" />}\n </li>\n )\n })}\n </ol>\n {steps.map((step, index) => (\n <div key={step.key} className={cn('p-6', index !== activeIndex && 'hidden')}>\n <DeferredSchema component={step}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n attributes={loaded.extraAttributes}\n className={schemaGapClass(loaded) || undefined}\n />\n )}\n </DeferredSchema>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2 border-t p-4\">\n {component.previousAction === false ? (\n <span />\n ) : component.previousAction ? (\n <ActionButton\n action={{ ...component.previousAction, disabled: activeIndex === 0 || undefined }}\n context={actionContext}\n onTrigger={() => setCurrent(activeIndex - 1)}\n />\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={activeIndex === 0}\n onClick={() => setCurrent(activeIndex - 1)}\n >\n {component.previousActionLabel ?? t('form.previous_step', {}, 'Back')}\n </Button>\n )}\n {activeIndex < steps.length - 1 ? (\n component.nextAction === false ? (\n <span />\n ) : component.nextAction ? (\n <ActionButton\n action={{\n ...component.nextAction,\n disabled: component.nextAction.disabled || validating || undefined,\n }}\n context={actionContext}\n onTrigger={() => next()}\n />\n ) : (\n <Button\n type=\"button\"\n disabled={validating}\n aria-busy={validating || undefined}\n onClick={() => void next()}\n >\n {component.nextActionLabel ?? t('form.next_step', {}, 'Next')}\n </Button>\n )\n ) : component.submitAction ? (\n <ActionButton action={component.submitAction} context={actionContext} />\n ) : component.submitActionHtml ? (\n <div\n data-slot=\"wizard-submit\"\n dangerouslySetInnerHTML={{ __html: component.submitActionHtml }}\n />\n ) : (\n <span className=\"text-muted-foreground text-xs\">{component.submitActionLabel}</span>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"wizard.js","names":[],"sources":["../../../src/components/layout/wizard.tsx"],"sourcesContent":["import type { WizardSchema } from '@alfakit/core'\nimport {\n collectFields,\n completedStepIcon,\n initialWizardStep,\n persistWizardStep,\n reportWizardProgress,\n schemaGapClass,\n wizardRequestedStepIndex,\n wizardStepFieldPaths,\n wizardStepQueryKey,\n wizardStepToReveal,\n} from '@alfakit/core'\nimport { CheckIcon } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { useT } from '../../context'\nimport { useStoreSelector } from '../../hooks/use-store'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ActionButton } from '../actions/action-button'\nimport { usePageActionContext } from '../actions/action-context'\nimport { useOptionalFormStore } from '../form/form-context'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { Button } from '../ui/button'\nimport { DeferredSchema } from './deferred'\n\n/**\n * \"Next\" validates the current step's fields on the server before moving on; the submit\n * validates the whole form, and the wizard jumps to the first step containing an error\n * when new errors arrive (the user can still move between steps afterwards).\n */\n// Used when a wizard renders outside a form. Module-level so the snapshot stays stable;\n// a new object per read makes useSyncExternalStore re-render forever.\nconst NO_ERRORS: Record<string, string[]> = {}\nconst NO_FORM_STORE = {\n get: () => ({ errors: NO_ERRORS }),\n subscribe: () => () => {},\n}\n\nexport function WizardComponent({ component }: { component: WizardSchema }) {\n const t = useT()\n const steps = component.schema ?? []\n const [current, setCurrent] = useState(() =>\n initialWizardStep(steps.length, component.startStep, component.persistStepInQueryString),\n )\n\n const stepQueryKey = wizardStepQueryKey(component.persistStepInQueryString)\n const actionContext = usePageActionContext()\n\n useEffect(() => {\n if (stepQueryKey) persistWizardStep(current, stepQueryKey)\n }, [current, stepQueryKey])\n const store = useOptionalFormStore()\n // Inside a form the submit action runs as a field action, so `$get` / `$set` work (W-S2-09).\n const fieldContext = useFieldActionContext(component)\n const submitContext = store ? fieldContext : actionContext\n\n // An action modal around this wizard shows its submit button on the last step only.\n useEffect(() => {\n reportWizardProgress(store, current, steps.length)\n }, [store, current, steps.length])\n const errors = useStoreSelector(store?.store ?? NO_FORM_STORE, (state) => state.errors)\n\n const stepHasError = (index: number) => {\n const step = steps[index]\n\n return step ? collectFields(step.schema ?? []).some((field) => errors[field.statePath]) : false\n }\n\n const [seenErrors, setSeenErrors] = useState<Record<string, string[]>>({})\n\n if (seenErrors !== errors) {\n setSeenErrors(errors)\n\n const reveal = wizardStepToReveal(steps, seenErrors, errors)\n\n if (reveal !== null) setCurrent(reveal)\n }\n\n // `goToStep()` from the server: move once per request (each carries a new id).\n const [seenStepRequest, setSeenStepRequest] = useState(component.goToStep?.id)\n\n if (seenStepRequest !== component.goToStep?.id) {\n setSeenStepRequest(component.goToStep?.id)\n\n const requested = wizardRequestedStepIndex(\n steps,\n component.goToStep,\n current,\n component.skippable,\n )\n\n if (requested !== null) setCurrent(requested)\n }\n\n const activeIndex = current\n const [validating, setValidating] = useState(false)\n\n const next = async () => {\n const step = steps[activeIndex]\n\n if (store && step) {\n setValidating(true)\n const valid = await store.validateStep(\n step.key,\n step.deferred ? (step.deferredPaths ?? []) : wizardStepFieldPaths(step),\n )\n setValidating(false)\n\n if (!valid) return\n }\n\n setCurrent(activeIndex + 1)\n }\n\n return (\n <div\n data-surface=\"wizard\"\n\n {...extraProps(component.extraAttributes, 'bg-card rounded-xl border shadow-xs')}\n >\n <ol className=\"flex flex-wrap gap-2 border-b p-4\" aria-label={t('steps', {}, 'Steps')}>\n {steps.map((step, index) => {\n const done = index < activeIndex\n const active = index === activeIndex\n const clickable = component.skippable || index <= activeIndex\n\n return (\n <li key={step.key} className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n disabled={!clickable}\n onClick={() => setCurrent(index)}\n className={cn(\n 'flex items-center gap-2 rounded-md px-2 py-1 text-sm transition-colors disabled:cursor-default',\n active ? 'text-foreground font-medium' : 'text-muted-foreground',\n clickable && !active && 'hover:text-foreground',\n )}\n >\n <span\n className={cn(\n 'flex size-6 items-center justify-center rounded-full border text-xs',\n active && 'border-primary bg-primary text-primary-foreground',\n done && 'border-success bg-success text-success-foreground',\n stepHasError(index) && 'border-destructive text-destructive',\n )}\n >\n {done ? (\n completedStepIcon(step, component) ? (\n <Icon name={completedStepIcon(step, component)} className=\"size-3.5\" />\n ) : (\n <Icon\n name=\"alfakit::schemas.wizard.completed-step\"\n fallback={CheckIcon}\n className=\"size-3.5\"\n />\n )\n ) : step.icon ? (\n <Icon name={step.icon} className=\"size-3.5\" />\n ) : (\n index + 1\n )}\n </span>\n <span className=\"flex flex-col items-start leading-tight\">\n {step.label}\n {step.description && (\n <span className=\"text-muted-foreground text-xs font-normal\">\n {step.description}\n </span>\n )}\n </span>\n </button>\n {index < steps.length - 1 && <span className=\"bg-border hidden h-px w-6 sm:block\" />}\n </li>\n )\n })}\n </ol>\n {steps.map((step, index) => (\n <div key={step.key} className={cn('p-6', index !== activeIndex && 'hidden')}>\n <DeferredSchema component={step}>\n {(loaded) => (\n <SchemaRenderer\n components={loaded.schema ?? []}\n columns={loaded.columns}\n attributes={loaded.extraAttributes}\n className={schemaGapClass(loaded) || undefined}\n />\n )}\n </DeferredSchema>\n </div>\n ))}\n <div className=\"flex items-center justify-between gap-2 border-t p-4\">\n {component.previousAction === false ? (\n <span />\n ) : component.previousAction ? (\n <ActionButton\n action={{ ...component.previousAction, disabled: activeIndex === 0 || undefined }}\n context={actionContext}\n onTrigger={() => setCurrent(activeIndex - 1)}\n />\n ) : (\n <Button\n type=\"button\"\n variant=\"outline\"\n disabled={activeIndex === 0}\n onClick={() => setCurrent(activeIndex - 1)}\n >\n {component.previousActionLabel ?? t('form.previous_step', {}, 'Back')}\n </Button>\n )}\n {activeIndex < steps.length - 1 ? (\n component.nextAction === false ? (\n <span />\n ) : component.nextAction ? (\n <ActionButton\n action={{\n ...component.nextAction,\n disabled: component.nextAction.disabled || validating || undefined,\n }}\n context={actionContext}\n onTrigger={() => next()}\n />\n ) : (\n <Button\n type=\"button\"\n disabled={validating}\n aria-busy={validating || undefined}\n onClick={() => void next()}\n >\n {component.nextActionLabel ?? t('form.next_step', {}, 'Next')}\n </Button>\n )\n ) : component.submitAction ? (\n <ActionButton action={component.submitAction} context={submitContext} />\n ) : component.submitActionHtml ? (\n <div\n data-slot=\"wizard-submit\"\n dangerouslySetInnerHTML={{ __html: component.submitActionHtml }}\n />\n ) : (\n <span className=\"text-muted-foreground text-xs\">{component.submitActionLabel}</span>\n )}\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"flash-toasts.d.ts","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"mappings":";;;;;;;wBAyBgB;wBA4BA,UACd,cAAc,oBACd,SAAS,MAAM;EAAgB,cAAc;;wBAqE/B,2BACd,QACA,SACA,cACA;EAEA,QAAQ;EACR;EACA;;EAEA;oBACD,IAAA"}
1
+ {"version":3,"file":"flash-toasts.d.ts","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"mappings":";;;;;;;wBAyBgB;wBAoCA,UACd,cAAc,oBACd,SAAS,MAAM;EAAgB,cAAc;;wBAqE/B,2BACd,QACA,SACA,cACA;EAEA,QAAQ;EACR;EACA;;EAEA;oBACD,IAAA"}
@@ -29,6 +29,16 @@ let toasterId;
29
29
  function setToasterId(id) {
30
30
  toasterId = id;
31
31
  }
32
+ /**
33
+ * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and
34
+ * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).
35
+ */
36
+ function toastOptions(options) {
37
+ return {
38
+ ...options ?? {},
39
+ toasterId
40
+ };
41
+ }
32
42
  /** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */
33
43
  function listenForClosedNotifications() {
34
44
  if (closeListening || typeof window === "undefined") return;
@@ -116,6 +126,6 @@ function notificationActionClasses(action) {
116
126
  }
117
127
  }
118
128
  //#endregion
119
- export { FlashToasts, NotificationActionButton, listenForClosedNotifications, setToasterId, showToast };
129
+ export { FlashToasts, NotificationActionButton, listenForClosedNotifications, setToasterId, showToast, toastOptions };
120
130
 
121
131
  //# sourceMappingURL=flash-toasts.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"flash-toasts.js","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"sourcesContent":["import type { NotificationActionSchema, NotificationSchema } from '@alfakit/core'\nimport { notificationTintStyles, onCloseNotification, resolveActionSize } from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useEffect } from 'react'\nimport { toast } from 'sonner'\nimport { useRegistry } from '../../context'\nimport {\n colorClass,\n cssColor,\n ghostButtonColorClasses,\n outlineButtonColorClasses,\n solidButtonColorClasses,\n textColorClasses,\n} from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ensureFlashToasts } from '../../standalone/runtime'\nimport { Button } from '../ui/button'\nimport { UndoButton } from './undo-button'\n\n/**\n * Turns flashed Alfakit notifications into sonner toasts. Mount once. Inertia 3 delivers\n * flash data as `page.flash` (and a `flash` event), not as a page prop, so this shares the\n * listener the standalone toaster uses.\n */\nexport function FlashToasts() {\n const registry = useRegistry()\n\n useEffect(() => {\n ensureFlashToasts(registry)\n }, [registry])\n\n return null\n}\n\nlet closeListening = false\n\n// Starter kit mode: toasts go to Alfakit's own toaster (`STARTER_KIT_TOASTER_ID`).\nlet toasterId: string | undefined\n\n/** Send Alfakit's toasts to the `<Toaster id>` with this id (undefined: the id-less one). */\nexport function setToasterId(id: string | undefined): void {\n toasterId = id\n}\n\n/** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */\nexport function listenForClosedNotifications(): void {\n if (closeListening || typeof window === 'undefined') return\n\n closeListening = true\n onCloseNotification((id) => toast.dismiss(id))\n}\n\nexport function showToast(\n notification: NotificationSchema,\n Custom?: React.ComponentType<{ notification: NotificationSchema }>,\n) {\n listenForClosedNotifications()\n\n const duration =\n notification.duration === 'persistent' ? Infinity : (notification.duration ?? 6000)\n\n if (Custom) {\n toast.custom(() => <Custom notification={notification} />, {\n id: notification.id,\n duration,\n toasterId,\n })\n\n return\n }\n\n const method =\n notification.status === 'success'\n ? toast.success\n : notification.status === 'danger'\n ? toast.error\n : notification.status === 'warning'\n ? toast.warning\n : notification.status === 'info'\n ? toast.info\n : toast\n\n const hasActions = (notification.actions?.length ?? 0) > 0 || Boolean(notification.undo)\n\n method(notification.title ?? '', {\n id: notification.id,\n duration,\n toasterId,\n // `color()`: tints the toast's background.\n style: notification.color\n ? (notificationTintStyles(cssColor(notification.color)).toast as React.CSSProperties)\n : undefined,\n // Action buttons go below the body (as in the Vue and Svelte toasts): sonner's `action`\n // slot sits beside the title and squeezes it into a narrow column.\n description:\n notification.body || hasActions ? (\n <div className=\"flex flex-col gap-1\">\n {notification.body && <span dangerouslySetInnerHTML={{ __html: notification.body }} />}\n {hasActions && (\n <div className=\"mt-1 flex flex-wrap items-center gap-1\">\n {notification.undo && (\n <UndoButton undo={notification.undo} toastId={notification.id} />\n )}\n {(notification.actions ?? []).map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n toastId={notification.id}\n />\n ))}\n </div>\n )}\n </div>\n ) : undefined,\n icon: notification.icon ? (\n <Icon\n name={notification.icon}\n className={cn('size-4', notification.iconColor && textColorClasses[notification.iconColor])}\n />\n ) : undefined,\n })\n}\n\nexport function NotificationActionButton({\n action,\n toastId,\n onMarkAsRead,\n onMarkAsUnread,\n}: {\n action: NotificationActionSchema\n toastId?: string\n onMarkAsRead?: () => void\n /** Database notifications: `markAsUnread()`. */\n onMarkAsUnread?: () => void\n}) {\n const run = () => {\n if (action.dispatch) {\n window.dispatchEvent(new CustomEvent(action.dispatch.event, { detail: action.dispatch.data }))\n }\n\n if (action.markAsRead) onMarkAsRead?.()\n if (action.markAsUnread) onMarkAsUnread?.()\n if (action.close && toastId) toast.dismiss(toastId)\n\n if (action.url) {\n if (action.openUrlInNewTab) {\n window.open(action.url, '_blank', 'noopener')\n } else {\n router.visit(action.url)\n }\n }\n }\n\n return (\n <Button\n type=\"button\"\n size={resolveActionSize(action.size, 'xs')}\n variant={notificationActionVariant(action)}\n className={notificationActionClasses(action)}\n onClick={run}\n >\n {action.icon && <Icon name={action.icon} className=\"size-3.5\" />}\n {action.label}\n </Button>\n )\n}\n\nfunction notificationActionVariant(\n action: NotificationActionSchema,\n): 'default' | 'outline' | 'ghost' | 'link' {\n switch (action.variant) {\n case 'button':\n return 'default'\n case 'outline':\n return 'outline'\n case 'ghost':\n case 'icon-button':\n return 'ghost'\n default:\n return 'link'\n }\n}\n\n/** The action's `color()` applied to its variant (links default to the primary colour). */\nfunction notificationActionClasses(action: NotificationActionSchema): string | undefined {\n if (!action.color) return undefined\n\n switch (action.variant) {\n case 'button':\n return colorClass(solidButtonColorClasses, action.color, 'primary')\n case 'outline':\n return colorClass(outlineButtonColorClasses, action.color, 'gray')\n case 'ghost':\n case 'icon-button':\n return colorClass(ghostButtonColorClasses, action.color, 'gray')\n default:\n return colorClass(textColorClasses, action.color, 'primary')\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyBA,SAAgB,cAAc;CAC5B,MAAM,WAAW,YAAY;CAE7B,gBAAgB;EACd,kBAAkB,QAAQ;CAC5B,GAAG,CAAC,QAAQ,CAAC;CAEb,OAAO;AACT;AAEA,IAAI,iBAAiB;AAGrB,IAAI;;AAGJ,SAAgB,aAAa,IAA8B;CACzD,YAAY;AACd;;AAGA,SAAgB,+BAAqC;CACnD,IAAI,kBAAkB,OAAO,WAAW,aAAa;CAErD,iBAAiB;CACjB,qBAAqB,OAAO,MAAM,QAAQ,EAAE,CAAC;AAC/C;AAEA,SAAgB,UACd,cACA,QACA;CACA,6BAA6B;CAE7B,MAAM,WACJ,aAAa,aAAa,eAAe,WAAY,aAAa,YAAY;CAEhF,IAAI,QAAQ;EACV,MAAM,aAAa,oBAAC,QAAD,EAAsB,aAAe,CAAA,GAAG;GACzD,IAAI,aAAa;GACjB;GACA;EACF,CAAC;EAED;CACF;CAEA,MAAM,SACJ,aAAa,WAAW,YACpB,MAAM,UACN,aAAa,WAAW,WACtB,MAAM,QACN,aAAa,WAAW,YACtB,MAAM,UACN,aAAa,WAAW,SACtB,MAAM,OACN;CAEZ,MAAM,cAAc,aAAa,SAAS,UAAU,KAAK,KAAK,QAAQ,aAAa,IAAI;CAEvF,OAAO,aAAa,SAAS,IAAI;EAC/B,IAAI,aAAa;EACjB;EACA;EAEA,OAAO,aAAa,QACf,uBAAuB,SAAS,aAAa,KAAK,CAAC,CAAC,CAAC,QACtD,KAAA;EAGJ,aACE,aAAa,QAAQ,aACnB,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACG,aAAa,QAAQ,oBAAC,QAAD,EAAM,yBAAyB,EAAE,QAAQ,aAAa,KAAK,EAAI,CAAA,GACpF,cACC,qBAAC,OAAD;IAAK,WAAU;IAAf,UAAA,CACG,aAAa,QACZ,oBAAC,YAAD;KAAY,MAAM,aAAa;KAAM,SAAS,aAAa;IAAK,CAAA,IAEhE,aAAa,WAAW,CAAC,EAAA,CAAG,KAAK,WACjC,oBAAC,0BAAD;KAEU;KACR,SAAS,aAAa;IACvB,GAHM,OAAO,IAGb,CACF,CACE;GAEJ,CAAA,CAAA;EACH,CAAA,IAAA,KAAA;EACN,MAAM,aAAa,OACjB,oBAAC,MAAD;GACE,MAAM,aAAa;GACnB,WAAW,GAAG,UAAU,aAAa,aAAa,iBAAiB,aAAa,UAAU;EAC3F,CAAA,IACC,KAAA;CACN,CAAC;AACH;AAEA,SAAgB,yBAAyB,EACvC,QACA,SACA,cACA,kBAOC;CACD,MAAM,YAAY;EAChB,IAAI,OAAO,UACT,OAAO,cAAc,IAAI,YAAY,OAAO,SAAS,OAAO,EAAE,QAAQ,OAAO,SAAS,KAAK,CAAC,CAAC;EAG/F,IAAI,OAAO,YAAY,eAAe;EACtC,IAAI,OAAO,cAAc,iBAAiB;EAC1C,IAAI,OAAO,SAAS,SAAS,MAAM,QAAQ,OAAO;EAElD,IAAI,OAAO,KAAK;GACd,IAAI,OAAO,iBACT,OAAO,KAAK,OAAO,KAAK,UAAU,UAAU;QAE5C,OAAO,MAAM,OAAO,GAAG;EAE3B;CACF;CAEA,OACE,qBAAC,QAAD;EACE,MAAK;EACL,MAAM,kBAAkB,OAAO,MAAM,IAAI;EACzC,SAAS,0BAA0B,MAAM;EACzC,WAAW,0BAA0B,MAAM;EAC3C,SAAS;EALX,UAAA,CAOG,OAAO,QAAQ,oBAAC,MAAD;GAAM,MAAM,OAAO;GAAM,WAAU;EAAY,CAAA,GAC9D,OAAO,KACF;;AAEZ;AAEA,SAAS,0BACP,QAC0C;CAC1C,QAAQ,OAAO,SAAf;EACE,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK;EACL,KAAK,eACH,OAAO;EACT,SACE,OAAO;CACX;AACF;;AAGA,SAAS,0BAA0B,QAAsD;CACvF,IAAI,CAAC,OAAO,OAAO,OAAO,KAAA;CAE1B,QAAQ,OAAO,SAAf;EACE,KAAK,UACH,OAAO,WAAW,yBAAyB,OAAO,OAAO,SAAS;EACpE,KAAK,WACH,OAAO,WAAW,2BAA2B,OAAO,OAAO,MAAM;EACnE,KAAK;EACL,KAAK,eACH,OAAO,WAAW,yBAAyB,OAAO,OAAO,MAAM;EACjE,SACE,OAAO,WAAW,kBAAkB,OAAO,OAAO,SAAS;CAC/D;AACF"}
1
+ {"version":3,"file":"flash-toasts.js","names":[],"sources":["../../../src/components/notifications/flash-toasts.tsx"],"sourcesContent":["import type { NotificationActionSchema, NotificationSchema } from '@alfakit/core'\nimport { notificationTintStyles, onCloseNotification, resolveActionSize } from '@alfakit/core'\nimport { router } from '@inertiajs/react'\nimport { useEffect } from 'react'\nimport { toast } from 'sonner'\nimport { useRegistry } from '../../context'\nimport {\n colorClass,\n cssColor,\n ghostButtonColorClasses,\n outlineButtonColorClasses,\n solidButtonColorClasses,\n textColorClasses,\n} from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ensureFlashToasts } from '../../standalone/runtime'\nimport { Button } from '../ui/button'\nimport { UndoButton } from './undo-button'\n\n/**\n * Turns flashed Alfakit notifications into sonner toasts. Mount once. Inertia 3 delivers\n * flash data as `page.flash` (and a `flash` event), not as a page prop, so this shares the\n * listener the standalone toaster uses.\n */\nexport function FlashToasts() {\n const registry = useRegistry()\n\n useEffect(() => {\n ensureFlashToasts(registry)\n }, [registry])\n\n return null\n}\n\nlet closeListening = false\n\n// Starter kit mode: toasts go to Alfakit's own toaster (`STARTER_KIT_TOASTER_ID`).\nlet toasterId: string | undefined\n\n/** Send Alfakit's toasts to the `<Toaster id>` with this id (undefined: the id-less one). */\nexport function setToasterId(id: string | undefined): void {\n toasterId = id\n}\n\n/**\n * Options for a direct `toast.*()` call: the starter kit toaster's id merged in, so PWA and\n * notification messages reach AlfaKit's toaster in a starter kit too (W-S1-03).\n */\nexport function toastOptions<T extends object>(options?: T): T & { toasterId?: string } {\n return { ...(options ?? ({} as T)), toasterId }\n}\n\n/** `closeNotification(id)` (or a `close-notification` event) dismisses that toast. */\nexport function listenForClosedNotifications(): void {\n if (closeListening || typeof window === 'undefined') return\n\n closeListening = true\n onCloseNotification((id) => toast.dismiss(id))\n}\n\nexport function showToast(\n notification: NotificationSchema,\n Custom?: React.ComponentType<{ notification: NotificationSchema }>,\n) {\n listenForClosedNotifications()\n\n const duration =\n notification.duration === 'persistent' ? Infinity : (notification.duration ?? 6000)\n\n if (Custom) {\n toast.custom(() => <Custom notification={notification} />, {\n id: notification.id,\n duration,\n toasterId,\n })\n\n return\n }\n\n const method =\n notification.status === 'success'\n ? toast.success\n : notification.status === 'danger'\n ? toast.error\n : notification.status === 'warning'\n ? toast.warning\n : notification.status === 'info'\n ? toast.info\n : toast\n\n const hasActions = (notification.actions?.length ?? 0) > 0 || Boolean(notification.undo)\n\n method(notification.title ?? '', {\n id: notification.id,\n duration,\n toasterId,\n // `color()`: tints the toast's background.\n style: notification.color\n ? (notificationTintStyles(cssColor(notification.color)).toast as React.CSSProperties)\n : undefined,\n // Action buttons go below the body (as in the Vue and Svelte toasts): sonner's `action`\n // slot sits beside the title and squeezes it into a narrow column.\n description:\n notification.body || hasActions ? (\n <div className=\"flex flex-col gap-1\">\n {notification.body && <span dangerouslySetInnerHTML={{ __html: notification.body }} />}\n {hasActions && (\n <div className=\"mt-1 flex flex-wrap items-center gap-1\">\n {notification.undo && (\n <UndoButton undo={notification.undo} toastId={notification.id} />\n )}\n {(notification.actions ?? []).map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n toastId={notification.id}\n />\n ))}\n </div>\n )}\n </div>\n ) : undefined,\n icon: notification.icon ? (\n <Icon\n name={notification.icon}\n className={cn('size-4', notification.iconColor && textColorClasses[notification.iconColor])}\n />\n ) : undefined,\n })\n}\n\nexport function NotificationActionButton({\n action,\n toastId,\n onMarkAsRead,\n onMarkAsUnread,\n}: {\n action: NotificationActionSchema\n toastId?: string\n onMarkAsRead?: () => void\n /** Database notifications: `markAsUnread()`. */\n onMarkAsUnread?: () => void\n}) {\n const run = () => {\n if (action.dispatch) {\n window.dispatchEvent(new CustomEvent(action.dispatch.event, { detail: action.dispatch.data }))\n }\n\n if (action.markAsRead) onMarkAsRead?.()\n if (action.markAsUnread) onMarkAsUnread?.()\n if (action.close && toastId) toast.dismiss(toastId)\n\n if (action.url) {\n if (action.openUrlInNewTab) {\n window.open(action.url, '_blank', 'noopener')\n } else {\n router.visit(action.url)\n }\n }\n }\n\n return (\n <Button\n type=\"button\"\n size={resolveActionSize(action.size, 'xs')}\n variant={notificationActionVariant(action)}\n className={notificationActionClasses(action)}\n onClick={run}\n >\n {action.icon && <Icon name={action.icon} className=\"size-3.5\" />}\n {action.label}\n </Button>\n )\n}\n\nfunction notificationActionVariant(\n action: NotificationActionSchema,\n): 'default' | 'outline' | 'ghost' | 'link' {\n switch (action.variant) {\n case 'button':\n return 'default'\n case 'outline':\n return 'outline'\n case 'ghost':\n case 'icon-button':\n return 'ghost'\n default:\n return 'link'\n }\n}\n\n/** The action's `color()` applied to its variant (links default to the primary colour). */\nfunction notificationActionClasses(action: NotificationActionSchema): string | undefined {\n if (!action.color) return undefined\n\n switch (action.variant) {\n case 'button':\n return colorClass(solidButtonColorClasses, action.color, 'primary')\n case 'outline':\n return colorClass(outlineButtonColorClasses, action.color, 'gray')\n case 'ghost':\n case 'icon-button':\n return colorClass(ghostButtonColorClasses, action.color, 'gray')\n default:\n return colorClass(textColorClasses, action.color, 'primary')\n }\n}\n"],"mappings":"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"}
@@ -6,7 +6,7 @@ import { Button } from "../ui/button.js";
6
6
  import { useStore } from "../../hooks/use-store.js";
7
7
  import { Spinner } from "../ui/spinner.js";
8
8
  import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover.js";
9
- import { NotificationActionButton } from "./flash-toasts.js";
9
+ import { NotificationActionButton, toastOptions } from "./flash-toasts.js";
10
10
  import { ScrollArea } from "../ui/scroll-area.js";
11
11
  import { NotificationCenter, notificationTintStyles, onDatabaseNotificationsSent, onOpenDatabaseNotifications, pwaStore, startPwa, togglePushNotifications } from "@alfakit/core";
12
12
  import { useEffect, useMemo, useState } from "react";
@@ -65,8 +65,8 @@ function NotificationsMenu({ variant = "topbar" } = {}) {
65
65
  const togglePush = async () => {
66
66
  const { message } = await togglePushNotifications();
67
67
  const text = t(message.key, {}, message.fallback);
68
- if (message.error) toast.error(text);
69
- else toast.success(text);
68
+ if (message.error) toast.error(text, toastOptions());
69
+ else toast.success(text, toastOptions());
70
70
  };
71
71
  const pushLabel = pwa.push === "subscribed" ? t("pwa.notifications.disable", {}, "Turn off notifications") : t("pwa.notifications.enable", {}, "Turn on notifications");
72
72
  if (!config || !center) return null;
@@ -1 +1 @@
1
- {"version":3,"file":"notifications-menu.js","names":[],"sources":["../../../src/components/notifications/notifications-menu.tsx"],"sourcesContent":["import {\n NotificationCenter,\n notificationTintStyles,\n onDatabaseNotificationsSent,\n onOpenDatabaseNotifications,\n pwaStore,\n startPwa,\n togglePushNotifications,\n} from '@alfakit/core'\nimport { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { toast } from 'sonner'\nimport { useDirection, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cssColor, textColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'\nimport { ScrollArea } from '../ui/scroll-area'\nimport { Spinner } from '../ui/spinner'\nimport { NotificationActionButton } from './flash-toasts'\n\nexport interface NotificationsMenuProps {\n /**\n * Where the trigger sits: `topbar` (an icon button, default) or `sidebar` (a full-width\n * row with a label, for layouts that move the bell into the sidebar).\n */\n variant?: 'topbar' | 'sidebar'\n}\n\n/**\n * Bell menu for database notifications. It also opens when something calls\n * `openDatabaseNotifications()` (or an action with `->openDatabaseNotifications()`).\n */\nexport function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps = {}) {\n const t = useT()\n const direction = useDirection()\n const format = useFormatter()\n const panel = usePanel()\n const config = panel.databaseNotifications\n const endpoints = panel.endpoints\n\n const center = useMemo(\n () =>\n endpoints.notifications && endpoints.notificationsReadAll && endpoints.notificationsDestroyAll\n ? new NotificationCenter(\n {\n notifications: endpoints.notifications,\n notificationsReadAll: endpoints.notificationsReadAll,\n notificationsDestroyAll: endpoints.notificationsDestroyAll,\n },\n config?.unreadCount ?? 0,\n )\n : null,\n [\n endpoints.notifications,\n endpoints.notificationsReadAll,\n endpoints.notificationsDestroyAll,\n config?.unreadCount,\n ],\n )\n\n const [open, setOpen] = useState(false)\n const state = useStore(\n center?.store ?? {\n get: () => ({ notifications: [], unreadCount: 0, loading: false, hasMore: false, page: 0 }),\n subscribe: () => () => {},\n },\n )\n\n useEffect(() => {\n if (!center) return\n\n center.startPolling(config?.pollingInterval)\n\n return () => center.stopPolling()\n }, [center, config?.pollingInterval])\n\n useEffect(() => {\n if (open) void center?.load()\n }, [open, center])\n\n // `openDatabaseNotifications()`: opened from code or an action.\n useEffect(() => onOpenDatabaseNotifications(() => setOpen(true)), [])\n\n // Realtime notifications announce stored ones, so the unread count updates right away.\n useEffect(\n () => (center ? onDatabaseNotificationsSent(() => void center.load()) : undefined),\n [center],\n )\n\n // `pwa(push: true)` in a starter kit: the app's user menu has no AlfaKit entries, so Web\n // Push is turned on or off from the bell's menu.\n const pwa = useStore(pwaStore)\n const kitPush = Boolean(panel.starterKit && panel.pwa?.push)\n const pushToggle = kitPush && pwa.push !== 'unsupported'\n\n useEffect(() => {\n if (kitPush) startPwa(panel.pwa)\n }, [kitPush, panel.pwa])\n\n const togglePush = async () => {\n const { message } = await togglePushNotifications()\n const text = t(message.key, {}, message.fallback)\n\n if (message.error) toast.error(text)\n else toast.success(text)\n }\n\n const pushLabel =\n pwa.push === 'subscribed'\n ? t('pwa.notifications.disable', {}, 'Turn off notifications')\n : t('pwa.notifications.enable', {}, 'Turn on notifications')\n\n if (!config || !center) {\n return null\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {variant === 'sidebar' ? (\n // In a collapsed (icon) sidebar only the bell and its badge stay visible.\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"relative w-full justify-start gap-2 group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0\"\n data-slot=\"notifications-trigger\"\n data-variant=\"sidebar\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n <span className=\"flex-1 text-start group-data-[collapsible=icon]:sr-only\">\n {t('notifications', {}, 'Notifications')}\n </span>\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs font-medium group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:-end-1 group-data-[collapsible=icon]:-top-1 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4 group-data-[collapsible=icon]:text-[10px]\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n <span className=\"sr-only\"> {t('unread_notifications', {}, 'unread')}</span>\n </span>\n )}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative\"\n aria-label={t('notifications', {}, 'Notifications')}\n data-slot=\"notifications-trigger\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n </span>\n )}\n </Button>\n )}\n </PopoverTrigger>\n <PopoverContent\n align={variant === 'sidebar' ? 'start' : 'end'}\n side={variant === 'sidebar' ? (direction === 'rtl' ? 'left' : 'right') : 'bottom'}\n className=\"w-80 p-0 sm:w-96\"\n >\n <div className=\"flex items-center justify-between border-b px-3 py-2\">\n <span className=\"text-sm font-semibold\">{t('notifications', {}, 'Notifications')}</span>\n <div className=\"flex items-center gap-1\">\n {pushToggle && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n data-slot=\"pwa-push\"\n disabled={pwa.pushBusy || pwa.push === 'denied'}\n title={pushLabel}\n aria-label={pushLabel}\n onClick={() => void togglePush()}\n >\n {pwa.push === 'subscribed' ? <BellOffIcon /> : <BellRingIcon />}\n </Button>\n )}\n {state.unreadCount > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('mark_all_read', {}, 'Mark all as read')}\n onClick={() => void center.markAllAsRead()}\n >\n <CheckCheckIcon />\n </Button>\n )}\n {state.notifications.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('clear_notifications', {}, 'Clear notifications')}\n onClick={() => void center.clear()}\n >\n <Trash2Icon />\n </Button>\n )}\n </div>\n </div>\n <ScrollArea className=\"max-h-96\">\n {state.loading && state.notifications.length === 0 ? (\n <div className=\"flex justify-center py-8\">\n <Spinner />\n </div>\n ) : state.notifications.length === 0 ? (\n <p className=\"text-muted-foreground px-3 py-8 text-center text-sm\">\n {t('no_notifications', {}, 'You have no notifications.')}\n </p>\n ) : (\n <ul className=\"divide-y\">\n {state.notifications.map((notification) => (\n <li\n key={notification.id}\n className={cn('flex gap-3 px-3 py-3', !notification.readAt && 'bg-primary/5')}\n style={\n notification.color\n ? notificationTintStyles(cssColor(notification.color)).item\n : undefined\n }\n >\n {notification.icon && (\n <Icon\n name={notification.icon}\n className={cn(\n 'mt-0.5 size-5 shrink-0',\n notification.iconColor && textColorClasses[notification.iconColor],\n )}\n />\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium\">{notification.title}</p>\n {!notification.readAt && (\n <button\n type=\"button\"\n className=\"focus-visible:ring-ring/50 -me-2 -mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full outline-none focus-visible:ring-[3px]\"\n aria-label={t('mark_as_read', {}, 'Mark as read')}\n onClick={() => void center.markAsRead(notification.id)}\n >\n <span className=\"bg-primary size-2 rounded-full\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n {notification.body && (\n <p\n className=\"text-muted-foreground text-sm\"\n dangerouslySetInnerHTML={{ __html: notification.body }}\n />\n )}\n {notification.date && (\n <time className=\"text-muted-foreground text-xs\">\n {format.relative(notification.date)}\n </time>\n )}\n {notification.actions && (\n <div className=\"flex flex-wrap gap-1\">\n {notification.actions.map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n onMarkAsRead={() => void center.markAsRead(notification.id)}\n onMarkAsUnread={() => void center.markAsUnread(notification.id)}\n />\n ))}\n </div>\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n {state.hasMore && state.notifications.length > 0 && (\n <div className=\"flex justify-center border-t p-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={state.loading}\n onClick={() => void center.loadMore()}\n >\n {state.loading && <Spinner />}\n {t('load_more', {}, 'Load more')}\n </Button>\n </div>\n )}\n </ScrollArea>\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"notifications-menu.js","names":[],"sources":["../../../src/components/notifications/notifications-menu.tsx"],"sourcesContent":["import {\n NotificationCenter,\n notificationTintStyles,\n onDatabaseNotificationsSent,\n onOpenDatabaseNotifications,\n pwaStore,\n startPwa,\n togglePushNotifications,\n} from '@alfakit/core'\nimport { BellIcon, BellOffIcon, BellRingIcon, CheckCheckIcon, Trash2Icon } from 'lucide-react'\nimport { useEffect, useMemo, useState } from 'react'\nimport { toast } from 'sonner'\nimport { useDirection, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cssColor, textColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'\nimport { ScrollArea } from '../ui/scroll-area'\nimport { Spinner } from '../ui/spinner'\nimport { NotificationActionButton, toastOptions } from './flash-toasts'\n\nexport interface NotificationsMenuProps {\n /**\n * Where the trigger sits: `topbar` (an icon button, default) or `sidebar` (a full-width\n * row with a label, for layouts that move the bell into the sidebar).\n */\n variant?: 'topbar' | 'sidebar'\n}\n\n/**\n * Bell menu for database notifications. It also opens when something calls\n * `openDatabaseNotifications()` (or an action with `->openDatabaseNotifications()`).\n */\nexport function NotificationsMenu({ variant = 'topbar' }: NotificationsMenuProps = {}) {\n const t = useT()\n const direction = useDirection()\n const format = useFormatter()\n const panel = usePanel()\n const config = panel.databaseNotifications\n const endpoints = panel.endpoints\n\n const center = useMemo(\n () =>\n endpoints.notifications && endpoints.notificationsReadAll && endpoints.notificationsDestroyAll\n ? new NotificationCenter(\n {\n notifications: endpoints.notifications,\n notificationsReadAll: endpoints.notificationsReadAll,\n notificationsDestroyAll: endpoints.notificationsDestroyAll,\n },\n config?.unreadCount ?? 0,\n )\n : null,\n [\n endpoints.notifications,\n endpoints.notificationsReadAll,\n endpoints.notificationsDestroyAll,\n config?.unreadCount,\n ],\n )\n\n const [open, setOpen] = useState(false)\n const state = useStore(\n center?.store ?? {\n get: () => ({ notifications: [], unreadCount: 0, loading: false, hasMore: false, page: 0 }),\n subscribe: () => () => {},\n },\n )\n\n useEffect(() => {\n if (!center) return\n\n center.startPolling(config?.pollingInterval)\n\n return () => center.stopPolling()\n }, [center, config?.pollingInterval])\n\n useEffect(() => {\n if (open) void center?.load()\n }, [open, center])\n\n // `openDatabaseNotifications()`: opened from code or an action.\n useEffect(() => onOpenDatabaseNotifications(() => setOpen(true)), [])\n\n // Realtime notifications announce stored ones, so the unread count updates right away.\n useEffect(\n () => (center ? onDatabaseNotificationsSent(() => void center.load()) : undefined),\n [center],\n )\n\n // `pwa(push: true)` in a starter kit: the app's user menu has no AlfaKit entries, so Web\n // Push is turned on or off from the bell's menu.\n const pwa = useStore(pwaStore)\n const kitPush = Boolean(panel.starterKit && panel.pwa?.push)\n const pushToggle = kitPush && pwa.push !== 'unsupported'\n\n useEffect(() => {\n if (kitPush) startPwa(panel.pwa)\n }, [kitPush, panel.pwa])\n\n const togglePush = async () => {\n const { message } = await togglePushNotifications()\n const text = t(message.key, {}, message.fallback)\n\n if (message.error) toast.error(text, toastOptions())\n else toast.success(text, toastOptions())\n }\n\n const pushLabel =\n pwa.push === 'subscribed'\n ? t('pwa.notifications.disable', {}, 'Turn off notifications')\n : t('pwa.notifications.enable', {}, 'Turn on notifications')\n\n if (!config || !center) {\n return null\n }\n\n return (\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger asChild>\n {variant === 'sidebar' ? (\n // In a collapsed (icon) sidebar only the bell and its badge stay visible.\n <Button\n type=\"button\"\n variant=\"ghost\"\n className=\"relative w-full justify-start gap-2 group-data-[collapsible=icon]:size-8 group-data-[collapsible=icon]:justify-center group-data-[collapsible=icon]:p-0\"\n data-slot=\"notifications-trigger\"\n data-variant=\"sidebar\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n <span className=\"flex-1 text-start group-data-[collapsible=icon]:sr-only\">\n {t('notifications', {}, 'Notifications')}\n </span>\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground flex h-5 min-w-5 items-center justify-center rounded-full px-1 text-xs font-medium group-data-[collapsible=icon]:absolute group-data-[collapsible=icon]:-end-1 group-data-[collapsible=icon]:-top-1 group-data-[collapsible=icon]:h-4 group-data-[collapsible=icon]:min-w-4 group-data-[collapsible=icon]:text-[10px]\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n <span className=\"sr-only\"> {t('unread_notifications', {}, 'unread')}</span>\n </span>\n )}\n </Button>\n ) : (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"relative\"\n aria-label={t('notifications', {}, 'Notifications')}\n data-slot=\"notifications-trigger\"\n >\n <Icon\n name=\"alfakit::panels.topbar.open-database-notifications-button\"\n fallback={BellIcon}\n />\n {state.unreadCount > 0 && (\n <span className=\"bg-primary text-primary-foreground absolute -end-0.5 -top-0.5 flex h-4 min-w-4 items-center justify-center rounded-full px-1 text-[10px] font-medium\">\n {state.unreadCount > 99 ? '99+' : state.unreadCount}\n </span>\n )}\n </Button>\n )}\n </PopoverTrigger>\n <PopoverContent\n align={variant === 'sidebar' ? 'start' : 'end'}\n side={variant === 'sidebar' ? (direction === 'rtl' ? 'left' : 'right') : 'bottom'}\n className=\"w-80 p-0 sm:w-96\"\n >\n <div className=\"flex items-center justify-between border-b px-3 py-2\">\n <span className=\"text-sm font-semibold\">{t('notifications', {}, 'Notifications')}</span>\n <div className=\"flex items-center gap-1\">\n {pushToggle && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n data-slot=\"pwa-push\"\n disabled={pwa.pushBusy || pwa.push === 'denied'}\n title={pushLabel}\n aria-label={pushLabel}\n onClick={() => void togglePush()}\n >\n {pwa.push === 'subscribed' ? <BellOffIcon /> : <BellRingIcon />}\n </Button>\n )}\n {state.unreadCount > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('mark_all_read', {}, 'Mark all as read')}\n onClick={() => void center.markAllAsRead()}\n >\n <CheckCheckIcon />\n </Button>\n )}\n {state.notifications.length > 0 && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n title={t('clear_notifications', {}, 'Clear notifications')}\n onClick={() => void center.clear()}\n >\n <Trash2Icon />\n </Button>\n )}\n </div>\n </div>\n <ScrollArea className=\"max-h-96\">\n {state.loading && state.notifications.length === 0 ? (\n <div className=\"flex justify-center py-8\">\n <Spinner />\n </div>\n ) : state.notifications.length === 0 ? (\n <p className=\"text-muted-foreground px-3 py-8 text-center text-sm\">\n {t('no_notifications', {}, 'You have no notifications.')}\n </p>\n ) : (\n <ul className=\"divide-y\">\n {state.notifications.map((notification) => (\n <li\n key={notification.id}\n className={cn('flex gap-3 px-3 py-3', !notification.readAt && 'bg-primary/5')}\n style={\n notification.color\n ? notificationTintStyles(cssColor(notification.color)).item\n : undefined\n }\n >\n {notification.icon && (\n <Icon\n name={notification.icon}\n className={cn(\n 'mt-0.5 size-5 shrink-0',\n notification.iconColor && textColorClasses[notification.iconColor],\n )}\n />\n )}\n <div className=\"flex min-w-0 flex-1 flex-col gap-1\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-medium\">{notification.title}</p>\n {!notification.readAt && (\n <button\n type=\"button\"\n className=\"focus-visible:ring-ring/50 -me-2 -mt-0.5 flex size-6 shrink-0 items-center justify-center rounded-full outline-none focus-visible:ring-[3px]\"\n aria-label={t('mark_as_read', {}, 'Mark as read')}\n onClick={() => void center.markAsRead(notification.id)}\n >\n <span className=\"bg-primary size-2 rounded-full\" aria-hidden=\"true\" />\n </button>\n )}\n </div>\n {notification.body && (\n <p\n className=\"text-muted-foreground text-sm\"\n dangerouslySetInnerHTML={{ __html: notification.body }}\n />\n )}\n {notification.date && (\n <time className=\"text-muted-foreground text-xs\">\n {format.relative(notification.date)}\n </time>\n )}\n {notification.actions && (\n <div className=\"flex flex-wrap gap-1\">\n {notification.actions.map((action) => (\n <NotificationActionButton\n key={action.name}\n action={action}\n onMarkAsRead={() => void center.markAsRead(notification.id)}\n onMarkAsUnread={() => void center.markAsUnread(notification.id)}\n />\n ))}\n </div>\n )}\n </div>\n </li>\n ))}\n </ul>\n )}\n {state.hasMore && state.notifications.length > 0 && (\n <div className=\"flex justify-center border-t p-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n disabled={state.loading}\n onClick={() => void center.loadMore()}\n >\n {state.loading && <Spinner />}\n {t('load_more', {}, 'Load more')}\n </Button>\n </div>\n )}\n </ScrollArea>\n </PopoverContent>\n </Popover>\n )\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pwa.d.ts","names":[],"sources":["../../../src/components/panel/pwa.tsx"],"mappings":";;;wBAqBsB,WAAW,GAAG,kBAAkB;;;;;wBAiBtC;;wBAwBA,gCAAY,IAAA"}
1
+ {"version":3,"file":"pwa.d.ts","names":[],"sources":["../../../src/components/panel/pwa.tsx"],"mappings":";;;wBAsBsB,WAAW,GAAG,kBAAkB;;;;;wBAiBtC;;wBA2BA,gCAAY,IAAA"}
@@ -2,6 +2,7 @@ import { usePanel, useT } from "../../context.js";
2
2
  import { Icon } from "../../lib/icons.js";
3
3
  import { DropdownMenuItem } from "../ui/dropdown-menu.js";
4
4
  import { useStore } from "../../hooks/use-store.js";
5
+ import { toastOptions } from "../notifications/flash-toasts.js";
5
6
  import { applyPwaUpdate, canInstallPwa, promptPwaInstall, pwaStore, startPwa, subscribeToPush, unsubscribeFromPush } from "@alfakit/core";
6
7
  import { useEffect } from "react";
7
8
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -11,7 +12,7 @@ import { toast } from "sonner";
11
12
  const UPDATE_TOAST_ID = "alfakit-pwa-update";
12
13
  /** Show the browser's install prompt, or the "Add to Home Screen" hint on iOS Safari. */
13
14
  async function installPwa(t) {
14
- if (await promptPwaInstall() === "ios") toast.info(t("pwa.ios_hint", {}, "To install this app, tap the Share button and then “Add to Home Screen”."), { duration: 1e4 });
15
+ if (await promptPwaInstall() === "ios") toast.info(t("pwa.ios_hint", {}, "To install this app, tap the Share button and then “Add to Home Screen”."), toastOptions({ duration: 1e4 }));
15
16
  }
16
17
  /**
17
18
  * `Panel::pwa()`: registers the service worker and offers "Reload" when a new version
@@ -27,14 +28,14 @@ function PwaManager() {
27
28
  }, [pwa]);
28
29
  useEffect(() => {
29
30
  if (!updateAvailable) return;
30
- toast(t("pwa.update_available", {}, "A new version is available"), {
31
+ toast(t("pwa.update_available", {}, "A new version is available"), toastOptions({
31
32
  id: UPDATE_TOAST_ID,
32
33
  duration: Infinity,
33
34
  action: {
34
35
  label: t("pwa.reload", {}, "Reload"),
35
36
  onClick: () => applyPwaUpdate()
36
37
  }
37
- });
38
+ }));
38
39
  }, [updateAvailable, t]);
39
40
  return null;
40
41
  }
@@ -47,10 +48,10 @@ function PwaMenuItems() {
47
48
  const push = panel.pwa.push && state.push !== "unsupported";
48
49
  const togglePush = async () => {
49
50
  const outcome = state.push === "subscribed" ? await unsubscribeFromPush() : await subscribeToPush();
50
- if (outcome === "subscribed") toast.success(t("pwa.notifications.enabled", {}, "Notifications are on for this device"));
51
- else if (outcome === "unsubscribed") toast.success(t("pwa.notifications.disabled", {}, "Notifications are off for this device"));
52
- else if (outcome === "denied") toast.error(t("pwa.notifications.blocked", {}, "Notifications are blocked in your browser settings"));
53
- else toast.error(t("pwa.notifications.failed", {}, "Couldn’t change notifications. Try again."));
51
+ if (outcome === "subscribed") toast.success(t("pwa.notifications.enabled", {}, "Notifications are on for this device"), toastOptions());
52
+ else if (outcome === "unsubscribed") toast.success(t("pwa.notifications.disabled", {}, "Notifications are off for this device"), toastOptions());
53
+ else if (outcome === "denied") toast.error(t("pwa.notifications.blocked", {}, "Notifications are blocked in your browser settings"), toastOptions());
54
+ else toast.error(t("pwa.notifications.failed", {}, "Couldn’t change notifications. Try again."), toastOptions());
54
55
  };
55
56
  return /* @__PURE__ */ jsxs(Fragment$1, { children: [canInstallPwa(state) && /* @__PURE__ */ jsxs(DropdownMenuItem, {
56
57
  "data-slot": "pwa-install",