@alfakit/react 0.1.1 → 0.1.6

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 (219) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/components/actions/action-button.d.ts.map +1 -1
  3. package/dist/components/actions/action-button.js +5 -4
  4. package/dist/components/actions/action-button.js.map +1 -1
  5. package/dist/components/actions/action-modals.d.ts.map +1 -1
  6. package/dist/components/actions/action-modals.js +6 -5
  7. package/dist/components/actions/action-modals.js.map +1 -1
  8. package/dist/components/ai/copilot.d.ts +3 -1
  9. package/dist/components/ai/copilot.d.ts.map +1 -1
  10. package/dist/components/ai/copilot.js +4 -1
  11. package/dist/components/ai/copilot.js.map +1 -1
  12. package/dist/components/cms/live-preview.js +3 -2
  13. package/dist/components/cms/live-preview.js.map +1 -1
  14. package/dist/components/cms/page.js +1 -1
  15. package/dist/components/fields/builder.js +15 -6
  16. package/dist/components/fields/builder.js.map +1 -1
  17. package/dist/components/fields/color-picker.js +1 -0
  18. package/dist/components/fields/color-picker.js.map +1 -1
  19. package/dist/components/fields/file-upload.js +11 -10
  20. package/dist/components/fields/file-upload.js.map +1 -1
  21. package/dist/components/fields/image-editor-dialog.js +4 -2
  22. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  23. package/dist/components/fields/item-actions.js +10 -10
  24. package/dist/components/fields/item-actions.js.map +1 -1
  25. package/dist/components/fields/key-value.d.ts.map +1 -1
  26. package/dist/components/fields/key-value.js +3 -3
  27. package/dist/components/fields/key-value.js.map +1 -1
  28. package/dist/components/fields/modal-table-select.js +6 -5
  29. package/dist/components/fields/modal-table-select.js.map +1 -1
  30. package/dist/components/fields/one-time-code-input.js +1 -1
  31. package/dist/components/fields/one-time-code-input.js.map +1 -1
  32. package/dist/components/fields/repeater-table.js +12 -1
  33. package/dist/components/fields/repeater-table.js.map +1 -1
  34. package/dist/components/fields/repeater.js +2 -2
  35. package/dist/components/fields/repeater.js.map +1 -1
  36. package/dist/components/fields/rich-editor.js +2 -1
  37. package/dist/components/fields/rich-editor.js.map +1 -1
  38. package/dist/components/fields/select.js +3 -3
  39. package/dist/components/fields/select.js.map +1 -1
  40. package/dist/components/file-manager/file-manager.js +1 -1
  41. package/dist/components/file-manager/file-manager.js.map +1 -1
  42. package/dist/components/form/form-block.d.ts.map +1 -1
  43. package/dist/components/form/form-block.js +14 -2
  44. package/dist/components/form/form-block.js.map +1 -1
  45. package/dist/components/form/form-context.js +7 -3
  46. package/dist/components/form/form-context.js.map +1 -1
  47. package/dist/components/kanban/kanban.js +4 -3
  48. package/dist/components/kanban/kanban.js.map +1 -1
  49. package/dist/components/layout/fused-group.js +1 -0
  50. package/dist/components/layout/fused-group.js.map +1 -1
  51. package/dist/components/layout/section.js +2 -2
  52. package/dist/components/layout/section.js.map +1 -1
  53. package/dist/components/layout/tabs.d.ts.map +1 -1
  54. package/dist/components/layout/tabs.js +54 -15
  55. package/dist/components/layout/tabs.js.map +1 -1
  56. package/dist/components/panel/app-sidebar.d.ts +2 -3
  57. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  58. package/dist/components/panel/app-sidebar.js +1 -24
  59. package/dist/components/panel/app-sidebar.js.map +1 -1
  60. package/dist/components/panel/brand.d.ts +10 -0
  61. package/dist/components/panel/brand.d.ts.map +1 -0
  62. package/dist/components/panel/brand.js +37 -0
  63. package/dist/components/panel/brand.js.map +1 -0
  64. package/dist/components/panel/global-search.js +1 -0
  65. package/dist/components/panel/global-search.js.map +1 -1
  66. package/dist/components/panel/index.d.ts +2 -1
  67. package/dist/components/panel/shortcut-keys.js +1 -0
  68. package/dist/components/panel/shortcut-keys.js.map +1 -1
  69. package/dist/components/panel/tenant-menu.js +2 -2
  70. package/dist/components/panel/tenant-menu.js.map +1 -1
  71. package/dist/components/panel/topbar.js +1 -1
  72. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  73. package/dist/components/realtime/live-records-pill.js +6 -3
  74. package/dist/components/realtime/live-records-pill.js.map +1 -1
  75. package/dist/components/table/cells.d.ts +6 -0
  76. package/dist/components/table/cells.d.ts.map +1 -1
  77. package/dist/components/table/cells.js +15 -3
  78. package/dist/components/table/cells.js.map +1 -1
  79. package/dist/components/table/column-manager.js +8 -7
  80. package/dist/components/table/column-manager.js.map +1 -1
  81. package/dist/components/table/content-layout.js +2 -1
  82. package/dist/components/table/content-layout.js.map +1 -1
  83. package/dist/components/table/grouping.js +11 -1
  84. package/dist/components/table/grouping.js.map +1 -1
  85. package/dist/components/table/pagination.js +16 -13
  86. package/dist/components/table/pagination.js.map +1 -1
  87. package/dist/components/table/table-block.js +14 -4
  88. package/dist/components/table/table-block.js.map +1 -1
  89. package/dist/components/ui/dialog.d.ts.map +1 -1
  90. package/dist/components/ui/dialog.js +3 -1
  91. package/dist/components/ui/dialog.js.map +1 -1
  92. package/dist/components/ui/popover.d.ts.map +1 -1
  93. package/dist/components/ui/popover.js +3 -1
  94. package/dist/components/ui/popover.js.map +1 -1
  95. package/dist/components/ui/sheet.d.ts.map +1 -1
  96. package/dist/components/ui/sheet.js +3 -1
  97. package/dist/components/ui/sheet.js.map +1 -1
  98. package/dist/components/ui/tooltip.js +3 -1
  99. package/dist/components/ui/tooltip.js.map +1 -1
  100. package/dist/components.css +347 -0
  101. package/dist/context.d.ts +5 -0
  102. package/dist/context.d.ts.map +1 -1
  103. package/dist/context.js +5 -1
  104. package/dist/context.js.map +1 -1
  105. package/dist/create-alfakit.d.ts.map +1 -1
  106. package/dist/create-alfakit.js +8 -5
  107. package/dist/create-alfakit.js.map +1 -1
  108. package/dist/eject-manifest.json +16 -3
  109. package/dist/index.d.ts +6 -3
  110. package/dist/index.js +24 -21
  111. package/dist/layouts/app-layout.d.ts.map +1 -1
  112. package/dist/layouts/app-layout.js +2 -29
  113. package/dist/layouts/app-layout.js.map +1 -1
  114. package/dist/layouts/auth-layout.js +2 -2
  115. package/dist/layouts/auth-layout.js.map +1 -1
  116. package/dist/layouts/fullscreen-layout.js +1 -1
  117. package/dist/layouts/fullscreen-layout.js.map +1 -1
  118. package/dist/layouts/lazy-layouts.js +15 -0
  119. package/dist/layouts/lazy-layouts.js.map +1 -0
  120. package/dist/layouts/max-widths.js +34 -0
  121. package/dist/layouts/max-widths.js.map +1 -0
  122. package/dist/layouts/panel-layouts.d.ts +10 -0
  123. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  124. package/dist/layouts/panel-layouts.js +20 -0
  125. package/dist/layouts/panel-layouts.js.map +1 -0
  126. package/dist/layouts/top-nav-layout.js +2 -2
  127. package/dist/layouts/top-nav-layout.js.map +1 -1
  128. package/dist/pages/page.d.ts.map +1 -1
  129. package/dist/pages/page.js +12 -7
  130. package/dist/pages/page.js.map +1 -1
  131. package/dist/standalone/alfakit-component.js +11 -6
  132. package/dist/standalone/alfakit-component.js.map +1 -1
  133. package/dist/standalone/alfakit-toaster.d.ts.map +1 -1
  134. package/dist/standalone/alfakit-toaster.js +8 -1
  135. package/dist/standalone/alfakit-toaster.js.map +1 -1
  136. package/dist/standalone.css +54 -0
  137. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  138. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  139. package/dist/starter-kit/dashboard-widgets.js +53 -0
  140. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  141. package/dist/starter-kit/impersonation-banner.js +34 -0
  142. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  143. package/dist/starter-kit/index.d.ts +3 -2
  144. package/dist/starter-kit/index.js +3 -2
  145. package/dist/starter-kit/starter-kit.d.ts +6 -0
  146. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  147. package/dist/starter-kit/starter-kit.js +18 -7
  148. package/dist/starter-kit/starter-kit.js.map +1 -1
  149. package/dist/starter-kit/widgets-loaded.js +21 -5
  150. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  151. package/dist/starter-kit/widgets-triggers.js +35 -3
  152. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  153. package/dist/starter-kit/widgets.d.ts +13 -5
  154. package/dist/starter-kit/widgets.d.ts.map +1 -1
  155. package/dist/starter-kit/widgets.js +56 -18
  156. package/dist/starter-kit/widgets.js.map +1 -1
  157. package/dist/styles.css +15 -2
  158. package/dist/themes.css +8 -335
  159. package/package.json +2 -2
  160. package/src/components/actions/action-button.tsx +7 -4
  161. package/src/components/actions/action-modals.tsx +13 -1
  162. package/src/components/ai/copilot.tsx +14 -1
  163. package/src/components/cms/live-preview.tsx +5 -2
  164. package/src/components/fields/builder.tsx +14 -5
  165. package/src/components/fields/color-picker.tsx +1 -0
  166. package/src/components/fields/file-upload.tsx +21 -10
  167. package/src/components/fields/image-editor-dialog.tsx +7 -2
  168. package/src/components/fields/item-actions.tsx +32 -9
  169. package/src/components/fields/key-value.tsx +3 -2
  170. package/src/components/fields/modal-table-select.tsx +8 -3
  171. package/src/components/fields/one-time-code-input.tsx +1 -1
  172. package/src/components/fields/repeater-table.tsx +12 -1
  173. package/src/components/fields/repeater.tsx +2 -3
  174. package/src/components/fields/rich-editor.tsx +3 -0
  175. package/src/components/fields/select.tsx +7 -3
  176. package/src/components/file-manager/file-manager.tsx +1 -1
  177. package/src/components/form/form-block.tsx +26 -1
  178. package/src/components/form/form-context.tsx +3 -6
  179. package/src/components/kanban/kanban.tsx +16 -3
  180. package/src/components/layout/fused-group.tsx +5 -1
  181. package/src/components/layout/section.tsx +2 -2
  182. package/src/components/layout/tabs.tsx +72 -17
  183. package/src/components/panel/app-sidebar.tsx +2 -38
  184. package/src/components/panel/brand.tsx +46 -0
  185. package/src/components/panel/global-search.tsx +1 -1
  186. package/src/components/panel/shortcut-keys.tsx +4 -1
  187. package/src/components/panel/tenant-menu.tsx +2 -2
  188. package/src/components/realtime/live-records-pill.tsx +4 -3
  189. package/src/components/table/cells.tsx +27 -5
  190. package/src/components/table/column-manager.tsx +12 -10
  191. package/src/components/table/content-layout.tsx +5 -1
  192. package/src/components/table/grouping.tsx +15 -1
  193. package/src/components/table/pagination.tsx +16 -9
  194. package/src/components/table/table-block.tsx +25 -12
  195. package/src/components/ui/dialog.tsx +5 -1
  196. package/src/components/ui/popover.tsx +5 -1
  197. package/src/components/ui/sheet.tsx +5 -1
  198. package/src/components/ui/tooltip.tsx +5 -1
  199. package/src/context.tsx +10 -0
  200. package/src/create-alfakit.tsx +11 -2
  201. package/src/index.ts +2 -0
  202. package/src/layouts/app-layout.tsx +4 -28
  203. package/src/layouts/auth-layout.tsx +2 -2
  204. package/src/layouts/fullscreen-layout.tsx +1 -1
  205. package/src/layouts/lazy-layouts.ts +14 -0
  206. package/src/layouts/max-widths.ts +29 -0
  207. package/src/layouts/panel-layouts.ts +17 -0
  208. package/src/layouts/top-nav-layout.tsx +2 -2
  209. package/src/pages/page.tsx +21 -12
  210. package/src/standalone/alfakit-component.tsx +18 -7
  211. package/src/standalone/alfakit-toaster.tsx +21 -2
  212. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  213. package/src/starter-kit/impersonation-banner.tsx +48 -0
  214. package/src/starter-kit/index.ts +2 -0
  215. package/src/starter-kit/starter-kit.tsx +31 -4
  216. package/src/starter-kit/widgets-loaded.tsx +23 -1
  217. package/src/starter-kit/widgets-triggers.tsx +37 -2
  218. package/src/starter-kit/widgets.tsx +95 -20
  219. package/src/styles.css +15 -2
@@ -1 +1 @@
1
- {"version":3,"file":"builder.js","names":[],"sources":["../../../src/components/fields/builder.tsx"],"sourcesContent":["import type {\n ActionSchema,\n BuilderBlockSchema,\n BuilderItemState,\n BuilderSchema,\n ItemActionName,\n ItemActionSchema,\n} from '@alfakit/core'\nimport {\n builderItemLabel,\n builderItems,\n builderItemSchema,\n canAddBuilderBlock,\n cloneIndexedState,\n cloneRepeaterItem,\n fieldIds,\n insertBuilderItem,\n isDeferredPlaceholder,\n itemActionContext,\n moveIndexedState,\n moveRepeaterItem,\n removeIndexedState,\n removeRepeaterItem,\n reorderedItemKeys,\n VIRTUAL_ESTIMATES,\n virtualizeOptions,\n} from '@alfakit/core'\nimport { EllipsisIcon, PencilIcon } from 'lucide-react'\nimport { createElement, useState, type KeyboardEvent } from 'react'\nimport { useActionRunner, useFormatter, useOptionalRegistry, useT } from '../../context'\nimport { useScrollToErroredItem, useVirtualList } from '../../hooks/use-virtual-list'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField, useOptionalFormStore } 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 { Button } from '../ui/button'\nimport { Collapsible, CollapsibleContent } from '../ui/collapsible'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { BuilderBlockPicker, BuilderBlockPickerPanel } from './builder-picker'\nimport {\n AddActionRow,\n CollapseAllActions,\n ItemHeaderActions,\n useItemActionFallbacks,\n useItemActions,\n useItemMoves,\n useRunItemAction,\n} from './item-actions'\n\n/**\n * A list of blocks, each with its own schema. Items use the server's per-item schema\n * when it describes the same block, else the block's template (fresh, moved or cloned\n * items the server has not seen yet).\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 BuilderField({ component }: { component: BuilderSchema }) {\n if (!isDeferredPlaceholder(component)) return <BuilderFieldContent component={component} />\n\n return (\n <DeferredSchema component={component}>\n {(loaded) => <BuilderFieldContent component={loaded} />}\n </DeferredSchema>\n )\n}\n\nfunction BuilderFieldContent({ component }: { component: BuilderSchema }) {\n const t = useT()\n const formatter = useFormatter()\n const registry = useOptionalRegistry()\n const form = useOptionalFormStore()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<unknown>(component)\n const locked = disabled || readOnly\n const items = builderItems(value)\n const [collapsed, setCollapsed] = useState<Record<number, boolean>>({})\n const [editing, setEditing] = useState<Record<number, boolean>>({})\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n const context = useFieldActionContext(component)\n const runner = useActionRunner()\n const runItemAction = useRunItemAction(context)\n const fallbacks = useItemActionFallbacks(component.addActionLabel)\n const resolve = useItemActions(component, fallbacks)\n // Long lists render only the visible blocks (plus a few around them).\n const virtual = useVirtualList(\n items.length,\n virtualizeOptions(component.virtualize, VIRTUAL_ESTIMATES.item, 12),\n )\n const { range, indexes, style, attach, item: attachItem } = virtual\n useScrollToErroredItem(virtual.list, range.active, component.statePath)\n\n const update = (next: unknown[]) => setValue(next as BuilderItemState[])\n const moveItem = (from: number, to: number) => {\n update(moveRepeaterItem(items, from, to))\n setCollapsed((state) => moveIndexedState(state, from, to))\n setEditing((state) => moveIndexedState(state, from, to))\n }\n const removeItem = (index: number) => {\n update(removeRepeaterItem(items, index))\n setCollapsed((state) => removeIndexedState(state, index))\n setEditing((state) => removeIndexedState(state, index))\n }\n const cloneItem = (index: number) => {\n update(cloneRepeaterItem(items, index))\n setCollapsed((state) => cloneIndexedState(state, index))\n setEditing((state) => cloneIndexedState(state, index))\n }\n const addBlock = (block: BuilderBlockSchema) =>\n runItemAction(resolve('add'), { block: block.name }, () =>\n update(insertBuilderItem(items, block)),\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(\n value,\n moveItem,\n (index) => builderItemLabel(component, items, index),\n items.length,\n )\n const dropItem = (from: number, to: number) =>\n runItemAction(resolve('reorder'), { items: reorderedItemKeys(items.length, from, to) }, () =>\n moveItem(from, to),\n )\n // Closing the editor re-renders the previews from the server.\n const toggleEditing = (index: number) => {\n const open = !editing[index]\n setEditing((state) => ({ ...state, [index]: open }))\n if (!open) void form?.refresh(component.statePath)\n }\n\n const blockOf = (type: string) => component.blocks.find((block) => block.name === type)\n const underMax = component.maxItems == null || items.length < component.maxItems\n const canAdd = component.addable && !locked && underMax\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 const addAction = resolve('add')\n const showEmptyPicker =\n canAdd && addAction !== null && !addAction.disabled && component.blocks.length > 0\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div ref={moveContainerRef} className=\"flex flex-col gap-3\" data-slot=\"builder\">\n {moveRegion}\n {items.length === 0 &&\n (showEmptyPicker ? (\n // No blocks yet: the picker is right there instead of behind a button.\n <div\n data-slot=\"builder-empty\"\n className=\"bg-muted/30 flex flex-col gap-3 rounded-lg border border-dashed p-3 sm:p-4\"\n >\n <div className=\"px-1\">\n <p className=\"text-sm font-medium\">\n {t('builder.empty_heading', {}, 'Add your first block')}\n </p>\n <p className=\"text-muted-foreground text-sm\">\n {t(\n 'builder.empty_description',\n {},\n 'Pick one to start. You can add more, reorder and remove them at any time.',\n )}\n </p>\n </div>\n <BuilderBlockPickerPanel\n component={component}\n items={items}\n onPick={addBlock}\n className=\"bg-card max-h-[28rem] rounded-md border\"\n />\n </div>\n ) : (\n <p className=\"text-muted-foreground rounded-lg border border-dashed p-4 text-center text-sm\">\n {t('builder.empty', {}, 'No blocks yet.')}\n </p>\n ))}\n {component.collapsible && items.length > 0 && (\n <CollapseAllActions\n context={context}\n resolve={(name) => resolve(name)}\n onCollapseAll={() =>\n setCollapsed(Object.fromEntries(items.map((_, index) => [index, true])))\n }\n onExpandAll={() =>\n setCollapsed(Object.fromEntries(items.map((_, index) => [index, false])))\n }\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', items.length === 0 && 'hidden')}\n >\n {indexes.map((index) => {\n const item = items[index] as BuilderItemState\n const block = blockOf(item.type)\n const isCollapsed = collapsed[index] ?? component.collapsed ?? false\n const label = component.blockLabels ? builderItemLabel(component, items, index) : null\n const icon = block?.icon ?? component.items[index]?.icon\n const server = component.items[index]\n const serverItem = server?.type === item.type ? server : undefined\n const previewName = serverItem?.previewComponent ?? block?.previewComponent\n const Preview = previewName\n ? registry?.resolve(`builder-preview:${previewName}`)\n : undefined\n const hasPreview =\n component.blockPreviews === true &&\n (Preview !== undefined || Boolean(serverItem?.preview))\n const showPreview = hasPreview && !editing[index]\n const summary = serverItem?.summary\n const menu = Boolean(component.blockActionsMenu) && !locked\n const canClone =\n Boolean(component.cloneable) &&\n canAdd &&\n block !== undefined &&\n canAddBuilderBlock(component, block, items)\n const openEditor = () => {\n if (!locked) toggleEditing(index)\n }\n\n return (\n <li\n key={index}\n ref={attachItem}\n data-index={index}\n aria-setsize={range.active ? items.length : undefined}\n aria-posinset={range.active ? index + 1 : undefined}\n data-block={item.type}\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 )}\n >\n <Collapsible open={!isCollapsed}>\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')?.label ?? undefined}\n aria-hidden=\"true\"\n >\n <Icon name={resolve('reorder')?.icon ?? 'grip-vertical'} />\n </span>\n )}\n {component.blockIcons && icon && (\n <Icon name={icon} className=\"text-muted-foreground size-4\" />\n )}\n {component.blockNumbers && (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n {formatter.number(index + 1)}\n </span>\n )}\n <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n {label && (\n <span\n dir=\"auto\"\n className=\"min-w-0 shrink-0 truncate text-sm font-medium sm:max-w-[60%]\"\n >\n {label}\n </span>\n )}\n {summary && (\n <span\n dir=\"auto\"\n className=\"text-muted-foreground min-w-0 truncate text-sm\"\n data-slot=\"builder-item-summary\"\n >\n {summary}\n </span>\n )}\n </span>\n <ItemHeaderActions\n context={context}\n itemKey={String(index)}\n index={index}\n count={items.length}\n resolve={(name) => resolve(name, serverItem)}\n extraActions={menu || locked ? undefined : serverItem?.actions}\n buttons={buttons && !menu}\n canClone={canClone && !menu}\n canDelete={canDelete && !menu}\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 {hasPreview && !locked && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('builder.edit_block', {}, 'Edit block')}\n aria-pressed={Boolean(editing[index])}\n onClick={() => toggleEditing(index)}\n >\n <Icon\n name=\"alfakit::forms.components.builder.actions.edit\"\n fallback={PencilIcon}\n />\n </Button>\n )}\n {menu && (\n <BlockActionsMenu\n label={label ?? ''}\n resolve={(name) => resolve(name, serverItem)}\n canMoveUp={buttons && index > 0}\n canMoveDown={buttons && index < items.length - 1}\n canClone={canClone}\n canDelete={canDelete}\n extraActions={locked ? undefined : serverItem?.actions}\n onAction={(name, onClient) =>\n runItemAction(\n resolve(name, serverItem),\n { item: String(index) },\n onClient,\n )\n }\n onExtraAction={(action) =>\n void runner.trigger(\n action,\n itemActionContext(context, { item: String(index) }),\n )\n }\n onMove={(to) => moveWithButton(index, to)}\n index={index}\n onClone={() => cloneItem(index)}\n onDelete={() => removeItem(index)}\n />\n )}\n </ItemHeaderActions>\n </div>\n <CollapsibleContent>\n {showPreview ? (\n <div\n data-slot=\"builder-preview\"\n className={cn(\n 'p-4',\n !component.interactivePreviews && !locked && 'cursor-pointer',\n )}\n {...(component.interactivePreviews || locked\n ? {}\n : {\n role: 'button',\n tabIndex: 0,\n 'aria-label': t('builder.edit_block', {}, 'Edit block'),\n onClick: openEditor,\n onKeyDown: (event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n openEditor()\n }\n },\n })}\n >\n <div\n className={cn(!component.interactivePreviews && 'pointer-events-none')}\n >\n {Preview ? (\n createElement(Preview, { data: item.data, block: item.type, index })\n ) : (\n <div dangerouslySetInnerHTML={{ __html: serverItem?.preview ?? '' }} />\n )}\n </div>\n </div>\n ) : (\n <div className=\"p-4\">\n <SchemaRenderer\n components={builderItemSchema(component, items, index)}\n columns={\n block?.columns ?? component.items[index]?.columns ?? { default: 1 }\n }\n />\n </div>\n )}\n </CollapsibleContent>\n </Collapsible>\n </li>\n )\n })}\n </ul>\n {canAdd && addAction && component.blocks.length > 0 && items.length > 0 && (\n <AddActionRow alignment={component.addActionAlignment}>\n <BuilderBlockPicker\n component={component}\n items={items}\n onPick={addBlock}\n trigger={\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={addAction.disabled}\n data-slot=\"builder-add\"\n >\n {addAction.icon && <Icon name={addAction.icon} />}\n {addAction.label}\n </Button>\n }\n />\n </AddActionRow>\n )}\n </div>\n </FieldWrapper>\n )\n}\n\n/**\n * `blockActionsMenu()`: an item's actions in one compact menu (move up / down, duplicate,\n * extra item actions, delete). Built-in actions run in the browser unless customised.\n */\nfunction BlockActionsMenu({\n label,\n resolve,\n canMoveUp,\n canMoveDown,\n canClone,\n canDelete,\n extraActions,\n index,\n onAction,\n onExtraAction,\n onMove,\n onClone,\n onDelete,\n}: {\n label: string\n resolve: (name: ItemActionName) => ItemActionSchema | null\n canMoveUp: boolean\n canMoveDown: boolean\n canClone: boolean\n canDelete: boolean\n extraActions?: ActionSchema[]\n index: number\n onAction: (name: ItemActionName, onClient: () => void) => void\n onExtraAction: (action: ActionSchema) => void\n onMove: (to: number) => void\n onClone: () => void\n onDelete: () => void\n}) {\n const t = useT()\n const entry = (\n name: ItemActionName,\n enabled: boolean,\n onClient: () => void,\n destructive = false,\n ) => {\n const action = resolve(name)\n\n if (!action || !enabled) return null\n\n return (\n <DropdownMenuItem\n key={name}\n variant={destructive ? 'destructive' : 'default'}\n disabled={action.disabled}\n onSelect={() => onAction(name, onClient)}\n >\n {action.icon && <Icon name={action.icon} />}\n {name === 'clone' ? t('builder.duplicate_block', {}, 'Duplicate') : action.label}\n </DropdownMenuItem>\n )\n }\n const visibleExtras = extraActions ?? []\n const hasMain = canMoveUp || canMoveDown || canClone || visibleExtras.length > 0\n\n if (!hasMain && !canDelete) return null\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"builder-item-menu\"\n aria-label={t('builder.block_actions', { block: label }, 'Actions for :block')}\n >\n <EllipsisIcon aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-44\">\n {entry('moveUp', canMoveUp, () => onMove(index - 1))}\n {entry('moveDown', canMoveDown, () => onMove(index + 1))}\n {entry('clone', canClone, onClone)}\n {visibleExtras.map((action) => (\n <DropdownMenuItem\n key={action.name}\n disabled={action.disabled}\n onSelect={() => onExtraAction(action)}\n >\n {action.icon && <Icon name={action.icon} />}\n {action.label}\n </DropdownMenuItem>\n ))}\n {canDelete && hasMain && <DropdownMenuSeparator />}\n {entry('delete', canDelete, onDelete, true)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"builder.js","names":[],"sources":["../../../src/components/fields/builder.tsx"],"sourcesContent":["import type {\n ActionSchema,\n BuilderBlockSchema,\n BuilderItemState,\n BuilderSchema,\n ItemActionName,\n ItemActionSchema,\n} from '@alfakit/core'\nimport {\n builderItemLabel,\n builderItems,\n builderItemSchema,\n canAddBuilderBlock,\n cloneIndexedState,\n cloneRepeaterItem,\n fieldIds,\n insertBuilderItem,\n isDeferredPlaceholder,\n itemActionContext,\n moveIndexedState,\n moveRepeaterItem,\n removeIndexedState,\n removeRepeaterItem,\n reorderedItemKeys,\n VIRTUAL_ESTIMATES,\n virtualizeOptions,\n} from '@alfakit/core'\nimport { EllipsisIcon, PencilIcon } from 'lucide-react'\nimport { createElement, useState, type KeyboardEvent } from 'react'\nimport { useActionRunner, useFormatter, useOptionalRegistry, useT } from '../../context'\nimport { useScrollToErroredItem, useVirtualList } from '../../hooks/use-virtual-list'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { useField, useOptionalFormStore } 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 { Button } from '../ui/button'\nimport { Collapsible, CollapsibleContent } from '../ui/collapsible'\nimport {\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from '../ui/dropdown-menu'\nimport { BuilderBlockPicker, BuilderBlockPickerPanel } from './builder-picker'\nimport {\n AddActionRow,\n CollapseAllActions,\n ItemHeaderActions,\n useItemActionFallbacks,\n useItemActions,\n useItemMoves,\n useRunItemAction,\n} from './item-actions'\n\n/**\n * A list of blocks, each with its own schema. Items use the server's per-item schema\n * when it describes the same block, else the block's template (fresh, moved or cloned\n * items the server has not seen yet).\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 BuilderField({ component }: { component: BuilderSchema }) {\n if (!isDeferredPlaceholder(component)) return <BuilderFieldContent component={component} />\n\n return (\n <DeferredSchema component={component}>\n {(loaded) => <BuilderFieldContent component={loaded} />}\n </DeferredSchema>\n )\n}\n\nfunction BuilderFieldContent({ component }: { component: BuilderSchema }) {\n const t = useT()\n const formatter = useFormatter()\n const registry = useOptionalRegistry()\n const form = useOptionalFormStore()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<unknown>(component)\n const locked = disabled || readOnly\n const items = builderItems(value)\n const [collapsed, setCollapsed] = useState<Record<number, boolean>>({})\n const [editing, setEditing] = useState<Record<number, boolean>>({})\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n const context = useFieldActionContext(component)\n const runner = useActionRunner()\n const runItemAction = useRunItemAction(context)\n const fallbacks = useItemActionFallbacks(component.addActionLabel, 'builder')\n const resolve = useItemActions(component, fallbacks)\n // Long lists render only the visible blocks (plus a few around them).\n const virtual = useVirtualList(\n items.length,\n virtualizeOptions(component.virtualize, VIRTUAL_ESTIMATES.item, 12),\n )\n const { range, indexes, style, attach, item: attachItem } = virtual\n useScrollToErroredItem(virtual.list, range.active, component.statePath)\n\n const update = (next: unknown[]) => setValue(next as BuilderItemState[])\n const moveItem = (from: number, to: number) => {\n update(moveRepeaterItem(items, from, to))\n setCollapsed((state) => moveIndexedState(state, from, to))\n setEditing((state) => moveIndexedState(state, from, to))\n }\n const removeItem = (index: number) => {\n update(removeRepeaterItem(items, index))\n setCollapsed((state) => removeIndexedState(state, index))\n setEditing((state) => removeIndexedState(state, index))\n }\n const cloneItem = (index: number) => {\n update(cloneRepeaterItem(items, index))\n setCollapsed((state) => cloneIndexedState(state, index))\n setEditing((state) => cloneIndexedState(state, index))\n }\n const addBlock = (block: BuilderBlockSchema) =>\n runItemAction(resolve('add'), { block: block.name }, () =>\n update(insertBuilderItem(items, block)),\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(\n value,\n moveItem,\n (index) => builderItemLabel(component, items, index),\n items.length,\n )\n const dropItem = (from: number, to: number) =>\n runItemAction(resolve('reorder'), { items: reorderedItemKeys(items.length, from, to) }, () =>\n moveItem(from, to),\n )\n // Closing the editor re-renders the previews from the server.\n const toggleEditing = (index: number) => {\n const open = !editing[index]\n setEditing((state) => ({ ...state, [index]: open }))\n if (!open) void form?.refresh(component.statePath)\n }\n\n const blockOf = (type: string) => component.blocks.find((block) => block.name === type)\n // Summaries are made on the server: collapsing items whose block has one brings them up\n // to date with the edits.\n const refreshSummaries = (indexes: number[]) => {\n if (indexes.some((index) => blockOf(items[index]?.type ?? '')?.summary)) {\n void form?.refresh(component.statePath)\n }\n }\n const underMax = component.maxItems == null || items.length < component.maxItems\n const canAdd = component.addable && !locked && underMax\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 const addAction = resolve('add')\n const showEmptyPicker =\n canAdd && addAction !== null && !addAction.disabled && component.blocks.length > 0\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div ref={moveContainerRef} className=\"flex flex-col gap-3\" data-slot=\"builder\">\n {moveRegion}\n {items.length === 0 &&\n (showEmptyPicker ? (\n // No blocks yet: the picker is right there instead of behind a button.\n <div\n data-slot=\"builder-empty\"\n className=\"bg-muted/30 flex flex-col gap-3 rounded-lg border border-dashed p-3 sm:p-4\"\n >\n <div className=\"px-1\">\n <p className=\"text-sm font-medium\">\n {t('builder.empty_heading', {}, 'Add your first block')}\n </p>\n <p className=\"text-muted-foreground text-sm\">\n {t(\n 'builder.empty_description',\n {},\n 'Pick one to start. You can add more, reorder and remove them at any time.',\n )}\n </p>\n </div>\n <BuilderBlockPickerPanel\n component={component}\n items={items}\n onPick={addBlock}\n className=\"bg-card max-h-[28rem] rounded-md border\"\n />\n </div>\n ) : (\n <p className=\"text-muted-foreground rounded-lg border border-dashed p-4 text-center text-sm\">\n {t('builder.empty', {}, 'No blocks yet.')}\n </p>\n ))}\n {component.collapsible && items.length > 0 && (\n <CollapseAllActions\n context={context}\n resolve={(name) => resolve(name)}\n onCollapseAll={() => {\n setCollapsed(Object.fromEntries(items.map((_, index) => [index, true])))\n refreshSummaries(items.map((_, index) => index))\n }}\n onExpandAll={() =>\n setCollapsed(Object.fromEntries(items.map((_, index) => [index, false])))\n }\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', items.length === 0 && 'hidden')}\n >\n {indexes.map((index) => {\n const item = items[index] as BuilderItemState\n const block = blockOf(item.type)\n const isCollapsed = collapsed[index] ?? component.collapsed ?? false\n const label = component.blockLabels ? builderItemLabel(component, items, index) : null\n const icon = block?.icon ?? component.items[index]?.icon\n const server = component.items[index]\n const serverItem = server?.type === item.type ? server : undefined\n const previewName = serverItem?.previewComponent ?? block?.previewComponent\n const Preview = previewName\n ? registry?.resolve(`builder-preview:${previewName}`)\n : undefined\n const hasPreview =\n component.blockPreviews === true &&\n (Preview !== undefined || Boolean(serverItem?.preview))\n const showPreview = hasPreview && !editing[index]\n const summary = serverItem?.summary\n const menu = Boolean(component.blockActionsMenu) && !locked\n const canClone =\n Boolean(component.cloneable) &&\n canAdd &&\n block !== undefined &&\n canAddBuilderBlock(component, block, items)\n const openEditor = () => {\n if (!locked) toggleEditing(index)\n }\n\n return (\n <li\n key={index}\n ref={attachItem}\n data-index={index}\n aria-setsize={range.active ? items.length : undefined}\n aria-posinset={range.active ? index + 1 : undefined}\n data-block={item.type}\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 )}\n >\n <Collapsible open={!isCollapsed}>\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')?.label ?? undefined}\n aria-hidden=\"true\"\n >\n <Icon name={resolve('reorder')?.icon ?? 'grip-vertical'} />\n </span>\n )}\n {component.blockIcons && icon && (\n <Icon name={icon} className=\"text-muted-foreground size-4\" />\n )}\n {component.blockNumbers && (\n <span className=\"text-muted-foreground text-xs tabular-nums\">\n {formatter.number(index + 1)}\n </span>\n )}\n <span className=\"flex min-w-0 flex-1 items-baseline gap-2\">\n {label && (\n <span\n dir=\"auto\"\n className=\"min-w-0 shrink-0 truncate text-sm font-medium sm:max-w-[60%]\"\n >\n {label}\n </span>\n )}\n {summary && (\n <span\n dir=\"auto\"\n className=\"text-muted-foreground min-w-0 truncate text-sm\"\n data-slot=\"builder-item-summary\"\n >\n {summary}\n </span>\n )}\n </span>\n <ItemHeaderActions\n context={context}\n itemKey={String(index)}\n index={index}\n count={items.length}\n resolve={(name) => resolve(name, serverItem)}\n extraActions={menu || locked ? undefined : serverItem?.actions}\n buttons={buttons && !menu}\n canClone={canClone && !menu}\n canDelete={canDelete && !menu}\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 if (!isCollapsed) refreshSummaries([index])\n }}\n >\n {hasPreview && !locked && (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={t('builder.edit_block', {}, 'Edit block')}\n aria-pressed={Boolean(editing[index])}\n onClick={() => toggleEditing(index)}\n >\n <Icon\n name=\"alfakit::forms.components.builder.actions.edit\"\n fallback={PencilIcon}\n />\n </Button>\n )}\n {menu && (\n <BlockActionsMenu\n label={label ?? ''}\n resolve={(name) => resolve(name, serverItem)}\n canMoveUp={buttons && index > 0}\n canMoveDown={buttons && index < items.length - 1}\n canClone={canClone}\n canDelete={canDelete}\n extraActions={locked ? undefined : serverItem?.actions}\n onAction={(name, onClient) =>\n runItemAction(\n resolve(name, serverItem),\n { item: String(index) },\n onClient,\n )\n }\n onExtraAction={(action) =>\n void runner.trigger(\n action,\n itemActionContext(context, { item: String(index) }),\n )\n }\n onMove={(to) => moveWithButton(index, to)}\n index={index}\n onClone={() => cloneItem(index)}\n onDelete={() => removeItem(index)}\n />\n )}\n </ItemHeaderActions>\n </div>\n <CollapsibleContent>\n {showPreview ? (\n <div\n data-slot=\"builder-preview\"\n className={cn(\n 'p-4',\n !component.interactivePreviews && !locked && 'cursor-pointer',\n )}\n {...(component.interactivePreviews || locked\n ? {}\n : {\n role: 'button',\n tabIndex: 0,\n 'aria-label': t('builder.edit_block', {}, 'Edit block'),\n onClick: openEditor,\n onKeyDown: (event: KeyboardEvent) => {\n if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault()\n openEditor()\n }\n },\n })}\n >\n <div\n className={cn(!component.interactivePreviews && 'pointer-events-none')}\n >\n {Preview ? (\n createElement(Preview, { data: item.data, block: item.type, index })\n ) : (\n <div dangerouslySetInnerHTML={{ __html: serverItem?.preview ?? '' }} />\n )}\n </div>\n </div>\n ) : (\n <div className=\"p-4\">\n <SchemaRenderer\n components={builderItemSchema(component, items, index)}\n columns={\n block?.columns ?? component.items[index]?.columns ?? { default: 1 }\n }\n />\n </div>\n )}\n </CollapsibleContent>\n </Collapsible>\n </li>\n )\n })}\n </ul>\n {canAdd && addAction && component.blocks.length > 0 && items.length > 0 && (\n <AddActionRow alignment={component.addActionAlignment}>\n <BuilderBlockPicker\n component={component}\n items={items}\n onPick={addBlock}\n trigger={\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n disabled={addAction.disabled}\n data-slot=\"builder-add\"\n >\n {addAction.icon && <Icon name={addAction.icon} />}\n {addAction.label}\n </Button>\n }\n />\n </AddActionRow>\n )}\n </div>\n </FieldWrapper>\n )\n}\n\n/**\n * `blockActionsMenu()`: an item's actions in one compact menu (move up / down, duplicate,\n * extra item actions, delete). Built-in actions run in the browser unless customised.\n */\nfunction BlockActionsMenu({\n label,\n resolve,\n canMoveUp,\n canMoveDown,\n canClone,\n canDelete,\n extraActions,\n index,\n onAction,\n onExtraAction,\n onMove,\n onClone,\n onDelete,\n}: {\n label: string\n resolve: (name: ItemActionName) => ItemActionSchema | null\n canMoveUp: boolean\n canMoveDown: boolean\n canClone: boolean\n canDelete: boolean\n extraActions?: ActionSchema[]\n index: number\n onAction: (name: ItemActionName, onClient: () => void) => void\n onExtraAction: (action: ActionSchema) => void\n onMove: (to: number) => void\n onClone: () => void\n onDelete: () => void\n}) {\n const t = useT()\n const entry = (\n name: ItemActionName,\n enabled: boolean,\n onClient: () => void,\n destructive = false,\n ) => {\n const action = resolve(name)\n\n if (!action || !enabled) return null\n\n return (\n <DropdownMenuItem\n key={name}\n variant={destructive ? 'destructive' : 'default'}\n disabled={action.disabled}\n onSelect={() => onAction(name, onClient)}\n >\n {action.icon && <Icon name={action.icon} />}\n {name === 'clone' ? t('builder.duplicate_block', {}, 'Duplicate') : action.label}\n </DropdownMenuItem>\n )\n }\n const visibleExtras = extraActions ?? []\n const hasMain = canMoveUp || canMoveDown || canClone || visibleExtras.length > 0\n\n if (!hasMain && !canDelete) return null\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n data-slot=\"builder-item-menu\"\n aria-label={t('builder.block_actions', { block: label }, 'Actions for :block')}\n >\n <EllipsisIcon aria-hidden=\"true\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-44\">\n {entry('moveUp', canMoveUp, () => onMove(index - 1))}\n {entry('moveDown', canMoveDown, () => onMove(index + 1))}\n {entry('clone', canClone, onClone)}\n {visibleExtras.map((action) => (\n <DropdownMenuItem\n key={action.name}\n disabled={action.disabled}\n onSelect={() => onExtraAction(action)}\n >\n {action.icon && <Icon name={action.icon} />}\n {action.label}\n </DropdownMenuItem>\n ))}\n {canDelete && hasMain && <DropdownMenuSeparator />}\n {entry('delete', canDelete, onDelete, true)}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"],"mappings":"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"}
@@ -33,6 +33,7 @@ function ColorPickerField({ component }) {
33
33
  }), /* @__PURE__ */ jsx(Input, {
34
34
  id,
35
35
  name: component.name,
36
+ dir: "ltr",
36
37
  value: value ?? "",
37
38
  onChange: (event) => setValue(event.target.value),
38
39
  onBlur: blur,
@@ -1 +1 @@
1
- {"version":3,"file":"color-picker.js","names":[],"sources":["../../../src/components/fields/color-picker.tsx"],"sourcesContent":["import type { ColorPickerSchema } from '@alfakit/core'\nimport { colorAlpha, colorToHex, formatColor, withColorAlpha } from '@alfakit/core'\nimport { useFormatter, useT } from '../../context'\nimport { extraProps } from '../../lib/attributes'\nimport { useField } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Input } from '../ui/input'\n\nexport function ColorPickerField({ component }: { component: ColorPickerSchema }) {\n const t = useT()\n const format = useFormatter()\n const { value, setValue, blur, error, id, disabled, readOnly, aria } = useField<string | null>(\n component,\n )\n const isHex = component.format === 'hex'\n // `rgba()`: the native swatch has no alpha, so a slider sets the opacity.\n const alpha = Math.round(colorAlpha(value) * 100)\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div className=\"flex items-center gap-2\">\n <label\n className=\"border-input relative size-9 shrink-0 cursor-pointer overflow-hidden rounded-md border shadow-xs\"\n style={{ backgroundColor: value ?? 'transparent' }}\n aria-label={t('form.pick_color', {}, 'Pick a colour')}\n >\n <input\n type=\"color\"\n value={colorToHex(value) ?? '#000000'}\n onChange={(event) => setValue(formatColor(event.target.value, component.format, value))}\n onBlur={blur}\n disabled={disabled || readOnly}\n className=\"absolute inset-0 size-full cursor-pointer opacity-0\"\n />\n </label>\n <Input\n id={id}\n name={component.name}\n value={value ?? ''}\n onChange={(event) => setValue(event.target.value)}\n onBlur={blur}\n placeholder={component.placeholder ?? (isHex ? '#000000' : component.format)}\n disabled={disabled}\n readOnly={readOnly}\n {...aria}\n\n {...extraProps(component.extraInputAttributes, 'font-mono')}\n />\n </div>\n {component.format === 'rgba' && (\n <div data-slot=\"color-picker-alpha\" className=\"mt-2 flex items-center gap-2\">\n <input\n type=\"range\"\n min={0}\n max={100}\n step={1}\n value={alpha}\n onChange={(event) => setValue(withColorAlpha(value, Number(event.target.value) / 100))}\n onBlur={blur}\n disabled={disabled || readOnly}\n aria-label={t('form.color_opacity', {}, 'Opacity')}\n aria-valuetext={`${alpha}%`}\n className=\"accent-primary h-2 flex-1 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <span className=\"text-muted-foreground w-10 text-end text-xs tabular-nums\">\n {format.percent(alpha / 100)}\n </span>\n </div>\n )}\n </FieldWrapper>\n )\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,iBAAiB,EAAE,aAA+C;CAChF,MAAM,IAAI,KAAK;CACf,MAAM,SAAS,aAAa;CAC5B,MAAM,EAAE,OAAO,UAAU,MAAM,OAAO,IAAI,UAAU,UAAU,SAAS,SACrE,SACF;CACA,MAAM,QAAQ,UAAU,WAAW;CAEnC,MAAM,QAAQ,KAAK,MAAM,WAAW,KAAK,IAAI,GAAG;CAEhD,OACE,qBAAC,cAAD;EAAc,OAAO;EAAW,SAAS;EAAW;EAApD,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,oBAAC,SAAD;IACE,WAAU;IACV,OAAO,EAAE,iBAAiB,SAAS,cAAc;IACjD,cAAY,EAAE,mBAAmB,CAAC,GAAG,eAAe;IAEpD,UAAA,oBAAC,SAAD;KACE,MAAK;KACL,OAAO,WAAW,KAAK,KAAK;KAC5B,WAAW,UAAU,SAAS,YAAY,MAAM,OAAO,OAAO,UAAU,QAAQ,KAAK,CAAC;KACtF,QAAQ;KACR,UAAU,YAAY;KACtB,WAAU;IACX,CAAA;GACI,CAAA,GACP,oBAAC,OAAD;IACM;IACJ,MAAM,UAAU;IAChB,OAAO,SAAS;IAChB,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;IAChD,QAAQ;IACR,aAAa,UAAU,gBAAgB,QAAQ,YAAY,UAAU;IAC3D;IACA;IACV,GAAI;IAEJ,GAAI,WAAW,UAAU,sBAAsB,WAAW;GAC3D,CAAA,CACE;EACJ,CAAA,GAAA,UAAU,WAAW,UACpB,qBAAC,OAAD;GAAK,aAAU;GAAqB,WAAU;GAA9C,UAAA,CACE,oBAAC,SAAD;IACE,MAAK;IACL,KAAK;IACL,KAAK;IACL,MAAM;IACN,OAAO;IACP,WAAW,UAAU,SAAS,eAAe,OAAO,OAAO,MAAM,OAAO,KAAK,IAAI,GAAG,CAAC;IACrF,QAAQ;IACR,UAAU,YAAY;IACtB,cAAY,EAAE,sBAAsB,CAAC,GAAG,SAAS;IACjD,kBAAgB,GAAG,MAAM;IACzB,WAAU;GACX,CAAA,GACD,oBAAC,QAAD;IAAM,WAAU;IACb,UAAA,OAAO,QAAQ,QAAQ,GAAG;GACvB,CAAA,CACH;EAEK,CAAA,CAAA;;AAElB"}
1
+ {"version":3,"file":"color-picker.js","names":[],"sources":["../../../src/components/fields/color-picker.tsx"],"sourcesContent":["import type { ColorPickerSchema } from '@alfakit/core'\nimport { colorAlpha, colorToHex, formatColor, withColorAlpha } from '@alfakit/core'\nimport { useFormatter, useT } from '../../context'\nimport { extraProps } from '../../lib/attributes'\nimport { useField } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Input } from '../ui/input'\n\nexport function ColorPickerField({ component }: { component: ColorPickerSchema }) {\n const t = useT()\n const format = useFormatter()\n const { value, setValue, blur, error, id, disabled, readOnly, aria } = useField<string | null>(\n component,\n )\n const isHex = component.format === 'hex'\n // `rgba()`: the native swatch has no alpha, so a slider sets the opacity.\n const alpha = Math.round(colorAlpha(value) * 100)\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div className=\"flex items-center gap-2\">\n <label\n className=\"border-input relative size-9 shrink-0 cursor-pointer overflow-hidden rounded-md border shadow-xs\"\n style={{ backgroundColor: value ?? 'transparent' }}\n aria-label={t('form.pick_color', {}, 'Pick a colour')}\n >\n <input\n type=\"color\"\n value={colorToHex(value) ?? '#000000'}\n onChange={(event) => setValue(formatColor(event.target.value, component.format, value))}\n onBlur={blur}\n disabled={disabled || readOnly}\n className=\"absolute inset-0 size-full cursor-pointer opacity-0\"\n />\n </label>\n <Input\n id={id}\n name={component.name}\n dir=\"ltr\"\n value={value ?? ''}\n onChange={(event) => setValue(event.target.value)}\n onBlur={blur}\n placeholder={component.placeholder ?? (isHex ? '#000000' : component.format)}\n disabled={disabled}\n readOnly={readOnly}\n {...aria}\n\n {...extraProps(component.extraInputAttributes, 'font-mono')}\n />\n </div>\n {component.format === 'rgba' && (\n <div data-slot=\"color-picker-alpha\" className=\"mt-2 flex items-center gap-2\">\n <input\n type=\"range\"\n min={0}\n max={100}\n step={1}\n value={alpha}\n onChange={(event) => setValue(withColorAlpha(value, Number(event.target.value) / 100))}\n onBlur={blur}\n disabled={disabled || readOnly}\n aria-label={t('form.color_opacity', {}, 'Opacity')}\n aria-valuetext={`${alpha}%`}\n className=\"accent-primary h-2 flex-1 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <span className=\"text-muted-foreground w-10 text-end text-xs tabular-nums\">\n {format.percent(alpha / 100)}\n </span>\n </div>\n )}\n </FieldWrapper>\n )\n}\n"],"mappings":";;;;;;;;AAQA,SAAgB,iBAAiB,EAAE,aAA+C;CAChF,MAAM,IAAI,KAAK;CACf,MAAM,SAAS,aAAa;CAC5B,MAAM,EAAE,OAAO,UAAU,MAAM,OAAO,IAAI,UAAU,UAAU,SAAS,SACrE,SACF;CACA,MAAM,QAAQ,UAAU,WAAW;CAEnC,MAAM,QAAQ,KAAK,MAAM,WAAW,KAAK,IAAI,GAAG;CAEhD,OACE,qBAAC,cAAD;EAAc,OAAO;EAAW,SAAS;EAAW;EAApD,UAAA,CACE,qBAAC,OAAD;GAAK,WAAU;GAAf,UAAA,CACE,oBAAC,SAAD;IACE,WAAU;IACV,OAAO,EAAE,iBAAiB,SAAS,cAAc;IACjD,cAAY,EAAE,mBAAmB,CAAC,GAAG,eAAe;IAEpD,UAAA,oBAAC,SAAD;KACE,MAAK;KACL,OAAO,WAAW,KAAK,KAAK;KAC5B,WAAW,UAAU,SAAS,YAAY,MAAM,OAAO,OAAO,UAAU,QAAQ,KAAK,CAAC;KACtF,QAAQ;KACR,UAAU,YAAY;KACtB,WAAU;IACX,CAAA;GACI,CAAA,GACP,oBAAC,OAAD;IACM;IACJ,MAAM,UAAU;IAChB,KAAI;IACJ,OAAO,SAAS;IAChB,WAAW,UAAU,SAAS,MAAM,OAAO,KAAK;IAChD,QAAQ;IACR,aAAa,UAAU,gBAAgB,QAAQ,YAAY,UAAU;IAC3D;IACA;IACV,GAAI;IAEJ,GAAI,WAAW,UAAU,sBAAsB,WAAW;GAC3D,CAAA,CACE;EACJ,CAAA,GAAA,UAAU,WAAW,UACpB,qBAAC,OAAD;GAAK,aAAU;GAAqB,WAAU;GAA9C,UAAA,CACE,oBAAC,SAAD;IACE,MAAK;IACL,KAAK;IACL,KAAK;IACL,MAAM;IACN,OAAO;IACP,WAAW,UAAU,SAAS,eAAe,OAAO,OAAO,MAAM,OAAO,KAAK,IAAI,GAAG,CAAC;IACrF,QAAQ;IACR,UAAU,YAAY;IACtB,cAAY,EAAE,sBAAsB,CAAC,GAAG,SAAS;IACjD,kBAAgB,GAAG,MAAM;IACzB,WAAU;GACX,CAAA,GACD,oBAAC,QAAD;IAAM,WAAU;IACb,UAAA,OAAO,QAAQ,QAAQ,GAAG;GACvB,CAAA,CACH;EAEK,CAAA,CAAA;;AAElB"}
@@ -1,4 +1,4 @@
1
- import { useChoice, usePanel, useT } from "../../context.js";
1
+ import { useChoice, useFormatter, usePanel, useT } from "../../context.js";
2
2
  import { cn } from "../../lib/utils.js";
3
3
  import { Button } from "../ui/button.js";
4
4
  import { useStore } from "../../hooks/use-store.js";
@@ -6,13 +6,14 @@ import { Spinner } from "../ui/spinner.js";
6
6
  import { useField, useFormStore } from "../form/form-context.js";
7
7
  import { FieldWrapper } from "../schema/field-wrapper.js";
8
8
  import { ImageEditorDialog } from "./image-editor-dialog-lazy.js";
9
- import { canEditImage, concurrencyLimiter, cssAspectRatio, editableImageName, fetchEditableImage, fieldMemory, fileUploadAccept, fileUploadPaths, filesFromClipboard, formatBytes, insertUploadedFile, moveRepeaterItem, panelUploadOptions, requestJson, transformImage, uploadTemporaryFile, uploadedFileLinks } from "@alfakit/core";
9
+ import { canEditImage, concurrencyLimiter, cssAspectRatio, editableImageName, fetchEditableImage, fieldMemory, fileManagerSize, fileUploadAccept, fileUploadPaths, filesFromClipboard, insertUploadedFile, moveRepeaterItem, panelUploadOptions, requestJson, transformImage, uploadTemporaryFile, uploadedFileLinks } from "@alfakit/core";
10
10
  import { useRef, useState } from "react";
11
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { ChevronDownIcon, ChevronUpIcon, DownloadIcon, ExternalLinkIcon, FileIcon, PencilIcon, UploadCloudIcon, XIcon } from "lucide-react";
13
13
  //#region src/components/fields/file-upload.tsx
14
14
  function FileUploadField({ component }) {
15
15
  const t = useT();
16
+ const formatter = useFormatter();
16
17
  const choice = useChoice();
17
18
  const panel = usePanel();
18
19
  const { value, setValue, error, id, disabled, readOnly, aria } = useField(component);
@@ -208,7 +209,7 @@ function FileUploadField({ component }) {
208
209
  variant: circle ? "secondary" : "ghost",
209
210
  size: "icon-xs",
210
211
  className: cn(circle && "rounded-full shadow"),
211
- "aria-label": t("remove", {}, "Remove"),
212
+ "aria-label": t("file_upload.remove", { name: file.name }, "Remove :name"),
212
213
  onClick: () => {
213
214
  if (file.error || file.progress !== void 0) {
214
215
  releasePreview(file.preview);
@@ -263,7 +264,7 @@ function FileUploadField({ component }) {
263
264
  children: file.name
264
265
  }), /* @__PURE__ */ jsx("p", {
265
266
  className: cn("text-muted-foreground text-xs", file.missing && "text-destructive"),
266
- children: file.error ?? (file.missing ? t("form.file_missing", {}, "This file no longer exists.") : file.progress !== void 0 && !file.error ? component.uploadingMessage ?? t("form.uploading", {}, "Uploading…") : file.size ? formatBytes(file.size) : file.temporary ? t("form.uploading", {}, "Pending") : "")
267
+ children: file.error ?? (file.missing ? t("form.file_missing", {}, "This file no longer exists.") : file.progress !== void 0 && !file.error ? component.uploadingMessage ?? t("form.uploading", {}, "Uploading…") : file.size ? fileManagerSize(file.size, formatter) : file.temporary ? t("form.uploading", {}, "Pending") : "")
267
268
  })]
268
269
  }),
269
270
  /* @__PURE__ */ jsxs("div", {
@@ -274,7 +275,7 @@ function FileUploadField({ component }) {
274
275
  type: "button",
275
276
  variant: "ghost",
276
277
  size: "icon-xs",
277
- "aria-label": t("form.move_up", {}, "Move up"),
278
+ "aria-label": t("file_upload.move_up", { name: file.name }, "Move :name up"),
278
279
  disabled: index === 0,
279
280
  onClick: () => move(index, index - 1),
280
281
  children: /* @__PURE__ */ jsx(ChevronUpIcon, {})
@@ -282,7 +283,7 @@ function FileUploadField({ component }) {
282
283
  type: "button",
283
284
  variant: "ghost",
284
285
  size: "icon-xs",
285
- "aria-label": t("form.move_down", {}, "Move down"),
286
+ "aria-label": t("file_upload.move_down", { name: file.name }, "Move :name down"),
286
287
  disabled: index === files.length - 1,
287
288
  onClick: () => move(index, index + 1),
288
289
  children: /* @__PURE__ */ jsx(ChevronDownIcon, {})
@@ -292,7 +293,7 @@ function FileUploadField({ component }) {
292
293
  variant: circle ? "secondary" : "ghost",
293
294
  size: "icon-xs",
294
295
  className: cn(circle && "rounded-full shadow"),
295
- "aria-label": t("image_editor.title", {}, "Edit image"),
296
+ "aria-label": t("file_upload.edit", { name: file.name }, "Edit :name"),
296
297
  title: t("image_editor.title", {}, "Edit image"),
297
298
  "data-action": "edit-image",
298
299
  onClick: () => void editExisting(file),
@@ -307,7 +308,7 @@ function FileUploadField({ component }) {
307
308
  href: uploadedFileLinks(file).open,
308
309
  target: "_blank",
309
310
  rel: "noopener noreferrer",
310
- "aria-label": `${t("open", {}, "Open")} ${t("opens_in_new_tab", {}, "(opens in a new tab)")}`,
311
+ "aria-label": `${t("file_upload.open", { name: file.name }, "Open :name")} ${t("opens_in_new_tab", {}, "(opens in a new tab)")}`,
311
312
  children: /* @__PURE__ */ jsx(ExternalLinkIcon, {})
312
313
  })
313
314
  }),
@@ -319,7 +320,7 @@ function FileUploadField({ component }) {
319
320
  children: /* @__PURE__ */ jsx("a", {
320
321
  href: uploadedFileLinks(file).download,
321
322
  download: true,
322
- "aria-label": t("download", {}, "Download"),
323
+ "aria-label": t("file_upload.download", { name: file.name }, "Download :name"),
323
324
  children: /* @__PURE__ */ jsx(DownloadIcon, {})
324
325
  })
325
326
  }),
@@ -357,7 +358,7 @@ function FileUploadField({ component }) {
357
358
  className: "text-muted-foreground text-xs",
358
359
  children: [
359
360
  component.acceptedFileTypes?.join(", "),
360
- component.maxSize ? `≤ ${formatBytes(component.maxSize * 1024, 0)}` : null,
361
+ component.maxSize ? `≤ ${fileManagerSize(component.maxSize * 1024, formatter)}` : null,
361
362
  component.minFiles ? choice("form.min_files", component.minFiles, {}, "At least :count file|At least :count files") : null
362
363
  ].filter(Boolean).join(" · ")
363
364
  })
@@ -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 filesFromClipboard,\n fileUploadAccept,\n fileUploadPaths,\n formatBytes,\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, 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 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('remove', {}, 'Remove')}\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 ? formatBytes(file.size)\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('form.move_up', {}, 'Move 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('form.move_down', {}, 'Move down')}\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('image_editor.title', {}, 'Edit image')}\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('open', {}, 'Open')} ${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('download', {}, 'Download')}\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 ? `≤ ${formatBytes(component.maxSize * 1024, 0)}` : 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":";;;;;;;;;;;;;AA+CA,SAAgB,gBAAgB,EAAE,aAA8C;CAC9E,MAAM,IAAI,KAAK;CACf,MAAM,SAAS,UAAU;CACzB,MAAM,QAAQ,SAAS;CACvB,MAAM,EAAE,OAAO,UAAU,OAAO,IAAI,UAAU,UAAU,SAAS,SAE/D,SAAS;CAEX,MAAM,SAAS,YAAY;CAC3B,MAAM,QAAQ,aAAa;CAK3B,MAAM,iBAAiB,YACrB,OACA,GAAG,UAAU,UAAU,mBACjB,CAAC,CACT;CACA,MAAM,gBAAgB,YACpB,OACA,GAAG,UAAU,UAAU,kBACjB,CAAC,CACT;CACA,MAAM,YAAY,SAAS,cAAc;CACzC,MAAM,WAAW,SAAS,aAAa;CACvC,MAAM,eAAe,eAAe;CACpC,MAAM,cAAc,cAAc;CAClC,MAAM,CAAC,UAAU,eAAe,SAAS,KAAK;CAE9C,MAAM,CAAC,gBAAgB,qBAAqB,+BAAoC,IAAI,IAAI,CAAC;CACzF,MAAM,WAAW,OAAyB,IAAI;CAC9C,MAAM,CAAC,UAAU,eAAe,SAAwB,IAAI;CAE5D,MAAM,CAAC,SAAS,cAAc,SAAkC,IAAI;CACpE,MAAM,aAAa,SACjB,IAAI,SAAsB,YACxB,WAAW;EACT;EACA,UAAU,WAAW;GACnB,WAAW,IAAI;GACf,QAAQ,MAAM;EAChB;CACF,CAAC,CACH;CAEF,MAAM,aAAa,YACjB,OACA,GAAG,UAAU,UAAU,kCACjB,IAAI,IAAI,CAChB,CAAC,CAAC,IAAI;CAEN,MAAM,kBAAkB,QAA4B;EAClD,IAAI,OAAO,WAAW,OAAO,GAAG,GAAG,IAAI,gBAAgB,GAAG;CAC5D;CAEA,MAAM,QAAQ,gBAAgB,KAAK;CACnC,MAAM,QAAqB,MAAM,KAC9B,SACC,CAAC,GAAG,UAAU,OAAO,GAAG,QAAQ,CAAC,CAAC,MAAM,SAAS,KAAK,SAAS,IAAI,KAAK;EACtE;EACA,MAAM,KAAK,QAAQ,SAAS,EAAE;CAChC,CACJ;CACA,MAAM,WAAW,UAAU,WAAY,UAAU,YAAY,WAAY;CACzE,MAAM,aAAa,MAAM,SAAS,UAAU,SAAS;CAErD,MAAM,UAAU,SAAmB,SAAS,UAAU,WAAW,OAAQ,KAAK,MAAM,IAAK;CAEzF,MAAM,SAAS,OAAO,SAA4B;EAChD,MAAM,WAAW,MAAM,UAAU;EAEjC,IAAI,CAAC,UACH;EAGF,MAAM,WAAW,MAAM,KAAK,IAAI,CAAC,CAAC,MAChC,GACA,KAAK,IAAI,GAAG,WAAW,MAAM,SAAS,UAAU,MAAM,CACxD;EAIA,MAAM,QAAQ,mBAAmB,UAAU,sBAAsB,CAAC;EAClE,MAAM,SAA0B,CAAC;EAEjC,KAAK,MAAM,CAAC,UAAU,WAAW,SAAS,QAAQ,GAAG;GACnD,IAAI,WAAW;GACf,MAAM,OAAO,QAAQ,UAAU,eAAe,aAAa,MAAM,CAAC;GAElE,IAAI,MAAM;IACR,MAAM,SAAS,MAAM,UAAU,MAAM;IAGrC,IAAI,CAAC,QAAQ;IAEb,WAAW;GACb;GAEA,MAAM,OAAO,MAAM,QAAQ,UAAU,IAAI;GAEzC,IAAI,CAAC,MAAM;GAEX,MAAM,QAAQ,YAAY,MAAM,QAAQ;GACxC,cAAc,UAAU,CAAC,GAAG,OAAO,KAAK,CAAC;GAEzC,OAAO,KAAK,YAAY,KAAK,UAAU,MAAM,KAAK,CAAC,CAAC;EACtD;EAEA,MAAM,QAAQ,IAAI,MAAM;CAC1B;;CAGA,MAAM,UAAU,OAAO,UAAgB,WAA0C;EAC/E,MAAM,OACJ,UAAU,wBACV,UAAU,0BACV,UAAU,2BACV,UAAU,uBACN,MAAM,eAAe,UAAU;GAE7B,aAAa,SAAS,OAAO,UAAU;GACvC,MAAM,UAAU;GAChB,OAAO,UAAU;GACjB,QAAQ,UAAU;GAClB,QAAQ,UAAU;EACpB,CAAC,IACD;EAEN,IAAI,UAAU,WAAW,KAAK,OAAO,UAAU,UAAU,MAAM;GAC7D,cAAc,UAAU,CACtB,GAAG,OACH;IACE,MAAM,KAAK;IACX,MAAM,KAAK;IACX,MAAM,KAAK;IACX,OAAO,EAAE,uBAAuB,CAAC,GAAG,yBAAyB;GAC/D,CACF,CAAC;GAED,OAAO;EACT;EAEA,IAAI,UAAU,WAAW,KAAK,OAAO,UAAU,UAAU,MAAM;GAC7D,cAAc,UAAU,CACtB,GAAG,OACH;IACE,MAAM,KAAK;IACX,MAAM,KAAK;IACX,MAAM,KAAK;IACX,OAAO,EAAE,uBAAuB,CAAC,GAAG,yBAAyB;GAC/D,CACF,CAAC;GAED,OAAO;EACT;EAEA,OAAO;CACT;;CAGA,MAAM,eAAe,MAAY,aAAyC;EACxE,MAAM,QAAmB;GAEvB,MAAM,WAAW,SAAS,GAAG,KAAK,aAAa,GAAG,KAAK,KAAK,GAAG,KAAK;GACpE,MAAM,KAAK;GACX,MAAM,KAAK;GACX,UAAU;GACV,SAAS,KAAK,KAAK,WAAW,QAAQ,IAAI,IAAI,gBAAgB,IAAI,IAAI,KAAA;EACxE;EACA,IAAI,MAAM,SAAS,WAAW,IAAI,MAAM,OAAO;EAE/C,OAAO;CACT;;;;;;CAOA,MAAM,eAAe,OAAO,aAAwB;EAClD,MAAM,WAAW,MAAM,UAAU;EACjC,MAAM,MAAM,SAAS,OAAO,SAAS;EACrC,MAAM,OAAO,kBAAkB,QAAQ;EAEvC,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,MAAM;EAEhC,MAAM,SAAS,MAAM,mBAAmB,KAAK,IAAI;EAEjD,IAAI,CAAC,QAAQ;GACX,cAAc,UAAU,CACtB,GAAG,OACH;IACE,MAAM,UAAU,SAAS;IACzB,MAAM,SAAS;IACf,OAAO,EAAE,4BAA4B,CAAC,GAAG,gCAAgC;GAC3E,CACF,CAAC;GAED;EACF;EAEA,MAAM,SAAS,MAAM,UAAU,MAAM;EAGrC,IAAI,CAAC,UAAU,WAAW,QAAQ;EAElC,MAAM,OAAO,MAAM,QAAQ,QAAQ,IAAI;EAEvC,IAAI,CAAC,MAAM;EAEX,MAAM,QAAQ,YAAY,MAAM,QAAQ,SAAS,MAAM;EACvD,cAAc,UAAU,CAAC,GAAG,OAAO,KAAK,CAAC;EACzC,MAAM,KAAK,UAAU,MAAM,OAAO,SAAS,IAAI;CACjD;CAEA,MAAM,OAAO,OAAO,UAAkB,MAAY,OAAkB,cAAuB;EACzF,IAAI;GAEF,MAAM,WAAW,MAAM,oBAAoB,UAAU,MAAM;IACzD,GAAG,mBAAmB,OAAO,UAAU,YAAY;IACnD,aAAa,aACX,cAAc,UACZ,MAAM,KAAK,SAAU,KAAK,SAAS,MAAM,OAAO;KAAE,GAAG;KAAM;IAAS,IAAI,IAAK,CAC/E;GACJ,CAAC;GAED,aAAa,UAAU,CACrB,GAAG,OACH;IACE,MAAM,SAAS;IACf,MAAM,SAAS;IACf,MAAM,SAAS;IACf,WAAW;IACX,KAAK,MAAM;GACb,CACF,CAAC;GACD,cAAc,UAAU,MAAM,QAAQ,SAAS,KAAK,SAAS,MAAM,IAAI,CAAC;GAExE,MAAM,UAAU,gBACd,MAAM,SAAmC,UAAU,WAAW,IAAI,CACpE;GAEA,IAAI,cAAc,KAAA,KAAa,QAAQ,SAAS,SAAS,GAAG;IAE1D,OAAO,QAAQ,KAAK,SAAU,SAAS,YAAY,SAAS,MAAM,IAAK,CAAC;IACxE,QAAQ,SAAS;GACnB,OACE,OAAO,mBAAmB,SAAS,SAAS,KAAK,WAAW,QAAQ,CAAC;EAEzE,SAAS,aAAa;GACpB,MAAM,UACH,YAAsD,QAAQ,OAAO,MACrE,YAAsB;GACzB,cAAc,UACZ,MAAM,KAAK,SACT,KAAK,SAAS,MAAM,OAAO;IAAE,GAAG;IAAM,OAAO;IAAS,UAAU,KAAA;GAAU,IAAI,IAChF,CACF;EACF;CACF;CAEA,MAAM,UAAU,SAAiB;EAC/B,OAAO,MAAM,QAAQ,SAAS,SAAS,IAAI,CAAC;EAC5C,QAAQ,IAAI;CACd;;CAGA,MAAM,WAAW,SAAiB;EAChC,eAAe,cAAc,IAAI,CAAC,CAAC,MAAM,SAAS,KAAK,SAAS,IAAI,CAAC,EAAE,GAAG;EAE1E,IAAI,KAAK,WAAW,MAAM,KAAK,MAAM,UAAU,eAC7C,YAAiB,MAAM,UAAU,eAAe;GAC9C,QAAQ;GACR,MAAM,EAAE,KAAK,KAAK;EACpB,CAAC,CAAC,CAAC,YAAY,KAAA,CAAS;CAE5B;CAEA,MAAM,UAAU,UAAqC;EACnD,MAAM,eAAe;EACrB,YAAY,KAAK;EAEjB,IAAI,CAAC,UAAU,MAAM,aAAa,MAAM,SAAS,GAC/C,OAAY,MAAM,aAAa,KAAK;CAExC;CAGA,MAAM,WAAW,UAA0C;EACzD,IAAI,UAAU,UAAU,cAAc,OAAO;EAE7C,MAAM,SAAS,mBAAmB,MAAM,aAAa;EAErD,IAAI,OAAO,SAAS,GAAG;GACrB,MAAM,eAAe;GACrB,OAAY,MAAM;EACpB;CACF;CAEA,MAAM,QAAQ,MAAc,OAAe,OAAO,iBAAiB,OAAO,MAAM,EAAE,CAAa;CAC/F,MAAM,cAAc,QAAQ,UAAU,eAAe,UAAU,YAAY,CAAC,MAAM;CAGlF,MAAM,WAAW,SACf,UAAU,gBAAgB,UACzB,UAAU,SAAS,oCAAoC,KAAK,KAAK,IAAI;CACxE,MAAM,SAAS,UAAU,gBAAgB,YAAY,UAAU;CAC/D,MAAM,aAAa,UAAU,gBAAgB,gBAAgB,CAAC;CAC9D,MAAM,gBAAgB,UAAU,WAAW,iBAAiB;CAC5D,MAAM,kBAAkB,UAAU,WAAW,sBAAsB;CACnE,MAAM,cAAc,UAAU,WAAW,gBAAgB;CAGzD,MAAM,mBAAmB,MAAiB,UACxC,QAAQ,UAAU,WAAW,KAC7B,CAAC,UACD,QAAQ,MAAM,UACd,CAAC,KAAK,WACN,QAAQ,KAAK,OAAO,KAAK,OAAO,KAChC,kBAAkB,IAAI,MAAM;CAE9B,MAAM,gBAAgB,SACpB,UAAU,aACV,CAAC,UACC,oBAAC,QAAD;EACE,MAAK;EACL,SAAS,SAAS,cAAc;EAChC,MAAK;EACL,WAAW,GAAG,UAAU,qBAAqB;EAC7C,cAAY,EAAE,UAAU,CAAC,GAAG,QAAQ;EACpC,eAAe;GACb,IAAI,KAAK,SAAS,KAAK,aAAa,KAAA,GAAW;IAC7C,eAAe,KAAK,OAAO;IAC3B,cAAc,UAAU,MAAM,QAAQ,SAAS,KAAK,SAAS,KAAK,IAAI,CAAC;GACzE,OACE,OAAO,KAAK,IAAI;EAEpB;EAEA,UAAA,oBAAC,OAAD,CAAQ,CAAA;CACF,CAAA;CAGZ,OACE,oBAAC,cAAD;EAAc,OAAO;EAAW,SAAS;EAAW;EAClD,UAAA,qBAAC,OAAD;GACE,aAAU;GACV,eAAa,UAAU;GACvB,WAAW,GAAG,uBAAuB,cAAc,uBAAuB;GAH5E,UAAA;KAKI,MAAM,SAAS,KAAK,UAAU,SAAS,MACvC,oBAAC,MAAD;KACE,WAAW,GACT,cACA,UAAU,gBAAgB,UAAU,6CACpC,UAAU,sBACZ;KAEC,UAAA,CAAC,GAAG,OAAO,GAAG,SAAS,CAAC,CAAC,KAAK,MAAM,UACnC,qBAAC,MAAD;MAEE,WAAW,eAAe,QAAQ,MAAM;MACxC,mBAAmB,YAAY,KAAK,IAAI;MACxC,aAAa,UAAU,eAAe,MAAM,eAAe;MAC3D,SAAS,UAAU;OACjB,IAAI,CAAC,eAAe,aAAa,MAAM;OACvC,MAAM,eAAe;OACrB,MAAM,gBAAgB;OACtB,MAAM,OAAO,MAAM,QAAQ,QAAQ;OACnC,IAAI,SAAS,MAAM,QAAQ,MAAM,QAAQ,KAAK,MAAM,KAAK;OACzD,YAAY,IAAI;MAClB;MACA,iBAAiB,YAAY,IAAI;MACjC,WAAW,GACT,aAAa,KAAK,QAAQ,cAC1B,sGACA,UAAU,oDACV,KAAK,SAAS,oBAChB;MAnBF,UAAA;OAqBG,CAAC,UAAU,iBAAiB,aAAa,IAAI;OAC7C,CAAC,UAAU,mBAAmB,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,SAClE,oBAAC,SAAD,CAAU,CAAA;OAEX,QAAQ,IAAI,MAAM,KAAK,OAAO,KAAK,YAAY,CAAC,eAAe,IAAI,KAAK,IAAI,IAC3E,oBAAC,OAAD;QACE,KAAK,KAAK,OAAO,KAAK;QACtB,KAAI;QACJ,eAAe,mBAAmB,YAAY,IAAI,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC;QAC7E,WAAW,GACT,yCACA,UAAU,sBACZ;QACA,OACE,CAAC,UAAU,UAAU,qBACjB;SACE,QAAQ,UAAU,UAAU,kBAAkB;SAC9C,OAAO;SACP,UAAU;QACZ,IACA,KAAA;OAEP,CAAA,IAED,CAAC,UAAU,oBAAC,UAAD,EAAU,WAAU,wCAAyC,CAAA;OAEzE,CAAC,UACA,qBAAC,OAAD;QAAK,WAAU;QAAf,UAAA,CACE,oBAAC,KAAD;SAAG,WAAU;SAAwB,UAAA,KAAK;QAAQ,CAAA,GAClD,oBAAC,KAAD;SACE,WAAW,GACT,iCACA,KAAK,WAAW,kBAClB;SAEC,UAAA,KAAK,UACH,KAAK,UACF,EAAE,qBAAqB,CAAC,GAAG,6BAA6B,IACxD,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,QAClC,UAAU,oBAAoB,EAAE,kBAAkB,CAAC,GAAG,YAAY,IACnE,KAAK,OACH,YAAY,KAAK,IAAI,IACrB,KAAK,YACH,EAAE,kBAAkB,CAAC,GAAG,SAAS,IACjC;QACX,CAAA,CACA;;OAEP,qBAAC,OAAD;QACE,WAAW,GAAG,2BAA2B,UAAU,2BAA2B;QADhF,UAAA;UAGI,UAAU,CAAC,oBAAoB,KAAK,aAAa,KAAA,KAAa,CAAC,KAAK,SACpE,oBAAC,SAAD,CAAU,CAAA;SAEX,eAAe,QAAQ,MAAM,UAC5B,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,QAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,cAAY,EAAE,gBAAgB,CAAC,GAAG,SAAS;UAC3C,UAAU,UAAU;UACpB,eAAe,KAAK,OAAO,QAAQ,CAAC;UAEpC,UAAA,oBAAC,eAAD,CAAgB,CAAA;SACV,CAAA,GACR,oBAAC,QAAD;UACE,MAAK;UACL,SAAQ;UACR,MAAK;UACL,cAAY,EAAE,kBAAkB,CAAC,GAAG,WAAW;UAC/C,UAAU,UAAU,MAAM,SAAS;UACnC,eAAe,KAAK,OAAO,QAAQ,CAAC;UAEpC,UAAA,oBAAC,iBAAD,CAAkB,CAAA;SACZ,CAAA,CACR,EAAA,CAAA;SAEH,gBAAgB,MAAM,KAAK,KAC1B,oBAAC,QAAD;UACE,MAAK;UACL,SAAS,SAAS,cAAc;UAChC,MAAK;UACL,WAAW,GAAG,UAAU,qBAAqB;UAC7C,cAAY,EAAE,sBAAsB,CAAC,GAAG,YAAY;UACpD,OAAO,EAAE,sBAAsB,CAAC,GAAG,YAAY;UAC/C,eAAY;UACZ,eAAe,KAAK,aAAa,IAAI;UAErC,UAAA,oBAAC,YAAD,CAAa,CAAA;SACP,CAAA;SAET,UAAU,YAAY,kBAAkB,IAAI,CAAC,CAAC,QAC7C,oBAAC,QAAD;UAAQ,SAAA;UAAQ,MAAK;UAAS,SAAQ;UAAQ,MAAK;UACjD,UAAA,oBAAC,KAAD;WACE,MAAM,kBAAkB,IAAI,CAAC,CAAC;WAC9B,QAAO;WACP,KAAI;WACJ,cAAY,GAAG,EAAE,QAAQ,CAAC,GAAG,MAAM,EAAE,GAAG,EAAE,oBAAoB,CAAC,GAAG,sBAAsB;WAExF,UAAA,oBAAC,kBAAD,CAAmB,CAAA;UAClB,CAAA;SACG,CAAA;SAET,UAAU,gBAAgB,kBAAkB,IAAI,CAAC,CAAC,YACjD,oBAAC,QAAD;UAAQ,SAAA;UAAQ,MAAK;UAAS,SAAQ;UAAQ,MAAK;UACjD,UAAA,oBAAC,KAAD;WACE,MAAM,kBAAkB,IAAI,CAAC,CAAC;WAC9B,UAAA;WACA,cAAY,EAAE,YAAY,CAAC,GAAG,UAAU;WAExC,UAAA,oBAAC,cAAD,CAAe,CAAA;UACd,CAAA;SACG,CAAA;UAER,UAAU,CAAC,kBAAkB,aAAa,IAAI;QAC7C;;MACH;KAzIG,GAAA,KAAK,IAyIR,CACL;IACC,CAAA;IAEL,cAAc,CAAC,UACd,qBAAC,OAAD;KACE,MAAK;KACL,UAAU;KACV,eAAe,SAAS,SAAS,MAAM;KACvC,oBAAkB,KAAK;KACvB,YAAY,UAAU;MACpB,IAAI,MAAM,QAAQ,WAAW,MAAM,QAAQ,KAAK;OAC9C,MAAM,eAAe;OACrB,SAAS,SAAS,MAAM;MAC1B;KACF;KACA,aAAa,UAAU;MACrB,MAAM,eAAe;MACrB,YAAY,IAAI;KAClB;KACA,mBAAmB,YAAY,KAAK;KAC5B;KACC;KACT,OACE,CAAC,UAAU,UAAU,mBACjB,EAAE,aAAa,eAAe,UAAU,gBAAgB,EAAE,IAC1D,KAAA;KAEN,cAAY;KACZ,WAAW,GACT,iKACA,gBAAgB,UACZ,2BACA,gBAAgB,QACd,uBACA,4BACN,cAAc,sBACd,YAAY,yBACZ,UAAU,wCACZ;KAlCF,UAAA;MAoCE,oBAAC,iBAAD,EAAiB,WAAU,SAAU,CAAA;MACpC,CAAC,UACA,oBAAC,QAAD,EAAA,UACG,UAAU,eACT,EAAE,mBAAmB,CAAC,GAAG,oCAAoC,EAC3D,CAAA;MAEP,CAAC,WACC,UAAU,qBAAqB,UAAU,WAAW,UAAU,aAC7D,oBAAC,QAAD;OAAM,WAAU;OACb,UAAA;QACC,UAAU,mBAAmB,KAAK,IAAI;QACtC,UAAU,UAAU,KAAK,YAAY,UAAU,UAAU,MAAM,CAAC,MAAM;QACtE,UAAU,WACN,OACE,kBACA,UAAU,UACV,CAAC,GACD,4CACF,IACA;OACN,CAAC,CACE,OAAO,OAAO,CAAC,CACf,KAAK,KAAK;MACT,CAAA;KAEP;;IAEP,oBAAC,SAAD;KACE,KAAK;KACD;KACJ,MAAK;KACL,WAAU;KACV,UAAU;KACV,GAAI;KACJ,UAAU,UAAU;KACpB,QAAQ,iBAAiB,SAAS;KAClC,UAAU;KACV,WAAW,UAAU;MAEnB,MAAM,SAAS,MAAM,OAAO,QAAQ,MAAM,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC;MACtE,MAAM,OAAO,QAAQ;MACrB,OAAY,MAAM;KACpB;IACD,CAAA;IACA,UAAU,eACT,oBAAC,mBAAD;KAAmB,SAAS;KAAS,SAAS,UAAU;IAAc,CAAA;GAErE;;CACO,CAAA;AAElB;;AAGA,SAAS,UAAU,OAAgC;CACjD,OAAO,OAAO,UAAU,YAAY,gBAAgB,KAAK,KAAK,IAAI,GAAG,MAAM,MAAM;AACnF"}
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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"}
@@ -5,7 +5,7 @@ import { useStore } from "../../hooks/use-store.js";
5
5
  import { Spinner } from "../ui/spinner.js";
6
6
  import { Input } from "../ui/input.js";
7
7
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "../ui/dialog.js";
8
- import { ImageEditor, loadEditorImage } from "@alfakit/core";
8
+ import { ImageEditor, imageEditorOptionsKey, loadEditorImage } from "@alfakit/core";
9
9
  import { useEffect, useId, useRef, useState } from "react";
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
11
  import { FlipHorizontal2Icon, FlipVertical2Icon, RotateCcwIcon, RotateCwIcon, Undo2Icon, ZoomInIcon, ZoomOutIcon } from "lucide-react";
@@ -26,8 +26,10 @@ function ImageEditorDialog({ request, options }) {
26
26
  const [loaded, setLoaded] = useState(null);
27
27
  const [saving, setSaving] = useState(false);
28
28
  const hintId = useId();
29
+ const optionsKey = imageEditorOptionsKey(options);
29
30
  useEffect(() => {
30
31
  if (!request) return;
32
+ const options = JSON.parse(optionsKey);
31
33
  let cancelled = false;
32
34
  loadEditorImage(request.file).then((bitmap) => {
33
35
  if (cancelled) return bitmap.close();
@@ -52,7 +54,7 @@ function ImageEditorDialog({ request, options }) {
52
54
  return null;
53
55
  });
54
56
  };
55
- }, [request, options]);
57
+ }, [request, optionsKey]);
56
58
  const finish = (file) => {
57
59
  request?.resolve(file);
58
60
  };
@@ -1 +1 @@
1
- {"version":3,"file":"image-editor-dialog.js","names":[],"sources":["../../../src/components/fields/image-editor-dialog.tsx"],"sourcesContent":["import type {\n CropHandle,\n FileUploadImageEditorSchema,\n ImageEditorData,\n ImageEditorDragMode,\n} from '@alfakit/core'\nimport { ImageEditor, loadEditorImage } from '@alfakit/core'\nimport {\n FlipHorizontal2Icon,\n FlipVertical2Icon,\n RotateCcwIcon,\n RotateCwIcon,\n Undo2Icon,\n ZoomInIcon,\n ZoomOutIcon,\n} from 'lucide-react'\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type FocusEvent,\n type KeyboardEvent,\n type PointerEvent,\n type ReactNode,\n} from 'react'\nimport { useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../ui/dialog'\nimport { Input } from '../ui/input'\nimport { Spinner } from '../ui/spinner'\n\nexport interface ImageEditRequest {\n file: File\n resolve: (file: File | null) => void\n}\n\nconst HANDLES: CropHandle[] = ['nw', 'ne', 'sw', 'se']\n\n/**\n * `FileUpload::imageEditor()`: crop, zoom, rotate and flip an image before it is uploaded\n * (or to replace an uploaded one). Resolves the request with the edited file, or null\n * when cancelled.\n */\nexport function ImageEditorDialog({\n request,\n options,\n}: {\n request: ImageEditRequest | null\n options: FileUploadImageEditorSchema\n}) {\n const t = useT()\n const [loaded, setLoaded] = useState<{ bitmap: ImageBitmap; editor: ImageEditor } | null>(null)\n const [saving, setSaving] = useState(false)\n const hintId = useId()\n\n useEffect(() => {\n if (!request) return\n\n let cancelled = false\n\n loadEditorImage(request.file)\n .then((bitmap) => {\n if (cancelled) return bitmap.close()\n\n setLoaded({\n bitmap,\n editor: new ImageEditor({\n width: bitmap.width,\n height: bitmap.height,\n aspectRatios: options.aspectRatios,\n circle: options.circle,\n mode: options.mode,\n emptyFillColor: options.emptyFillColor,\n viewportWidth: options.viewportWidth,\n viewportHeight: options.viewportHeight,\n }),\n })\n })\n // Undecodable: upload the file as it is.\n .catch(() => !cancelled && request.resolve(request.file))\n\n return () => {\n cancelled = true\n setLoaded((current) => {\n current?.bitmap.close()\n\n return null\n })\n }\n }, [request, options])\n\n const finish = (file: File | null) => {\n request?.resolve(file)\n }\n\n const save = async () => {\n if (!request || !loaded) return\n\n setSaving(true)\n\n try {\n finish(await loaded.editor.export(loaded.bitmap, request.file))\n } catch {\n finish(request.file)\n } finally {\n setSaving(false)\n }\n }\n\n return (\n <Dialog open={request !== null} onOpenChange={(open) => !open && finish(null)}>\n <DialogContent className=\"sm:max-w-3xl\" data-slot=\"image-editor\">\n <DialogHeader>\n <DialogTitle>{t('image_editor.title', {}, 'Edit image')}</DialogTitle>\n <DialogDescription id={hintId} className=\"sr-only\">\n {t(\n 'image_editor.keyboard_hint',\n {},\n 'Arrow keys move the crop area. Shift and arrow keys resize it.',\n )}{' '}\n {t('image_editor.zoom_hint', {}, 'Plus and minus keys zoom the image.')}\n </DialogDescription>\n </DialogHeader>\n {loaded ? (\n <ImageEditorCanvas editor={loaded.editor} bitmap={loaded.bitmap} hintId={hintId} />\n ) : (\n <div className=\"flex h-48 items-center justify-center\">\n <Spinner />\n </div>\n )}\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => finish(null)}>\n {t('image_editor.cancel', {}, 'Cancel')}\n </Button>\n <Button type=\"button\" disabled={!loaded || saving} onClick={() => void save()}>\n {saving && <Spinner />}\n {t('image_editor.save', {}, 'Apply')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\n/** The numeric inputs, in the order Filament shows them. */\nconst NUMERIC_FIELDS = [\n ['x', 'image_editor.x', 'X'],\n ['y', 'image_editor.y', 'Y'],\n ['width', 'image_editor.width', 'Width'],\n ['height', 'image_editor.height', 'Height'],\n ['rotate', 'image_editor.rotation', 'Rotation'],\n] as const satisfies ReadonlyArray<readonly [keyof ImageEditorData, string, string]>\n\nfunction ImageEditorCanvas({\n editor,\n bitmap,\n hintId,\n}: {\n editor: ImageEditor\n bitmap: ImageBitmap\n hintId: string\n}) {\n const t = useT()\n const state = useStore(editor.store)\n const canvas = useRef<HTMLCanvasElement>(null)\n const frame = useRef<HTMLDivElement>(null)\n const inputsId = useId()\n\n // Measure the container, and follow its size.\n useEffect(() => {\n const element = frame.current\n\n if (!element) return\n\n const measure = () => editor.setContainer(element.clientWidth, element.clientHeight)\n measure()\n\n if (typeof ResizeObserver === 'undefined') return\n\n const observer = new ResizeObserver(measure)\n observer.observe(element)\n\n return () => observer.disconnect()\n }, [editor])\n\n // Wheel and trackpad pinch zoom. A native listener: React's wheel listener is passive.\n useEffect(() => {\n const element = frame.current\n\n if (!element) return\n\n const onWheel = (event: WheelEvent) => {\n const rect = element.getBoundingClientRect()\n\n if (editor.wheel(event, { x: event.clientX - rect.left, y: event.clientY - rect.top })) {\n event.preventDefault()\n }\n }\n\n element.addEventListener('wheel', onWheel, { passive: false })\n\n return () => element.removeEventListener('wheel', onWheel)\n }, [editor])\n\n useEffect(() => {\n if (canvas.current) {\n editor.drawPreview(canvas.current, bitmap, globalThis.devicePixelRatio || 1)\n }\n }, [editor, bitmap, state])\n\n const box = editor.cropBox()\n const data = editor.getData()\n\n const start = (mode: ImageEditorDragMode) => (event: PointerEvent<HTMLElement>) => {\n event.preventDefault()\n event.stopPropagation()\n event.currentTarget.setPointerCapture?.(event.pointerId)\n editor.startDrag(mode, event.clientX, event.clientY, undefined, event.pointerId ?? 0)\n }\n\n const end = (event: PointerEvent<HTMLElement>) => editor.endDrag(event.pointerId ?? 0)\n\n // Numeric inputs apply on blur and Enter; invalid values snap back to the current one.\n const commit =\n (field: keyof ImageEditorData) =>\n (event: FocusEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement>) => {\n const input = event.currentTarget\n const value = input.value.trim() === '' ? Number.NaN : Number(input.value)\n\n editor.setData({ [field]: value })\n input.value = String(editor.getData()[field])\n }\n\n const ratioLabel = (ratio: string | null) => ratio ?? t('image_editor.free', {}, 'Free')\n\n const tool = (label: string, icon: ReactNode, onClick: () => void, disabled = false) => (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n >\n {icon}\n </Button>\n )\n\n return (\n <div className=\"flex flex-col gap-4 md:flex-row\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-3\">\n {/* Image space: left is the image's left in every reading direction. */}\n <div\n ref={frame}\n dir=\"ltr\"\n data-slot=\"image-editor-viewport\"\n className=\"bg-muted/40 relative h-72 w-full cursor-grab touch-none overflow-hidden rounded-md select-none sm:h-96\"\n onPointerDown={start('pan')}\n onPointerMove={(event) =>\n editor.dragTo(event.clientX, event.clientY, event.pointerId ?? 0)\n }\n onPointerUp={end}\n onPointerCancel={end}\n >\n <canvas ref={canvas} className=\"pointer-events-none absolute inset-0 block size-full\" />\n <div\n role=\"group\"\n tabIndex={0}\n aria-label={t('image_editor.crop_area', {}, 'Crop area')}\n aria-describedby={hintId}\n data-slot=\"image-editor-crop\"\n className={cn(\n 'ring-ring/50 absolute cursor-move border-2 border-white shadow-[0_0_0_9999px_rgb(0_0_0/0.5)] outline-none focus-visible:ring-[3px]',\n editor.circle && 'rounded-full',\n )}\n // Runtime geometry: no Tailwind class can hold these values.\n style={{\n left: `${box.left}%`,\n top: `${box.top}%`,\n width: `${box.width}%`,\n height: `${box.height}%`,\n }}\n onPointerDown={start('move')}\n onKeyDown={(event) => {\n if (editor.handleKey(event)) event.preventDefault()\n }}\n >\n {HANDLES.map((handle) => (\n <span\n key={handle}\n data-handle={handle}\n aria-hidden=\"true\"\n className=\"absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-black/40 bg-white\"\n style={{\n left: handle.endsWith('w') ? '0%' : '100%',\n top: handle.startsWith('n') ? '0%' : '100%',\n cursor: handle === 'nw' || handle === 'se' ? 'nwse-resize' : 'nesw-resize',\n }}\n onPointerDown={start(handle)}\n />\n ))}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {editor.aspectRatios.length > 1 ? (\n <div\n role=\"group\"\n aria-label={t('image_editor.aspect_ratio', {}, 'Aspect ratio')}\n className=\"flex flex-wrap gap-1\"\n >\n {editor.aspectRatios.map((ratio) => (\n <Button\n key={ratio ?? 'free'}\n type=\"button\"\n size=\"sm\"\n variant={state.aspectRatio === ratio ? 'default' : 'outline'}\n aria-pressed={state.aspectRatio === ratio}\n onClick={() => editor.setAspectRatio(ratio)}\n >\n {ratioLabel(ratio)}\n </Button>\n ))}\n </div>\n ) : (\n <span />\n )}\n <div className=\"flex flex-wrap gap-1\">\n {tool(\n t('image_editor.zoom_out', {}, 'Zoom out'),\n <ZoomOutIcon />,\n () => editor.zoomOut(),\n !editor.canZoomOut,\n )}\n {tool(\n t('image_editor.zoom_in', {}, 'Zoom in'),\n <ZoomInIcon />,\n () => editor.zoomIn(),\n !editor.canZoomIn,\n )}\n {tool(t('image_editor.rotate_left', {}, 'Rotate left'), <RotateCcwIcon />, () =>\n editor.rotate(-1),\n )}\n {tool(t('image_editor.rotate_right', {}, 'Rotate right'), <RotateCwIcon />, () =>\n editor.rotate(1),\n )}\n {tool(\n t('image_editor.flip_horizontal', {}, 'Flip horizontally'),\n <FlipHorizontal2Icon />,\n () => editor.flip('x'),\n )}\n {tool(\n t('image_editor.flip_vertical', {}, 'Flip vertically'),\n <FlipVertical2Icon />,\n () => editor.flip('y'),\n )}\n {tool(t('image_editor.reset', {}, 'Reset'), <Undo2Icon />, () => editor.reset())}\n </div>\n </div>\n </div>\n {/* Numeric inputs: beside the image from md up, under it on phones. */}\n <div\n role=\"group\"\n aria-label={t('image_editor.inputs', {}, 'Position and size')}\n data-slot=\"image-editor-inputs\"\n className=\"grid grid-cols-2 content-start gap-x-3 gap-y-2 sm:grid-cols-4 md:w-40 md:shrink-0 md:grid-cols-1\"\n >\n {NUMERIC_FIELDS.map(([field, key, fallback]) => (\n <div key={field} className=\"flex flex-col gap-1\">\n <label htmlFor={`${inputsId}-${field}`} className=\"text-muted-foreground text-xs\">\n {t(key, {}, fallback)}\n </label>\n <Input\n // Remount with the new value when the editor changes it (drag, zoom…).\n key={`${field}:${data[field]}`}\n id={`${inputsId}-${field}`}\n type=\"number\"\n inputMode=\"decimal\"\n step={1}\n data-field={field}\n className=\"h-8 tabular-nums\"\n defaultValue={data[field]}\n onBlur={commit(field)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n commit(field)(event)\n }\n }}\n />\n </div>\n ))}\n <div className=\"col-span-2 flex gap-1 sm:col-span-4 md:col-span-1\">\n {(\n [\n ['x', 'image_editor.scale_x', 'Scale X'],\n ['y', 'image_editor.scale_y', 'Scale Y'],\n ] as const\n ).map(([axis, key, fallback]) => {\n const flipped = axis === 'x' ? state.flipX : state.flipY\n\n return (\n <Button\n key={axis}\n type=\"button\"\n size=\"sm\"\n variant={flipped ? 'default' : 'outline'}\n aria-pressed={flipped}\n data-field={`scale${axis.toUpperCase()}`}\n className=\"flex-1\"\n onClick={() => editor.flip(axis)}\n >\n {t(key, {}, fallback)}\n </Button>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"image-editor-dialog.js","names":[],"sources":["../../../src/components/fields/image-editor-dialog.tsx"],"sourcesContent":["import type {\n CropHandle,\n FileUploadImageEditorSchema,\n ImageEditorData,\n ImageEditorDragMode,\n} from '@alfakit/core'\nimport { ImageEditor, imageEditorOptionsKey, loadEditorImage } from '@alfakit/core'\nimport {\n FlipHorizontal2Icon,\n FlipVertical2Icon,\n RotateCcwIcon,\n RotateCwIcon,\n Undo2Icon,\n ZoomInIcon,\n ZoomOutIcon,\n} from 'lucide-react'\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type FocusEvent,\n type KeyboardEvent,\n type PointerEvent,\n type ReactNode,\n} from 'react'\nimport { useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { cn } from '../../lib/utils'\nimport { Button } from '../ui/button'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../ui/dialog'\nimport { Input } from '../ui/input'\nimport { Spinner } from '../ui/spinner'\n\nexport interface ImageEditRequest {\n file: File\n resolve: (file: File | null) => void\n}\n\nconst HANDLES: CropHandle[] = ['nw', 'ne', 'sw', 'se']\n\n/**\n * `FileUpload::imageEditor()`: crop, zoom, rotate and flip an image before it is uploaded\n * (or to replace an uploaded one). Resolves the request with the edited file, or null\n * when cancelled.\n */\nexport function ImageEditorDialog({\n request,\n options,\n}: {\n request: ImageEditRequest | null\n options: FileUploadImageEditorSchema\n}) {\n const t = useT()\n const [loaded, setLoaded] = useState<{ bitmap: ImageBitmap; editor: ImageEditor } | null>(null)\n const [saving, setSaving] = useState(false)\n const hintId = useId()\n\n // Restart only for another image or other settings: the schema object itself is new on\n // every render of a template item (a new builder block), which kept restarting the load.\n const optionsKey = imageEditorOptionsKey(options)\n\n useEffect(() => {\n if (!request) return\n\n const options = JSON.parse(optionsKey) as FileUploadImageEditorSchema\n let cancelled = false\n\n loadEditorImage(request.file)\n .then((bitmap) => {\n if (cancelled) return bitmap.close()\n\n setLoaded({\n bitmap,\n editor: new ImageEditor({\n width: bitmap.width,\n height: bitmap.height,\n aspectRatios: options.aspectRatios,\n circle: options.circle,\n mode: options.mode,\n emptyFillColor: options.emptyFillColor,\n viewportWidth: options.viewportWidth,\n viewportHeight: options.viewportHeight,\n }),\n })\n })\n // Undecodable: upload the file as it is.\n .catch(() => !cancelled && request.resolve(request.file))\n\n return () => {\n cancelled = true\n setLoaded((current) => {\n current?.bitmap.close()\n\n return null\n })\n }\n }, [request, optionsKey])\n\n const finish = (file: File | null) => {\n request?.resolve(file)\n }\n\n const save = async () => {\n if (!request || !loaded) return\n\n setSaving(true)\n\n try {\n finish(await loaded.editor.export(loaded.bitmap, request.file))\n } catch {\n finish(request.file)\n } finally {\n setSaving(false)\n }\n }\n\n return (\n <Dialog open={request !== null} onOpenChange={(open) => !open && finish(null)}>\n <DialogContent className=\"sm:max-w-3xl\" data-slot=\"image-editor\">\n <DialogHeader>\n <DialogTitle>{t('image_editor.title', {}, 'Edit image')}</DialogTitle>\n <DialogDescription id={hintId} className=\"sr-only\">\n {t(\n 'image_editor.keyboard_hint',\n {},\n 'Arrow keys move the crop area. Shift and arrow keys resize it.',\n )}{' '}\n {t('image_editor.zoom_hint', {}, 'Plus and minus keys zoom the image.')}\n </DialogDescription>\n </DialogHeader>\n {loaded ? (\n <ImageEditorCanvas editor={loaded.editor} bitmap={loaded.bitmap} hintId={hintId} />\n ) : (\n <div className=\"flex h-48 items-center justify-center\">\n <Spinner />\n </div>\n )}\n <DialogFooter>\n <Button type=\"button\" variant=\"outline\" onClick={() => finish(null)}>\n {t('image_editor.cancel', {}, 'Cancel')}\n </Button>\n <Button type=\"button\" disabled={!loaded || saving} onClick={() => void save()}>\n {saving && <Spinner />}\n {t('image_editor.save', {}, 'Apply')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\n/** The numeric inputs, in the order Filament shows them. */\nconst NUMERIC_FIELDS = [\n ['x', 'image_editor.x', 'X'],\n ['y', 'image_editor.y', 'Y'],\n ['width', 'image_editor.width', 'Width'],\n ['height', 'image_editor.height', 'Height'],\n ['rotate', 'image_editor.rotation', 'Rotation'],\n] as const satisfies ReadonlyArray<readonly [keyof ImageEditorData, string, string]>\n\nfunction ImageEditorCanvas({\n editor,\n bitmap,\n hintId,\n}: {\n editor: ImageEditor\n bitmap: ImageBitmap\n hintId: string\n}) {\n const t = useT()\n const state = useStore(editor.store)\n const canvas = useRef<HTMLCanvasElement>(null)\n const frame = useRef<HTMLDivElement>(null)\n const inputsId = useId()\n\n // Measure the container, and follow its size.\n useEffect(() => {\n const element = frame.current\n\n if (!element) return\n\n const measure = () => editor.setContainer(element.clientWidth, element.clientHeight)\n measure()\n\n if (typeof ResizeObserver === 'undefined') return\n\n const observer = new ResizeObserver(measure)\n observer.observe(element)\n\n return () => observer.disconnect()\n }, [editor])\n\n // Wheel and trackpad pinch zoom. A native listener: React's wheel listener is passive.\n useEffect(() => {\n const element = frame.current\n\n if (!element) return\n\n const onWheel = (event: WheelEvent) => {\n const rect = element.getBoundingClientRect()\n\n if (editor.wheel(event, { x: event.clientX - rect.left, y: event.clientY - rect.top })) {\n event.preventDefault()\n }\n }\n\n element.addEventListener('wheel', onWheel, { passive: false })\n\n return () => element.removeEventListener('wheel', onWheel)\n }, [editor])\n\n useEffect(() => {\n if (canvas.current) {\n editor.drawPreview(canvas.current, bitmap, globalThis.devicePixelRatio || 1)\n }\n }, [editor, bitmap, state])\n\n const box = editor.cropBox()\n const data = editor.getData()\n\n const start = (mode: ImageEditorDragMode) => (event: PointerEvent<HTMLElement>) => {\n event.preventDefault()\n event.stopPropagation()\n event.currentTarget.setPointerCapture?.(event.pointerId)\n editor.startDrag(mode, event.clientX, event.clientY, undefined, event.pointerId ?? 0)\n }\n\n const end = (event: PointerEvent<HTMLElement>) => editor.endDrag(event.pointerId ?? 0)\n\n // Numeric inputs apply on blur and Enter; invalid values snap back to the current one.\n const commit =\n (field: keyof ImageEditorData) =>\n (event: FocusEvent<HTMLInputElement> | KeyboardEvent<HTMLInputElement>) => {\n const input = event.currentTarget\n const value = input.value.trim() === '' ? Number.NaN : Number(input.value)\n\n editor.setData({ [field]: value })\n input.value = String(editor.getData()[field])\n }\n\n const ratioLabel = (ratio: string | null) => ratio ?? t('image_editor.free', {}, 'Free')\n\n const tool = (label: string, icon: ReactNode, onClick: () => void, disabled = false) => (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n aria-label={label}\n title={label}\n disabled={disabled}\n onClick={onClick}\n >\n {icon}\n </Button>\n )\n\n return (\n <div className=\"flex flex-col gap-4 md:flex-row\">\n <div className=\"flex min-w-0 flex-1 flex-col gap-3\">\n {/* Image space: left is the image's left in every reading direction. */}\n <div\n ref={frame}\n dir=\"ltr\"\n data-slot=\"image-editor-viewport\"\n className=\"bg-muted/40 relative h-72 w-full cursor-grab touch-none overflow-hidden rounded-md select-none sm:h-96\"\n onPointerDown={start('pan')}\n onPointerMove={(event) =>\n editor.dragTo(event.clientX, event.clientY, event.pointerId ?? 0)\n }\n onPointerUp={end}\n onPointerCancel={end}\n >\n <canvas ref={canvas} className=\"pointer-events-none absolute inset-0 block size-full\" />\n <div\n role=\"group\"\n tabIndex={0}\n aria-label={t('image_editor.crop_area', {}, 'Crop area')}\n aria-describedby={hintId}\n data-slot=\"image-editor-crop\"\n className={cn(\n 'ring-ring/50 absolute cursor-move border-2 border-white shadow-[0_0_0_9999px_rgb(0_0_0/0.5)] outline-none focus-visible:ring-[3px]',\n editor.circle && 'rounded-full',\n )}\n // Runtime geometry: no Tailwind class can hold these values.\n style={{\n left: `${box.left}%`,\n top: `${box.top}%`,\n width: `${box.width}%`,\n height: `${box.height}%`,\n }}\n onPointerDown={start('move')}\n onKeyDown={(event) => {\n if (editor.handleKey(event)) event.preventDefault()\n }}\n >\n {HANDLES.map((handle) => (\n <span\n key={handle}\n data-handle={handle}\n aria-hidden=\"true\"\n className=\"absolute size-3 -translate-x-1/2 -translate-y-1/2 rounded-sm border border-black/40 bg-white\"\n style={{\n left: handle.endsWith('w') ? '0%' : '100%',\n top: handle.startsWith('n') ? '0%' : '100%',\n cursor: handle === 'nw' || handle === 'se' ? 'nwse-resize' : 'nesw-resize',\n }}\n onPointerDown={start(handle)}\n />\n ))}\n </div>\n </div>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n {editor.aspectRatios.length > 1 ? (\n <div\n role=\"group\"\n aria-label={t('image_editor.aspect_ratio', {}, 'Aspect ratio')}\n className=\"flex flex-wrap gap-1\"\n >\n {editor.aspectRatios.map((ratio) => (\n <Button\n key={ratio ?? 'free'}\n type=\"button\"\n size=\"sm\"\n variant={state.aspectRatio === ratio ? 'default' : 'outline'}\n aria-pressed={state.aspectRatio === ratio}\n onClick={() => editor.setAspectRatio(ratio)}\n >\n {ratioLabel(ratio)}\n </Button>\n ))}\n </div>\n ) : (\n <span />\n )}\n <div className=\"flex flex-wrap gap-1\">\n {tool(\n t('image_editor.zoom_out', {}, 'Zoom out'),\n <ZoomOutIcon />,\n () => editor.zoomOut(),\n !editor.canZoomOut,\n )}\n {tool(\n t('image_editor.zoom_in', {}, 'Zoom in'),\n <ZoomInIcon />,\n () => editor.zoomIn(),\n !editor.canZoomIn,\n )}\n {tool(t('image_editor.rotate_left', {}, 'Rotate left'), <RotateCcwIcon />, () =>\n editor.rotate(-1),\n )}\n {tool(t('image_editor.rotate_right', {}, 'Rotate right'), <RotateCwIcon />, () =>\n editor.rotate(1),\n )}\n {tool(\n t('image_editor.flip_horizontal', {}, 'Flip horizontally'),\n <FlipHorizontal2Icon />,\n () => editor.flip('x'),\n )}\n {tool(\n t('image_editor.flip_vertical', {}, 'Flip vertically'),\n <FlipVertical2Icon />,\n () => editor.flip('y'),\n )}\n {tool(t('image_editor.reset', {}, 'Reset'), <Undo2Icon />, () => editor.reset())}\n </div>\n </div>\n </div>\n {/* Numeric inputs: beside the image from md up, under it on phones. */}\n <div\n role=\"group\"\n aria-label={t('image_editor.inputs', {}, 'Position and size')}\n data-slot=\"image-editor-inputs\"\n className=\"grid grid-cols-2 content-start gap-x-3 gap-y-2 sm:grid-cols-4 md:w-40 md:shrink-0 md:grid-cols-1\"\n >\n {NUMERIC_FIELDS.map(([field, key, fallback]) => (\n <div key={field} className=\"flex flex-col gap-1\">\n <label htmlFor={`${inputsId}-${field}`} className=\"text-muted-foreground text-xs\">\n {t(key, {}, fallback)}\n </label>\n <Input\n // Remount with the new value when the editor changes it (drag, zoom…).\n key={`${field}:${data[field]}`}\n id={`${inputsId}-${field}`}\n type=\"number\"\n inputMode=\"decimal\"\n step={1}\n data-field={field}\n className=\"h-8 tabular-nums\"\n defaultValue={data[field]}\n onBlur={commit(field)}\n onKeyDown={(event) => {\n if (event.key === 'Enter') {\n event.preventDefault()\n commit(field)(event)\n }\n }}\n />\n </div>\n ))}\n <div className=\"col-span-2 flex gap-1 sm:col-span-4 md:col-span-1\">\n {(\n [\n ['x', 'image_editor.scale_x', 'Scale X'],\n ['y', 'image_editor.scale_y', 'Scale Y'],\n ] as const\n ).map(([axis, key, fallback]) => {\n const flipped = axis === 'x' ? state.flipX : state.flipY\n\n return (\n <Button\n key={axis}\n type=\"button\"\n size=\"sm\"\n variant={flipped ? 'default' : 'outline'}\n aria-pressed={flipped}\n data-field={`scale${axis.toUpperCase()}`}\n className=\"flex-1\"\n onClick={() => editor.flip(axis)}\n >\n {t(key, {}, fallback)}\n </Button>\n )\n })}\n </div>\n </div>\n </div>\n )\n}\n"],"mappings":"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"}