@alfakit/react 0.1.10 → 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 (91) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/components/actions/action-button.js +13 -1
  3. package/dist/components/actions/action-button.js.map +1 -1
  4. package/dist/components/dev-tools/dev-tools-inspector.js +2 -2
  5. package/dist/components/dev-tools/dev-tools-inspector.js.map +1 -1
  6. package/dist/components/entries/other-entries.d.ts.map +1 -1
  7. package/dist/components/entries/other-entries.js +1 -1
  8. package/dist/components/entries/other-entries.js.map +1 -1
  9. package/dist/components/fields/builder.d.ts.map +1 -1
  10. package/dist/components/fields/builder.js +3 -3
  11. package/dist/components/fields/builder.js.map +1 -1
  12. package/dist/components/fields/file-upload.d.ts.map +1 -1
  13. package/dist/components/fields/file-upload.js +2 -5
  14. package/dist/components/fields/file-upload.js.map +1 -1
  15. package/dist/components/fields/money-input.d.ts +5 -0
  16. package/dist/components/fields/money-input.d.ts.map +1 -1
  17. package/dist/components/fields/money-input.js +2 -5
  18. package/dist/components/fields/money-input.js.map +1 -1
  19. package/dist/components/fields/repeater.d.ts.map +1 -1
  20. package/dist/components/fields/repeater.js +3 -3
  21. package/dist/components/fields/repeater.js.map +1 -1
  22. package/dist/components/fields/rich-editor-extensions.js +5 -2
  23. package/dist/components/fields/rich-editor-extensions.js.map +1 -1
  24. package/dist/components/layout/wizard.d.ts.map +1 -1
  25. package/dist/components/layout/wizard.js +4 -1
  26. package/dist/components/layout/wizard.js.map +1 -1
  27. package/dist/components/notifications/flash-toasts.d.ts.map +1 -1
  28. package/dist/components/notifications/flash-toasts.js +11 -1
  29. package/dist/components/notifications/flash-toasts.js.map +1 -1
  30. package/dist/components/notifications/notifications-menu.js +3 -3
  31. package/dist/components/notifications/notifications-menu.js.map +1 -1
  32. package/dist/components/panel/lazy-placeholder.d.ts +1 -0
  33. package/dist/components/panel/lazy-placeholder.d.ts.map +1 -1
  34. package/dist/components/panel/lazy-placeholder.js +38 -4
  35. package/dist/components/panel/lazy-placeholder.js.map +1 -1
  36. package/dist/components/panel/pwa.d.ts.map +1 -1
  37. package/dist/components/panel/pwa.js +8 -7
  38. package/dist/components/panel/pwa.js.map +1 -1
  39. package/dist/components/panel/shortcuts-overlay.js +1 -1
  40. package/dist/components/panel/shortcuts-overlay.js.map +1 -1
  41. package/dist/components/table/cells.js +2 -2
  42. package/dist/components/table/cells.js.map +1 -1
  43. package/dist/components/ui/calendar.d.ts.map +1 -1
  44. package/dist/components/ui/calendar.js +7 -0
  45. package/dist/components/ui/calendar.js.map +1 -1
  46. package/dist/components/widgets/dashboard-customizer.js +2 -2
  47. package/dist/components/widgets/dashboard-customizer.js.map +1 -1
  48. package/dist/components/widgets/widgets-block.js +2 -2
  49. package/dist/components/widgets/widgets-block.js.map +1 -1
  50. package/dist/components.css +19 -0
  51. package/dist/create-alfakit.d.ts.map +1 -1
  52. package/dist/create-alfakit.js +3 -2
  53. package/dist/create-alfakit.js.map +1 -1
  54. package/dist/eject-manifest.json +4 -2
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +2 -2
  57. package/dist/pages/page.js +1 -1
  58. package/dist/pages/page.js.map +1 -1
  59. package/dist/standalone/alfakit-component.js +2 -2
  60. package/dist/standalone/alfakit-component.js.map +1 -1
  61. package/dist/standalone.css +21 -0
  62. package/dist/starter-kit/index.d.ts +2 -2
  63. package/dist/starter-kit/index.js +2 -2
  64. package/dist/starter-kit/starter-kit.d.ts +9 -0
  65. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  66. package/dist/starter-kit/starter-kit.js +21 -2
  67. package/dist/starter-kit/starter-kit.js.map +1 -1
  68. package/package.json +2 -2
  69. package/src/components/actions/action-button.tsx +15 -1
  70. package/src/components/dev-tools/dev-tools-inspector.tsx +6 -1
  71. package/src/components/entries/other-entries.tsx +2 -0
  72. package/src/components/fields/builder.tsx +7 -2
  73. package/src/components/fields/file-upload.tsx +2 -7
  74. package/src/components/fields/money-input.tsx +3 -14
  75. package/src/components/fields/repeater.tsx +6 -3
  76. package/src/components/fields/rich-editor-extensions.tsx +9 -5
  77. package/src/components/layout/wizard.tsx +5 -1
  78. package/src/components/notifications/flash-toasts.tsx +8 -0
  79. package/src/components/notifications/notifications-menu.tsx +3 -3
  80. package/src/components/panel/lazy-placeholder.tsx +41 -1
  81. package/src/components/panel/pwa.tsx +23 -9
  82. package/src/components/panel/shortcuts-overlay.tsx +1 -1
  83. package/src/components/table/cells.tsx +6 -2
  84. package/src/components/ui/calendar.tsx +8 -0
  85. package/src/components/widgets/dashboard-customizer.tsx +2 -2
  86. package/src/components/widgets/widgets-block.tsx +2 -2
  87. package/src/create-alfakit.tsx +9 -1
  88. package/src/pages/page.tsx +1 -1
  89. package/src/standalone/alfakit-component.tsx +2 -2
  90. package/src/starter-kit/index.ts +1 -0
  91. package/src/starter-kit/starter-kit.tsx +22 -0
@@ -1 +1 @@
1
- {"version":3,"file":"file-upload.js","names":[],"sources":["../../../src/components/fields/file-upload.tsx"],"sourcesContent":["import type { FileUploadSchema, UploadedFile } from '@alfakit/core'\nimport {\n canEditImage,\n concurrencyLimiter,\n cssAspectRatio,\n editableImageName,\n fetchEditableImage,\n fieldMemory,\n fileManagerSize,\n filesFromClipboard,\n fileUploadAccept,\n fileUploadPaths,\n insertUploadedFile,\n moveRepeaterItem,\n panelUploadOptions,\n requestJson,\n transformImage,\n uploadedFileLinks,\n uploadTemporaryFile,\n} from '@alfakit/core'\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n DownloadIcon,\n ExternalLinkIcon,\n FileIcon,\n PencilIcon,\n UploadCloudIcon,\n XIcon,\n} from 'lucide-react'\nimport { useRef, useState, type ClipboardEvent, type DragEvent } from 'react'\nimport { useChoice, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cn } from '../../lib/utils'\nimport { useField, useFormStore } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Spinner } from '../ui/spinner'\nimport type { ImageEditRequest } from './image-editor-dialog'\nimport { ImageEditorDialog } from './image-editor-dialog-lazy'\n\ninterface LocalFile extends UploadedFile {\n progress?: number\n error?: string\n preview?: string\n}\n\nexport function FileUploadField({ component }: { component: FileUploadSchema }) {\n const t = useT()\n const formatter = useFormatter()\n const choice = useChoice()\n const panel = usePanel()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<\n string | string[] | null\n >(component)\n // Read-only keeps open/download links but hides upload and remove controls.\n const locked = disabled || readOnly\n const store = useFormStore()\n // In-flight uploads and the metadata of files uploaded in this session (the server schema\n // only knows about saved files) live in the form's field memory, not in component state:\n // a virtualised Repeater item unmounts when scrolled out of view and finds them again,\n // local image previews included, when it mounts back.\n const uploadingStore = fieldMemory<LocalFile[]>(\n store,\n `${component.statePath}:uploading`,\n () => [],\n )\n const uploadedStore = fieldMemory<FileUploadSchema['files']>(\n store,\n `${component.statePath}:uploaded`,\n () => [],\n )\n const uploading = useStore(uploadingStore)\n const uploaded = useStore(uploadedStore)\n const setUploading = uploadingStore.set\n const setUploaded = uploadedStore.set\n const [dragging, setDragging] = useState(false)\n // Previews whose image failed to load (missing or unreachable file) show the file icon.\n const [brokenPreviews, setBrokenPreviews] = useState<ReadonlySet<string>>(() => new Set())\n const inputRef = useRef<HTMLInputElement>(null)\n const [dragPath, setDragPath] = useState<string | null>(null)\n // The image open in the editor (`imageEditor()`), one at a time.\n const [editing, setEditing] = useState<ImageEditRequest | null>(null)\n const editImage = (file: File) =>\n new Promise<File | null>((resolve) =>\n setEditing({\n file,\n resolve: (edited) => {\n setEditing(null)\n resolve(edited)\n },\n }),\n )\n // Object URLs made for local image previews; released when the file is removed.\n const objectUrls = fieldMemory<Set<string>>(\n store,\n `${component.statePath}:previews`,\n () => new Set(),\n ).get()\n\n const releasePreview = (url: string | undefined) => {\n if (url && objectUrls.delete(url)) URL.revokeObjectURL(url)\n }\n\n const paths = fileUploadPaths(value)\n const files: LocalFile[] = paths.map(\n (path) =>\n [...component.files, ...uploaded].find((file) => file.path === path) ?? {\n path,\n name: path.replace(/^tmp:/, ''),\n },\n )\n const maxFiles = component.multiple ? (component.maxFiles ?? Infinity) : 1\n const canAddMore = files.length + uploading.length < maxFiles\n\n const commit = (next: string[]) => setValue(component.multiple ? next : (next[0] ?? null))\n\n const upload = async (list: FileList | File[]) => {\n const endpoint = panel.endpoints.upload\n\n if (!endpoint) {\n return\n }\n\n const accepted = Array.from(list).slice(\n 0,\n Math.max(0, maxFiles - files.length - uploading.length),\n )\n\n // `maxParallelUploads()`: files are prepared (edited, resized) one by one and then\n // queued, so at most that many upload at once.\n const limit = concurrencyLimiter(component.maxParallelUploads ?? 1)\n const queued: Promise<void>[] = []\n\n for (const [position, picked] of accepted.entries()) {\n let original = picked\n const edit = Boolean(component.imageEditor && canEditImage(picked))\n\n if (edit) {\n const edited = await editImage(picked)\n\n // Cancelled: the file is not uploaded.\n if (!edited) continue\n\n original = edited\n }\n\n const file = await prepare(original, edit)\n\n if (!file) continue\n\n const local = pendingFile(file, position)\n setUploading((state) => [...state, local])\n\n queued.push(limit(() => send(endpoint, file, local)))\n }\n\n await Promise.all(queued)\n }\n\n /** Resize and orient a picked (or edited) file, and check its size. Null when rejected. */\n const prepare = async (original: File, edited: boolean): Promise<File | null> => {\n const file =\n component.imageCropAspectRatio ||\n component.imageResizeTargetWidth ||\n component.imageResizeTargetHeight ||\n component.orientImagesFromExif\n ? await transformImage(original, {\n // The editor already cropped the image.\n aspectRatio: edited ? null : component.imageCropAspectRatio,\n mode: component.imageResizeMode,\n width: component.imageResizeTargetWidth,\n height: component.imageResizeTargetHeight,\n orient: component.orientImagesFromExif,\n })\n : original\n\n if (component.minSize && file.size < component.minSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_small', {}, 'This file is too small.'),\n },\n ])\n\n return null\n }\n\n if (component.maxSize && file.size > component.maxSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_large', {}, 'This file is too large.'),\n },\n ])\n\n return null\n }\n\n return file\n }\n\n /** The in-progress entry shown while `file` uploads. */\n const pendingFile = (file: File, position: number | string): LocalFile => {\n const local: LocalFile = {\n // Unique within the batch and across re-picks of a changed file.\n path: `pending:${position}:${file.lastModified}:${file.size}:${file.name}`,\n name: file.name,\n size: file.size,\n progress: 0,\n preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,\n }\n if (local.preview) objectUrls.add(local.preview)\n\n return local\n }\n\n /**\n * The \"Edit\" button on an uploaded image: open it in the editor and upload the result\n * in its place (the old file goes like any removed one: a temporary upload is deleted\n * now, a stored file when the form is saved with `deleteRemovedFiles()`).\n */\n const editExisting = async (existing: LocalFile) => {\n const endpoint = panel.endpoints.upload\n const url = existing.url ?? existing.preview\n const name = editableImageName(existing)\n\n if (!endpoint || !url || !name) return\n\n const source = await fetchEditableImage(url, name)\n\n if (!source) {\n setUploading((state) => [\n ...state,\n {\n path: `failed:${existing.path}`,\n name: existing.name,\n error: t('image_editor.load_failed', {}, 'The image could not be opened.'),\n },\n ])\n\n return\n }\n\n const edited = await editImage(source)\n\n // Cancelled, or the editor couldn't decode it: keep the file as it is.\n if (!edited || edited === source) return\n\n const file = await prepare(edited, true)\n\n if (!file) return\n\n const local = pendingFile(file, `edit:${existing.path}`)\n setUploading((state) => [...state, local])\n await send(endpoint, file, local, existing.path)\n }\n\n const send = async (endpoint: string, file: File, local: LocalFile, replacing?: string) => {\n try {\n // Large files go in resumable chunks; progress covers the whole file.\n const response = await uploadTemporaryFile(endpoint, file, {\n ...panelUploadOptions(panel, component.uploadPolicy),\n onProgress: (progress) =>\n setUploading((state) =>\n state.map((item) => (item.path === local.path ? { ...item, progress } : item)),\n ),\n })\n\n setUploaded((state) => [\n ...state,\n {\n path: response.key,\n name: response.name,\n size: response.size,\n temporary: true,\n url: local.preview,\n },\n ])\n setUploading((state) => state.filter((item) => item.path !== local.path))\n // Read the store, not `value`: it's stale after the first file of a batch.\n const current = fileUploadPaths(\n store.getValue<string | string[] | null>(component.statePath, null),\n )\n\n if (replacing !== undefined && current.includes(replacing)) {\n // An edited image takes the old one's place.\n commit(current.map((path) => (path === replacing ? response.key : path)))\n discard(replacing)\n } else {\n commit(insertUploadedFile(current, response.key, component, maxFiles))\n }\n } catch (uploadError) {\n const message =\n (uploadError as { errors?: Record<string, string[]> }).errors?.file?.[0] ??\n (uploadError as Error).message\n setUploading((state) =>\n state.map((item) =>\n item.path === local.path ? { ...item, error: message, progress: undefined } : item,\n ),\n )\n }\n }\n\n const remove = (path: string) => {\n commit(paths.filter((item) => item !== path))\n discard(path)\n }\n\n /** Release a file that left the field: its preview, and a temporary upload's file. */\n const discard = (path: string) => {\n releasePreview(uploadedStore.get().find((file) => file.path === path)?.url)\n\n if (path.startsWith('tmp:') && panel.endpoints.uploadDestroy) {\n void requestJson(panel.endpoints.uploadDestroy, {\n method: 'DELETE',\n body: { key: path },\n }).catch(() => undefined)\n }\n }\n\n const onDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n setDragging(false)\n\n if (!locked && event.dataTransfer.files.length > 0) {\n void upload(event.dataTransfer.files)\n }\n }\n\n // `pasteable()`: images and files pasted while the drop zone has focus.\n const onPaste = (event: ClipboardEvent<HTMLDivElement>) => {\n if (locked || component.pasteable === false) return\n\n const pasted = filesFromClipboard(event.clipboardData)\n\n if (pasted.length > 0) {\n event.preventDefault()\n void upload(pasted)\n }\n }\n\n const move = (from: number, to: number) => commit(moveRepeaterItem(paths, from, to) as string[])\n const reorderable = Boolean(component.reorderable && component.multiple && !locked)\n\n // `previewable(false)` shows every file as a name and icon, without the image.\n const isImage = (file: LocalFile) =>\n component.previewable !== false &&\n (component.image || /\\.(png|jpe?g|gif|webp|svg|avif)$/i.test(file.name))\n const circle = component.panelLayout === 'circle' || component.avatar\n const integrated = component.panelLayout === 'integrated' && !circle\n const removeAtStart = component.positions?.removeButton === 'start'\n const progressAtStart = component.positions?.progressIndicator === 'start'\n const uploadAlign = component.positions?.uploadButton ?? 'center'\n\n // `imageEditor()`: uploaded JPEG, PNG and WebP images can be edited again.\n const canEditExisting = (file: LocalFile, index: number) =>\n Boolean(component.imageEditor) &&\n !locked &&\n index < files.length &&\n !file.missing &&\n Boolean(file.url ?? file.preview) &&\n editableImageName(file) !== null\n\n const renderRemove = (file: LocalFile) =>\n component.deletable &&\n !locked && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.remove', { name: file.name }, 'Remove :name')}\n onClick={() => {\n if (file.error || file.progress !== undefined) {\n releasePreview(file.preview)\n setUploading((state) => state.filter((item) => item.path !== file.path))\n } else {\n remove(file.path)\n }\n }}\n >\n <XIcon />\n </Button>\n )\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n data-slot=\"file-upload\"\n data-layout={component.panelLayout}\n className={cn('flex flex-col gap-3', integrated && 'rounded-md border p-2')}\n >\n {(files.length > 0 || uploading.length > 0) && (\n <ul\n className={cn(\n 'grid gap-2',\n component.panelLayout === 'grid' && 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4',\n circle && 'justify-items-center',\n )}\n >\n {[...files, ...uploading].map((file, index) => (\n <li\n key={file.path}\n draggable={reorderable && index < files.length}\n onDragStart={() => setDragPath(file.path)}\n onDragOver={(event) => reorderable && event.preventDefault()}\n onDrop={(event) => {\n if (!reorderable || dragPath === null) return\n event.preventDefault()\n event.stopPropagation()\n const from = paths.indexOf(dragPath)\n if (from !== -1 && index < files.length) move(from, index)\n setDragPath(null)\n }}\n onDragEnd={() => setDragPath(null)}\n className={cn(\n dragPath === file.path && 'opacity-50',\n 'bg-muted/30 relative flex min-w-0 items-center gap-3 overflow-hidden rounded-md border p-2 text-sm',\n circle && 'size-28 justify-center rounded-full border-0 p-0',\n file.error && 'border-destructive',\n )}\n >\n {!circle && removeAtStart && renderRemove(file)}\n {!circle && progressAtStart && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {isImage(file) && (file.url || file.preview) && !brokenPreviews.has(file.path) ? (\n <img\n src={file.url ?? file.preview}\n alt=\"\"\n onError={() => setBrokenPreviews((current) => new Set(current).add(file.path))}\n className={cn(\n 'size-12 shrink-0 rounded object-cover',\n circle && 'size-28 rounded-full',\n )}\n style={\n !circle && component.imagePreviewHeight\n ? {\n height: cssLength(component.imagePreviewHeight),\n width: 'auto',\n maxWidth: '50%',\n }\n : undefined\n }\n />\n ) : (\n !circle && <FileIcon className=\"text-muted-foreground size-6 shrink-0\" />\n )}\n {!circle && (\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium\">{file.name}</p>\n <p\n className={cn(\n 'text-muted-foreground text-xs',\n file.missing && 'text-destructive',\n )}\n >\n {file.error ??\n (file.missing\n ? t('form.file_missing', {}, 'This file no longer exists.')\n : file.progress !== undefined && !file.error\n ? (component.uploadingMessage ?? t('form.uploading', {}, 'Uploading…'))\n : file.size\n ? fileManagerSize(file.size, formatter)\n : file.temporary\n ? t('form.uploading', {}, 'Pending')\n : '')}\n </p>\n </div>\n )}\n <div\n className={cn('flex items-center gap-1', circle && 'absolute -end-1 -bottom-1')}\n >\n {(circle || !progressAtStart) && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {reorderable && index < files.length && (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('file_upload.move_up', { name: file.name }, 'Move :name up')}\n disabled={index === 0}\n onClick={() => move(index, index - 1)}\n >\n <ChevronUpIcon />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t(\n 'file_upload.move_down',\n { name: file.name },\n 'Move :name down',\n )}\n disabled={index === files.length - 1}\n onClick={() => move(index, index + 1)}\n >\n <ChevronDownIcon />\n </Button>\n </>\n )}\n {canEditExisting(file, index) && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.edit', { name: file.name }, 'Edit :name')}\n title={t('image_editor.title', {}, 'Edit image')}\n data-action=\"edit-image\"\n onClick={() => void editExisting(file)}\n >\n <PencilIcon />\n </Button>\n )}\n {component.openable && uploadedFileLinks(file).open && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).open}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${t('file_upload.open', { name: file.name }, 'Open :name')} ${t('opens_in_new_tab', {}, '(opens in a new tab)')}`}\n >\n <ExternalLinkIcon />\n </a>\n </Button>\n )}\n {component.downloadable && uploadedFileLinks(file).download && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).download}\n download\n aria-label={t(\n 'file_upload.download',\n { name: file.name },\n 'Download :name',\n )}\n >\n <DownloadIcon />\n </a>\n </Button>\n )}\n {(circle || !removeAtStart) && renderRemove(file)}\n </div>\n </li>\n ))}\n </ul>\n )}\n {canAddMore && !locked && (\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => inputRef.current?.click()}\n aria-describedby={aria['aria-describedby']}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n inputRef.current?.click()\n }\n }}\n onDragOver={(event) => {\n event.preventDefault()\n setDragging(true)\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={onDrop}\n onPaste={onPaste}\n style={\n !circle && component.panelAspectRatio\n ? { aspectRatio: cssAspectRatio(component.panelAspectRatio) }\n : undefined\n }\n data-align={uploadAlign}\n className={cn(\n 'text-muted-foreground hover:border-ring flex cursor-pointer flex-col justify-center gap-1 rounded-md border border-dashed px-4 py-6 text-sm transition-colors',\n uploadAlign === 'start'\n ? 'items-start text-start'\n : uploadAlign === 'end'\n ? 'items-end text-end'\n : 'items-center text-center',\n integrated && 'border-transparent',\n dragging && 'border-ring bg-accent',\n circle && 'size-28 items-center rounded-full py-0',\n )}\n >\n <UploadCloudIcon className=\"size-5\" />\n {!circle && (\n <span>\n {component.placeholder ??\n t('form.drop_files', {}, 'Drop files here or click to browse')}\n </span>\n )}\n {!circle &&\n (component.acceptedFileTypes || component.maxSize || component.minFiles) && (\n <span className=\"text-muted-foreground text-xs\">\n {[\n component.acceptedFileTypes?.join(', '),\n component.maxSize\n ? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}`\n : null,\n component.minFiles\n ? choice(\n 'form.min_files',\n component.minFiles,\n {},\n 'At least :count file|At least :count files',\n )\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </span>\n )}\n </div>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n className=\"sr-only\"\n tabIndex={-1}\n {...aria}\n multiple={component.multiple}\n accept={fileUploadAccept(component)}\n disabled={locked}\n onChange={(event) => {\n // Copy the list: clearing the input empties it while the editor is open.\n const picked = event.target.files ? Array.from(event.target.files) : []\n event.target.value = ''\n void upload(picked)\n }}\n />\n {component.imageEditor && (\n <ImageEditorDialog request={editing} options={component.imageEditor} />\n )}\n </div>\n </FieldWrapper>\n )\n}\n\n/** `imagePreviewHeight()`: numbers are pixels, strings are CSS lengths (`'250px'`, `'10rem'`). */\nfunction cssLength(value: number | string): string {\n return typeof value === 'number' || /^\\d+(\\.\\d+)?$/.test(value) ? `${value}px` : value\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"file-upload.js","names":[],"sources":["../../../src/components/fields/file-upload.tsx"],"sourcesContent":["import type { FileUploadSchema, UploadedFile } from '@alfakit/core'\nimport {\n canEditImage,\n concurrencyLimiter,\n cssAspectRatio,\n editableImageName,\n fetchEditableImage,\n fieldMemory,\n fileManagerSize,\n filesFromClipboard,\n fileUploadAccept,\n fileUploadEntries,\n fileUploadPaths,\n insertUploadedFile,\n moveRepeaterItem,\n panelUploadOptions,\n requestJson,\n transformImage,\n uploadedFileLinks,\n uploadTemporaryFile,\n} from '@alfakit/core'\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n DownloadIcon,\n ExternalLinkIcon,\n FileIcon,\n PencilIcon,\n UploadCloudIcon,\n XIcon,\n} from 'lucide-react'\nimport { useRef, useState, type ClipboardEvent, type DragEvent } from 'react'\nimport { useChoice, useFormatter, usePanel, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cn } from '../../lib/utils'\nimport { useField, useFormStore } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Spinner } from '../ui/spinner'\nimport type { ImageEditRequest } from './image-editor-dialog'\nimport { ImageEditorDialog } from './image-editor-dialog-lazy'\n\ninterface LocalFile extends UploadedFile {\n progress?: number\n error?: string\n preview?: string\n}\n\nexport function FileUploadField({ component }: { component: FileUploadSchema }) {\n const t = useT()\n const formatter = useFormatter()\n const choice = useChoice()\n const panel = usePanel()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<\n string | string[] | null\n >(component)\n // Read-only keeps open/download links but hides upload and remove controls.\n const locked = disabled || readOnly\n const store = useFormStore()\n // In-flight uploads and the metadata of files uploaded in this session (the server schema\n // only knows about saved files) live in the form's field memory, not in component state:\n // a virtualised Repeater item unmounts when scrolled out of view and finds them again,\n // local image previews included, when it mounts back.\n const uploadingStore = fieldMemory<LocalFile[]>(\n store,\n `${component.statePath}:uploading`,\n () => [],\n )\n const uploadedStore = fieldMemory<FileUploadSchema['files']>(\n store,\n `${component.statePath}:uploaded`,\n () => [],\n )\n const uploading = useStore(uploadingStore)\n const uploaded = useStore(uploadedStore)\n const setUploading = uploadingStore.set\n const setUploaded = uploadedStore.set\n const [dragging, setDragging] = useState(false)\n // Previews whose image failed to load (missing or unreachable file) show the file icon.\n const [brokenPreviews, setBrokenPreviews] = useState<ReadonlySet<string>>(() => new Set())\n const inputRef = useRef<HTMLInputElement>(null)\n const [dragPath, setDragPath] = useState<string | null>(null)\n // The image open in the editor (`imageEditor()`), one at a time.\n const [editing, setEditing] = useState<ImageEditRequest | null>(null)\n const editImage = (file: File) =>\n new Promise<File | null>((resolve) =>\n setEditing({\n file,\n resolve: (edited) => {\n setEditing(null)\n resolve(edited)\n },\n }),\n )\n // Object URLs made for local image previews; released when the file is removed.\n const objectUrls = fieldMemory<Set<string>>(\n store,\n `${component.statePath}:previews`,\n () => new Set(),\n ).get()\n\n const releasePreview = (url: string | undefined) => {\n if (url && objectUrls.delete(url)) URL.revokeObjectURL(url)\n }\n\n const paths = fileUploadPaths(value)\n const files: LocalFile[] = fileUploadEntries(paths, component.files, uploaded)\n const maxFiles = component.multiple ? (component.maxFiles ?? Infinity) : 1\n const canAddMore = files.length + uploading.length < maxFiles\n\n const commit = (next: string[]) => setValue(component.multiple ? next : (next[0] ?? null))\n\n const upload = async (list: FileList | File[]) => {\n const endpoint = panel.endpoints.upload\n\n if (!endpoint) {\n return\n }\n\n const accepted = Array.from(list).slice(\n 0,\n Math.max(0, maxFiles - files.length - uploading.length),\n )\n\n // `maxParallelUploads()`: files are prepared (edited, resized) one by one and then\n // queued, so at most that many upload at once.\n const limit = concurrencyLimiter(component.maxParallelUploads ?? 1)\n const queued: Promise<void>[] = []\n\n for (const [position, picked] of accepted.entries()) {\n let original = picked\n const edit = Boolean(component.imageEditor && canEditImage(picked))\n\n if (edit) {\n const edited = await editImage(picked)\n\n // Cancelled: the file is not uploaded.\n if (!edited) continue\n\n original = edited\n }\n\n const file = await prepare(original, edit)\n\n if (!file) continue\n\n const local = pendingFile(file, position)\n setUploading((state) => [...state, local])\n\n queued.push(limit(() => send(endpoint, file, local)))\n }\n\n await Promise.all(queued)\n }\n\n /** Resize and orient a picked (or edited) file, and check its size. Null when rejected. */\n const prepare = async (original: File, edited: boolean): Promise<File | null> => {\n const file =\n component.imageCropAspectRatio ||\n component.imageResizeTargetWidth ||\n component.imageResizeTargetHeight ||\n component.orientImagesFromExif\n ? await transformImage(original, {\n // The editor already cropped the image.\n aspectRatio: edited ? null : component.imageCropAspectRatio,\n mode: component.imageResizeMode,\n width: component.imageResizeTargetWidth,\n height: component.imageResizeTargetHeight,\n orient: component.orientImagesFromExif,\n })\n : original\n\n if (component.minSize && file.size < component.minSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_small', {}, 'This file is too small.'),\n },\n ])\n\n return null\n }\n\n if (component.maxSize && file.size > component.maxSize * 1024) {\n setUploading((state) => [\n ...state,\n {\n path: file.name,\n name: file.name,\n size: file.size,\n error: t('form.file_too_large', {}, 'This file is too large.'),\n },\n ])\n\n return null\n }\n\n return file\n }\n\n /** The in-progress entry shown while `file` uploads. */\n const pendingFile = (file: File, position: number | string): LocalFile => {\n const local: LocalFile = {\n // Unique within the batch and across re-picks of a changed file.\n path: `pending:${position}:${file.lastModified}:${file.size}:${file.name}`,\n name: file.name,\n size: file.size,\n progress: 0,\n preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined,\n }\n if (local.preview) objectUrls.add(local.preview)\n\n return local\n }\n\n /**\n * The \"Edit\" button on an uploaded image: open it in the editor and upload the result\n * in its place (the old file goes like any removed one: a temporary upload is deleted\n * now, a stored file when the form is saved with `deleteRemovedFiles()`).\n */\n const editExisting = async (existing: LocalFile) => {\n const endpoint = panel.endpoints.upload\n const url = existing.url ?? existing.preview\n const name = editableImageName(existing)\n\n if (!endpoint || !url || !name) return\n\n const source = await fetchEditableImage(url, name)\n\n if (!source) {\n setUploading((state) => [\n ...state,\n {\n path: `failed:${existing.path}`,\n name: existing.name,\n error: t('image_editor.load_failed', {}, 'The image could not be opened.'),\n },\n ])\n\n return\n }\n\n const edited = await editImage(source)\n\n // Cancelled, or the editor couldn't decode it: keep the file as it is.\n if (!edited || edited === source) return\n\n const file = await prepare(edited, true)\n\n if (!file) return\n\n const local = pendingFile(file, `edit:${existing.path}`)\n setUploading((state) => [...state, local])\n await send(endpoint, file, local, existing.path)\n }\n\n const send = async (endpoint: string, file: File, local: LocalFile, replacing?: string) => {\n try {\n // Large files go in resumable chunks; progress covers the whole file.\n const response = await uploadTemporaryFile(endpoint, file, {\n ...panelUploadOptions(panel, component.uploadPolicy),\n onProgress: (progress) =>\n setUploading((state) =>\n state.map((item) => (item.path === local.path ? { ...item, progress } : item)),\n ),\n })\n\n setUploaded((state) => [\n ...state,\n {\n path: response.key,\n name: response.name,\n size: response.size,\n temporary: true,\n url: local.preview,\n },\n ])\n setUploading((state) => state.filter((item) => item.path !== local.path))\n // Read the store, not `value`: it's stale after the first file of a batch.\n const current = fileUploadPaths(\n store.getValue<string | string[] | null>(component.statePath, null),\n )\n\n if (replacing !== undefined && current.includes(replacing)) {\n // An edited image takes the old one's place.\n commit(current.map((path) => (path === replacing ? response.key : path)))\n discard(replacing)\n } else {\n commit(insertUploadedFile(current, response.key, component, maxFiles))\n }\n } catch (uploadError) {\n const message =\n (uploadError as { errors?: Record<string, string[]> }).errors?.file?.[0] ??\n (uploadError as Error).message\n setUploading((state) =>\n state.map((item) =>\n item.path === local.path ? { ...item, error: message, progress: undefined } : item,\n ),\n )\n }\n }\n\n const remove = (path: string) => {\n commit(paths.filter((item) => item !== path))\n discard(path)\n }\n\n /** Release a file that left the field: its preview, and a temporary upload's file. */\n const discard = (path: string) => {\n releasePreview(uploadedStore.get().find((file) => file.path === path)?.url)\n\n if (path.startsWith('tmp:') && panel.endpoints.uploadDestroy) {\n void requestJson(panel.endpoints.uploadDestroy, {\n method: 'DELETE',\n body: { key: path },\n }).catch(() => undefined)\n }\n }\n\n const onDrop = (event: DragEvent<HTMLDivElement>) => {\n event.preventDefault()\n setDragging(false)\n\n if (!locked && event.dataTransfer.files.length > 0) {\n void upload(event.dataTransfer.files)\n }\n }\n\n // `pasteable()`: images and files pasted while the drop zone has focus.\n const onPaste = (event: ClipboardEvent<HTMLDivElement>) => {\n if (locked || component.pasteable === false) return\n\n const pasted = filesFromClipboard(event.clipboardData)\n\n if (pasted.length > 0) {\n event.preventDefault()\n void upload(pasted)\n }\n }\n\n const move = (from: number, to: number) => commit(moveRepeaterItem(paths, from, to) as string[])\n const reorderable = Boolean(component.reorderable && component.multiple && !locked)\n\n // `previewable(false)` shows every file as a name and icon, without the image.\n const isImage = (file: LocalFile) =>\n component.previewable !== false &&\n (component.image || /\\.(png|jpe?g|gif|webp|svg|avif)$/i.test(file.name))\n const circle = component.panelLayout === 'circle' || component.avatar\n const integrated = component.panelLayout === 'integrated' && !circle\n const removeAtStart = component.positions?.removeButton === 'start'\n const progressAtStart = component.positions?.progressIndicator === 'start'\n const uploadAlign = component.positions?.uploadButton ?? 'center'\n\n // `imageEditor()`: uploaded JPEG, PNG and WebP images can be edited again.\n const canEditExisting = (file: LocalFile, index: number) =>\n Boolean(component.imageEditor) &&\n !locked &&\n index < files.length &&\n !file.missing &&\n Boolean(file.url ?? file.preview) &&\n editableImageName(file) !== null\n\n const renderRemove = (file: LocalFile) =>\n component.deletable &&\n !locked && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.remove', { name: file.name }, 'Remove :name')}\n onClick={() => {\n if (file.error || file.progress !== undefined) {\n releasePreview(file.preview)\n setUploading((state) => state.filter((item) => item.path !== file.path))\n } else {\n remove(file.path)\n }\n }}\n >\n <XIcon />\n </Button>\n )\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n data-slot=\"file-upload\"\n data-layout={component.panelLayout}\n className={cn('flex flex-col gap-3', integrated && 'rounded-md border p-2')}\n >\n {(files.length > 0 || uploading.length > 0) && (\n <ul\n className={cn(\n 'grid gap-2',\n component.panelLayout === 'grid' && 'grid-cols-2 sm:grid-cols-3 md:grid-cols-4',\n circle && 'justify-items-center',\n )}\n >\n {[...files, ...uploading].map((file, index) => (\n <li\n key={file.path}\n draggable={reorderable && index < files.length}\n onDragStart={() => setDragPath(file.path)}\n onDragOver={(event) => reorderable && event.preventDefault()}\n onDrop={(event) => {\n if (!reorderable || dragPath === null) return\n event.preventDefault()\n event.stopPropagation()\n const from = paths.indexOf(dragPath)\n if (from !== -1 && index < files.length) move(from, index)\n setDragPath(null)\n }}\n onDragEnd={() => setDragPath(null)}\n className={cn(\n dragPath === file.path && 'opacity-50',\n 'bg-muted/30 relative flex min-w-0 items-center gap-3 overflow-hidden rounded-md border p-2 text-sm',\n circle && 'size-28 justify-center rounded-full border-0 p-0',\n file.error && 'border-destructive',\n )}\n >\n {!circle && removeAtStart && renderRemove(file)}\n {!circle && progressAtStart && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {isImage(file) && (file.url || file.preview) && !brokenPreviews.has(file.path) ? (\n <img\n src={file.url ?? file.preview}\n alt=\"\"\n onError={() => setBrokenPreviews((current) => new Set(current).add(file.path))}\n className={cn(\n 'size-12 shrink-0 rounded object-cover',\n circle && 'size-28 rounded-full',\n )}\n style={\n !circle && component.imagePreviewHeight\n ? {\n height: cssLength(component.imagePreviewHeight),\n width: 'auto',\n maxWidth: '50%',\n }\n : undefined\n }\n />\n ) : (\n !circle && <FileIcon className=\"text-muted-foreground size-6 shrink-0\" />\n )}\n {!circle && (\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate font-medium\">{file.name}</p>\n <p\n className={cn(\n 'text-muted-foreground text-xs',\n file.missing && 'text-destructive',\n )}\n >\n {file.error ??\n (file.missing\n ? t('form.file_missing', {}, 'This file no longer exists.')\n : file.progress !== undefined && !file.error\n ? (component.uploadingMessage ?? t('form.uploading', {}, 'Uploading…'))\n : file.size\n ? fileManagerSize(file.size, formatter)\n : file.temporary\n ? t('form.uploading', {}, 'Pending')\n : '')}\n </p>\n </div>\n )}\n <div\n className={cn('flex items-center gap-1', circle && 'absolute -end-1 -bottom-1')}\n >\n {(circle || !progressAtStart) && file.progress !== undefined && !file.error && (\n <Spinner />\n )}\n {reorderable && index < files.length && (\n <>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('file_upload.move_up', { name: file.name }, 'Move :name up')}\n disabled={index === 0}\n onClick={() => move(index, index - 1)}\n >\n <ChevronUpIcon />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t(\n 'file_upload.move_down',\n { name: file.name },\n 'Move :name down',\n )}\n disabled={index === files.length - 1}\n onClick={() => move(index, index + 1)}\n >\n <ChevronDownIcon />\n </Button>\n </>\n )}\n {canEditExisting(file, index) && (\n <Button\n type=\"button\"\n variant={circle ? 'secondary' : 'ghost'}\n size=\"icon-xs\"\n className={cn(circle && 'rounded-full shadow')}\n aria-label={t('file_upload.edit', { name: file.name }, 'Edit :name')}\n title={t('image_editor.title', {}, 'Edit image')}\n data-action=\"edit-image\"\n onClick={() => void editExisting(file)}\n >\n <PencilIcon />\n </Button>\n )}\n {component.openable && uploadedFileLinks(file).open && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).open}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n aria-label={`${t('file_upload.open', { name: file.name }, 'Open :name')} ${t('opens_in_new_tab', {}, '(opens in a new tab)')}`}\n >\n <ExternalLinkIcon />\n </a>\n </Button>\n )}\n {component.downloadable && uploadedFileLinks(file).download && (\n <Button asChild type=\"button\" variant=\"ghost\" size=\"icon-xs\">\n <a\n href={uploadedFileLinks(file).download}\n download\n aria-label={t(\n 'file_upload.download',\n { name: file.name },\n 'Download :name',\n )}\n >\n <DownloadIcon />\n </a>\n </Button>\n )}\n {(circle || !removeAtStart) && renderRemove(file)}\n </div>\n </li>\n ))}\n </ul>\n )}\n {canAddMore && !locked && (\n <div\n role=\"button\"\n tabIndex={0}\n onClick={() => inputRef.current?.click()}\n aria-describedby={aria['aria-describedby']}\n onKeyDown={(event) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n inputRef.current?.click()\n }\n }}\n onDragOver={(event) => {\n event.preventDefault()\n setDragging(true)\n }}\n onDragLeave={() => setDragging(false)}\n onDrop={onDrop}\n onPaste={onPaste}\n style={\n !circle && component.panelAspectRatio\n ? { aspectRatio: cssAspectRatio(component.panelAspectRatio) }\n : undefined\n }\n data-align={uploadAlign}\n className={cn(\n 'text-muted-foreground hover:border-ring flex cursor-pointer flex-col justify-center gap-1 rounded-md border border-dashed px-4 py-6 text-sm transition-colors',\n uploadAlign === 'start'\n ? 'items-start text-start'\n : uploadAlign === 'end'\n ? 'items-end text-end'\n : 'items-center text-center',\n integrated && 'border-transparent',\n dragging && 'border-ring bg-accent',\n circle && 'size-28 items-center rounded-full py-0',\n )}\n >\n <UploadCloudIcon className=\"size-5\" />\n {!circle && (\n <span>\n {component.placeholder ??\n t('form.drop_files', {}, 'Drop files here or click to browse')}\n </span>\n )}\n {!circle &&\n (component.acceptedFileTypes || component.maxSize || component.minFiles) && (\n <span className=\"text-muted-foreground text-xs\">\n {[\n component.acceptedFileTypes?.join(', '),\n component.maxSize\n ? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}`\n : null,\n component.minFiles\n ? choice(\n 'form.min_files',\n component.minFiles,\n {},\n 'At least :count file|At least :count files',\n )\n : null,\n ]\n .filter(Boolean)\n .join(' · ')}\n </span>\n )}\n </div>\n )}\n <input\n ref={inputRef}\n id={id}\n type=\"file\"\n className=\"sr-only\"\n tabIndex={-1}\n {...aria}\n multiple={component.multiple}\n accept={fileUploadAccept(component)}\n disabled={locked}\n onChange={(event) => {\n // Copy the list: clearing the input empties it while the editor is open.\n const picked = event.target.files ? Array.from(event.target.files) : []\n event.target.value = ''\n void upload(picked)\n }}\n />\n {component.imageEditor && (\n <ImageEditorDialog request={editing} options={component.imageEditor} />\n )}\n </div>\n </FieldWrapper>\n )\n}\n\n/** `imagePreviewHeight()`: numbers are pixels, strings are CSS lengths (`'250px'`, `'10rem'`). */\nfunction cssLength(value: number | string): string {\n return typeof value === 'number' || /^\\d+(\\.\\d+)?$/.test(value) ? `${value}px` : value\n}\n"],"mappings":"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"}
@@ -1,5 +1,10 @@
1
1
  import { MoneyInputSchema } from "@alfakit/core";
2
2
  //#region src/components/fields/money-input.d.ts
3
+ /**
4
+ * Amount input for any ISO 4217 currency. Shows the symbol where the user's locale puts
5
+ * it, accepts the locale's separators and digits ("1.234,50", "١٢٫٥"), and rounds to
6
+ * the currency's precision on blur. The form value is a plain decimal string ("1234.50").
7
+ */
3
8
  export declare function MoneyInputField({ component }: {
4
9
  component: MoneyInputSchema;
5
10
  }): import("react").JSX.Element;
@@ -1 +1 @@
1
- {"version":3,"file":"money-input.d.ts","names":[],"sources":["../../../src/components/fields/money-input.tsx"],"mappings":";;wBAsBgB,kBAAkB;EAAe,WAAW;oBAAkB,IAAA"}
1
+ {"version":3,"file":"money-input.d.ts","names":[],"sources":["../../../src/components/fields/money-input.tsx"],"mappings":";;;;;;;wBAegB,kBAAkB;EAAe,WAAW;oBAAkB,IAAA"}
@@ -13,9 +13,6 @@ import { jsx, jsxs } from "react/jsx-runtime";
13
13
  * it, accepts the locale's separators and digits ("1.234,50", "١٢٫٥"), and rounds to
14
14
  * the currency's precision on blur. The form value is a plain decimal string ("1234.50").
15
15
  */
16
- function clampAmount(amount, min, max) {
17
- return Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min ?? Number.NEGATIVE_INFINITY, amount));
18
- }
19
16
  function MoneyInputField({ component }) {
20
17
  const { value, setValue, blur, error, id, disabled, aria } = useField(component);
21
18
  const format = useFormatter();
@@ -32,7 +29,7 @@ function MoneyInputField({ component }) {
32
29
  return;
33
30
  }
34
31
  const parsed = format.parseNumber(text);
35
- setValue(parsed === null ? text : clampAmount(parsed, component.min, component.max).toFixed(component.decimals));
32
+ setValue(parsed === null ? text : parsed.toFixed(component.decimals));
36
33
  };
37
34
  const affix = (text, icon, side) => text || icon ? /* @__PURE__ */ jsxs("span", {
38
35
  className: cn("text-muted-foreground bg-muted/50 flex items-center gap-1 px-3 text-sm whitespace-nowrap", side === "start" ? "border-e" : "border-s"),
@@ -86,6 +83,6 @@ function MoneyInputField({ component }) {
86
83
  });
87
84
  }
88
85
  //#endregion
89
- export { MoneyInputField, clampAmount };
86
+ export { MoneyInputField };
90
87
 
91
88
  //# sourceMappingURL=money-input.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"money-input.js","names":[],"sources":["../../../src/components/fields/money-input.tsx"],"sourcesContent":["import type { MoneyInputSchema } from '@alfakit/core'\nimport { useState } from 'react'\nimport { useFormatter } from '../../context'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Input } from '../ui/input'\n\n/**\n * Amount input for any ISO 4217 currency. Shows the symbol where the user's locale puts\n * it, accepts the locale's separators and digits (\"1.234,50\", \"١٢٫٥\"), and rounds to\n * the currency's precision on blur. The form value is a plain decimal string (\"1234.50\").\n */\nexport function clampAmount(amount: number, min?: number | null, max?: number | null): number {\n const lower = min ?? Number.NEGATIVE_INFINITY\n const upper = max ?? Number.POSITIVE_INFINITY\n\n return Math.min(upper, Math.max(lower, amount))\n}\n\nexport function MoneyInputField({ component }: { component: MoneyInputSchema }) {\n const { value, setValue, blur, error, id, disabled, aria } = useField<string | number | null>(\n component,\n )\n const format = useFormatter()\n const [draft, setDraft] = useState<string | null>(null)\n\n const { prefix, suffix } = format.currencyAffixes(component.currency)\n const amount = value === null || value === undefined || value === '' ? null : Number(value)\n const display =\n amount === null || Number.isNaN(amount)\n ? String(value ?? '')\n : format.number(amount, {\n minimumFractionDigits: component.decimals,\n maximumFractionDigits: component.decimals,\n })\n\n const commit = (text: string) => {\n if (text.trim() === '') {\n setValue(null)\n\n return\n }\n\n const parsed = format.parseNumber(text)\n\n // Unparseable input is kept as typed so server validation can explain the problem;\n // amounts are brought inside `minValue()` / `maxValue()`.\n setValue(\n parsed === null\n ? text\n : clampAmount(parsed, component.min, component.max).toFixed(component.decimals),\n )\n }\n\n const affix = (text: string, icon: string | undefined, side: 'start' | 'end') =>\n text || icon ? (\n <span\n className={cn(\n 'text-muted-foreground bg-muted/50 flex items-center gap-1 px-3 text-sm whitespace-nowrap',\n side === 'start' ? 'border-e' : 'border-s',\n )}\n >\n {side === 'start' && <Icon name={icon} />}\n {text}\n {side === 'end' && <Icon name={icon} />}\n </span>\n ) : null\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n data-slot=\"input-group\"\n className={cn(\n 'border-input dark:bg-input/30 has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-ring/50 flex h-9 w-full items-stretch overflow-hidden rounded-md border shadow-xs has-[input:focus-visible]:ring-[3px]',\n error && 'border-destructive',\n disabled && 'opacity-50',\n )}\n >\n {affix([component.prefix, prefix].filter(Boolean).join(' '), component.prefixIcon, 'start')}\n <Input\n id={id}\n name={component.name}\n type=\"text\"\n inputMode={component.decimals > 0 ? 'decimal' : 'numeric'}\n dir=\"ltr\"\n value={draft ?? display}\n onFocus={() =>\n setDraft(\n amount === null || Number.isNaN(amount)\n ? String(value ?? '')\n : format.number(amount, {\n useGrouping: false,\n maximumFractionDigits: component.decimals,\n }),\n )\n }\n onChange={(event) => {\n setDraft(event.target.value)\n const parsed = format.parseNumber(event.target.value)\n setValue(\n event.target.value.trim() === ''\n ? null\n : parsed === null\n ? event.target.value\n : String(parsed),\n )\n }}\n onBlur={(event) => {\n commit(event.target.value)\n setDraft(null)\n blur()\n }}\n placeholder={component.placeholder}\n disabled={disabled}\n readOnly={component.readOnly}\n required={component.required}\n autoFocus={component.autofocus}\n {...aria}\n\n {...extraProps(\n component.extraInputAttributes,\n 'h-full border-0 bg-transparent text-end tabular-nums shadow-none focus-visible:ring-0 dark:bg-transparent',\n )}\n />\n {affix([suffix, component.suffix].filter(Boolean).join(' '), component.suffixIcon, 'end')}\n </div>\n </FieldWrapper>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,YAAY,QAAgB,KAAqB,KAA6B;CAI5F,OAAO,KAAK,IAFE,OAAO,OAAO,mBAEL,KAAK,IAHd,OAAO,OAAO,mBAGW,MAAM,CAAC;AAChD;AAEA,SAAgB,gBAAgB,EAAE,aAA8C;CAC9E,MAAM,EAAE,OAAO,UAAU,MAAM,OAAO,IAAI,UAAU,SAAS,SAC3D,SACF;CACA,MAAM,SAAS,aAAa;CAC5B,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CAEtD,MAAM,EAAE,QAAQ,WAAW,OAAO,gBAAgB,UAAU,QAAQ;CACpE,MAAM,SAAS,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,KAAK,OAAO,OAAO,KAAK;CAC1F,MAAM,UACJ,WAAW,QAAQ,OAAO,MAAM,MAAM,IAClC,OAAO,SAAS,EAAE,IAClB,OAAO,OAAO,QAAQ;EACpB,uBAAuB,UAAU;EACjC,uBAAuB,UAAU;CACnC,CAAC;CAEP,MAAM,UAAU,SAAiB;EAC/B,IAAI,KAAK,KAAK,MAAM,IAAI;GACtB,SAAS,IAAI;GAEb;EACF;EAEA,MAAM,SAAS,OAAO,YAAY,IAAI;EAItC,SACE,WAAW,OACP,OACA,YAAY,QAAQ,UAAU,KAAK,UAAU,GAAG,CAAC,CAAC,QAAQ,UAAU,QAAQ,CAClF;CACF;CAEA,MAAM,SAAS,MAAc,MAA0B,SACrD,QAAQ,OACN,qBAAC,QAAD;EACE,WAAW,GACT,4FACA,SAAS,UAAU,aAAa,UAClC;EAJF,UAAA;GAMG,SAAS,WAAW,oBAAC,MAAD,EAAM,MAAM,KAAO,CAAA;GACvC;GACA,SAAS,SAAS,oBAAC,MAAD,EAAM,MAAM,KAAO,CAAA;EAClC;CACJ,CAAA,IAAA;CAEN,OACE,oBAAC,cAAD;EAAc,OAAO;EAAW,SAAS;EAAW;EAClD,UAAA,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,GACT,6NACA,SAAS,sBACT,YAAY,YACd;GANF,UAAA;IAQG,MAAM,CAAC,UAAU,QAAQ,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,GAAG,UAAU,YAAY,OAAO;IAC1F,oBAAC,OAAD;KACM;KACJ,MAAM,UAAU;KAChB,MAAK;KACL,WAAW,UAAU,WAAW,IAAI,YAAY;KAChD,KAAI;KACJ,OAAO,SAAS;KAChB,eACE,SACE,WAAW,QAAQ,OAAO,MAAM,MAAM,IAClC,OAAO,SAAS,EAAE,IAClB,OAAO,OAAO,QAAQ;MACpB,aAAa;MACb,uBAAuB,UAAU;KACnC,CAAC,CACP;KAEF,WAAW,UAAU;MACnB,SAAS,MAAM,OAAO,KAAK;MAC3B,MAAM,SAAS,OAAO,YAAY,MAAM,OAAO,KAAK;MACpD,SACE,MAAM,OAAO,MAAM,KAAK,MAAM,KAC1B,OACA,WAAW,OACT,MAAM,OAAO,QACb,OAAO,MAAM,CACrB;KACF;KACA,SAAS,UAAU;MACjB,OAAO,MAAM,OAAO,KAAK;MACzB,SAAS,IAAI;MACb,KAAK;KACP;KACA,aAAa,UAAU;KACb;KACV,UAAU,UAAU;KACpB,UAAU,UAAU;KACpB,WAAW,UAAU;KACrB,GAAI;KAEJ,GAAI,WACF,UAAU,sBACV,2GACF;IACD,CAAA;IACA,MAAM,CAAC,QAAQ,UAAU,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,GAAG,UAAU,YAAY,KAAK;GACrF;;CACO,CAAA;AAElB"}
1
+ {"version":3,"file":"money-input.js","names":[],"sources":["../../../src/components/fields/money-input.tsx"],"sourcesContent":["import type { MoneyInputSchema } from '@alfakit/core'\nimport { useState } from 'react'\nimport { useFormatter } from '../../context'\nimport { extraProps } from '../../lib/attributes'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Input } from '../ui/input'\n\n/**\n * Amount input for any ISO 4217 currency. Shows the symbol where the user's locale puts\n * it, accepts the locale's separators and digits (\"1.234,50\", \"١٢٫٥\"), and rounds to\n * the currency's precision on blur. The form value is a plain decimal string (\"1234.50\").\n */\nexport function MoneyInputField({ component }: { component: MoneyInputSchema }) {\n const { value, setValue, blur, error, id, disabled, aria } = useField<string | number | null>(\n component,\n )\n const format = useFormatter()\n const [draft, setDraft] = useState<string | null>(null)\n\n const { prefix, suffix } = format.currencyAffixes(component.currency)\n const amount = value === null || value === undefined || value === '' ? null : Number(value)\n const display =\n amount === null || Number.isNaN(amount)\n ? String(value ?? '')\n : format.number(amount, {\n minimumFractionDigits: component.decimals,\n maximumFractionDigits: component.decimals,\n })\n\n const commit = (text: string) => {\n if (text.trim() === '') {\n setValue(null)\n\n return\n }\n\n const parsed = format.parseNumber(text)\n\n // Unparseable input is kept as typed and out-of-range amounts are kept as well, so the\n // server's `minValue()` / `maxValue()` rules report them (W-V3-11).\n setValue(parsed === null ? text : parsed.toFixed(component.decimals))\n }\n\n const affix = (text: string, icon: string | undefined, side: 'start' | 'end') =>\n text || icon ? (\n <span\n className={cn(\n 'text-muted-foreground bg-muted/50 flex items-center gap-1 px-3 text-sm whitespace-nowrap',\n side === 'start' ? 'border-e' : 'border-s',\n )}\n >\n {side === 'start' && <Icon name={icon} />}\n {text}\n {side === 'end' && <Icon name={icon} />}\n </span>\n ) : null\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n data-slot=\"input-group\"\n className={cn(\n 'border-input dark:bg-input/30 has-[input:focus-visible]:border-ring has-[input:focus-visible]:ring-ring/50 flex h-9 w-full items-stretch overflow-hidden rounded-md border shadow-xs has-[input:focus-visible]:ring-[3px]',\n error && 'border-destructive',\n disabled && 'opacity-50',\n )}\n >\n {affix([component.prefix, prefix].filter(Boolean).join(' '), component.prefixIcon, 'start')}\n <Input\n id={id}\n name={component.name}\n type=\"text\"\n inputMode={component.decimals > 0 ? 'decimal' : 'numeric'}\n dir=\"ltr\"\n value={draft ?? display}\n onFocus={() =>\n setDraft(\n amount === null || Number.isNaN(amount)\n ? String(value ?? '')\n : format.number(amount, {\n useGrouping: false,\n maximumFractionDigits: component.decimals,\n }),\n )\n }\n onChange={(event) => {\n setDraft(event.target.value)\n const parsed = format.parseNumber(event.target.value)\n setValue(\n event.target.value.trim() === ''\n ? null\n : parsed === null\n ? event.target.value\n : String(parsed),\n )\n }}\n onBlur={(event) => {\n commit(event.target.value)\n setDraft(null)\n blur()\n }}\n placeholder={component.placeholder}\n disabled={disabled}\n readOnly={component.readOnly}\n required={component.required}\n autoFocus={component.autofocus}\n {...aria}\n\n {...extraProps(\n component.extraInputAttributes,\n 'h-full border-0 bg-transparent text-end tabular-nums shadow-none focus-visible:ring-0 dark:bg-transparent',\n )}\n />\n {affix([suffix, component.suffix].filter(Boolean).join(' '), component.suffixIcon, 'end')}\n </div>\n </FieldWrapper>\n )\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,SAAgB,gBAAgB,EAAE,aAA8C;CAC9E,MAAM,EAAE,OAAO,UAAU,MAAM,OAAO,IAAI,UAAU,SAAS,SAC3D,SACF;CACA,MAAM,SAAS,aAAa;CAC5B,MAAM,CAAC,OAAO,YAAY,SAAwB,IAAI;CAEtD,MAAM,EAAE,QAAQ,WAAW,OAAO,gBAAgB,UAAU,QAAQ;CACpE,MAAM,SAAS,UAAU,QAAQ,UAAU,KAAA,KAAa,UAAU,KAAK,OAAO,OAAO,KAAK;CAC1F,MAAM,UACJ,WAAW,QAAQ,OAAO,MAAM,MAAM,IAClC,OAAO,SAAS,EAAE,IAClB,OAAO,OAAO,QAAQ;EACpB,uBAAuB,UAAU;EACjC,uBAAuB,UAAU;CACnC,CAAC;CAEP,MAAM,UAAU,SAAiB;EAC/B,IAAI,KAAK,KAAK,MAAM,IAAI;GACtB,SAAS,IAAI;GAEb;EACF;EAEA,MAAM,SAAS,OAAO,YAAY,IAAI;EAItC,SAAS,WAAW,OAAO,OAAO,OAAO,QAAQ,UAAU,QAAQ,CAAC;CACtE;CAEA,MAAM,SAAS,MAAc,MAA0B,SACrD,QAAQ,OACN,qBAAC,QAAD;EACE,WAAW,GACT,4FACA,SAAS,UAAU,aAAa,UAClC;EAJF,UAAA;GAMG,SAAS,WAAW,oBAAC,MAAD,EAAM,MAAM,KAAO,CAAA;GACvC;GACA,SAAS,SAAS,oBAAC,MAAD,EAAM,MAAM,KAAO,CAAA;EAClC;CACJ,CAAA,IAAA;CAEN,OACE,oBAAC,cAAD;EAAc,OAAO;EAAW,SAAS;EAAW;EAClD,UAAA,qBAAC,OAAD;GACE,aAAU;GACV,WAAW,GACT,6NACA,SAAS,sBACT,YAAY,YACd;GANF,UAAA;IAQG,MAAM,CAAC,UAAU,QAAQ,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,GAAG,UAAU,YAAY,OAAO;IAC1F,oBAAC,OAAD;KACM;KACJ,MAAM,UAAU;KAChB,MAAK;KACL,WAAW,UAAU,WAAW,IAAI,YAAY;KAChD,KAAI;KACJ,OAAO,SAAS;KAChB,eACE,SACE,WAAW,QAAQ,OAAO,MAAM,MAAM,IAClC,OAAO,SAAS,EAAE,IAClB,OAAO,OAAO,QAAQ;MACpB,aAAa;MACb,uBAAuB,UAAU;KACnC,CAAC,CACP;KAEF,WAAW,UAAU;MACnB,SAAS,MAAM,OAAO,KAAK;MAC3B,MAAM,SAAS,OAAO,YAAY,MAAM,OAAO,KAAK;MACpD,SACE,MAAM,OAAO,MAAM,KAAK,MAAM,KAC1B,OACA,WAAW,OACT,MAAM,OAAO,QACb,OAAO,MAAM,CACrB;KACF;KACA,SAAS,UAAU;MACjB,OAAO,MAAM,OAAO,KAAK;MACzB,SAAS,IAAI;MACb,KAAK;KACP;KACA,aAAa,UAAU;KACb;KACV,UAAU,UAAU;KACpB,UAAU,UAAU;KACpB,WAAW,UAAU;KACrB,GAAI;KAEJ,GAAI,WACF,UAAU,sBACV,2GACF;IACD,CAAA;IACA,MAAM,CAAC,QAAQ,UAAU,MAAM,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,GAAG,GAAG,UAAU,YAAY,KAAK;GACrF;;CACO,CAAA;AAElB"}
@@ -1 +1 @@
1
- {"version":3,"file":"repeater.d.ts","names":[],"sources":["../../../src/components/fields/repeater.tsx"],"mappings":";;;;;;;;;;wBAiDgB,gBAAgB;EAAe,WAAW;oBAAgB,IAAA"}
1
+ {"version":3,"file":"repeater.d.ts","names":[],"sources":["../../../src/components/fields/repeater.tsx"],"mappings":";;;;;;;;;;wBAkDgB,gBAAgB;EAAe,WAAW;oBAAgB,IAAA"}
@@ -10,7 +10,7 @@ import { FieldWrapper } from "../schema/field-wrapper.js";
10
10
  import { Collapsible, CollapsibleContent } from "../ui/collapsible.js";
11
11
  import { AddActionRow, CollapseAllActions, ItemActionButton, ItemHeaderActions, useItemActionFallbacks, useItemActions, useItemMoves, useRunItemAction } from "./item-actions.js";
12
12
  import { RepeaterTable } from "./repeater-table.js";
13
- import { VIRTUAL_ESTIMATES, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, fieldIds, gridColumnsClasses, isDeferredPlaceholder, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, reorderedItemKeys, templateForIndex, virtualizeOptions } from "@alfakit/core";
13
+ import { VIRTUAL_ESTIMATES, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, fieldIds, gridColumnsClasses, isDeferredPlaceholder, itemExtraActions, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, reorderedItemKeys, templateForIndex, virtualizeOptions } from "@alfakit/core";
14
14
  import { useState } from "react";
15
15
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
16
16
  //#region src/components/fields/repeater.tsx
@@ -91,7 +91,7 @@ function RepeaterFieldContent({ component }) {
91
91
  virtual,
92
92
  context,
93
93
  resolve: (name, index) => resolve(name, component.items[index]),
94
- extraActionsFor: (index) => component.items[index]?.actions
94
+ extraActionsFor: (index) => itemExtraActions(component, component.items[index])
95
95
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [component.collapsible && !component.simple && items.length > 0 && /* @__PURE__ */ jsx(CollapseAllActions, {
96
96
  context,
97
97
  resolve: (name) => resolve(name),
@@ -150,7 +150,7 @@ function RepeaterFieldContent({ component }) {
150
150
  index,
151
151
  count: items.length,
152
152
  resolve: (name) => resolve(name, serverItem),
153
- extraActions: locked ? void 0 : serverItem?.actions,
153
+ extraActions: locked ? void 0 : itemExtraActions(component, serverItem),
154
154
  buttons,
155
155
  canClone: Boolean(component.cloneable) && canAdd,
156
156
  canDelete,
@@ -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 }