@alfakit/react 0.1.2 → 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 (171) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/components/actions/action-modals.js +4 -0
  3. package/dist/components/actions/action-modals.js.map +1 -1
  4. package/dist/components/ai/copilot.d.ts +3 -1
  5. package/dist/components/ai/copilot.d.ts.map +1 -1
  6. package/dist/components/ai/copilot.js +4 -1
  7. package/dist/components/ai/copilot.js.map +1 -1
  8. package/dist/components/cms/live-preview.js +3 -2
  9. package/dist/components/cms/live-preview.js.map +1 -1
  10. package/dist/components/cms/page.js +1 -1
  11. package/dist/components/fields/builder.js +15 -6
  12. package/dist/components/fields/builder.js.map +1 -1
  13. package/dist/components/fields/image-editor-dialog.js +4 -2
  14. package/dist/components/fields/image-editor-dialog.js.map +1 -1
  15. package/dist/components/fields/item-actions.js +10 -10
  16. package/dist/components/fields/item-actions.js.map +1 -1
  17. package/dist/components/fields/key-value.d.ts.map +1 -1
  18. package/dist/components/fields/key-value.js +3 -3
  19. package/dist/components/fields/key-value.js.map +1 -1
  20. package/dist/components/fields/modal-table-select.js +6 -5
  21. package/dist/components/fields/modal-table-select.js.map +1 -1
  22. package/dist/components/fields/repeater.js +2 -2
  23. package/dist/components/fields/repeater.js.map +1 -1
  24. package/dist/components/fields/rich-editor.js +2 -1
  25. package/dist/components/fields/rich-editor.js.map +1 -1
  26. package/dist/components/form/form-block.js +1 -1
  27. package/dist/components/form/form-block.js.map +1 -1
  28. package/dist/components/form/form-context.js +2 -2
  29. package/dist/components/form/form-context.js.map +1 -1
  30. package/dist/components/kanban/kanban.js +4 -3
  31. package/dist/components/kanban/kanban.js.map +1 -1
  32. package/dist/components/layout/section.js +2 -2
  33. package/dist/components/layout/section.js.map +1 -1
  34. package/dist/components/panel/app-sidebar.d.ts +2 -3
  35. package/dist/components/panel/app-sidebar.d.ts.map +1 -1
  36. package/dist/components/panel/app-sidebar.js +1 -24
  37. package/dist/components/panel/app-sidebar.js.map +1 -1
  38. package/dist/components/panel/brand.d.ts +10 -0
  39. package/dist/components/panel/brand.d.ts.map +1 -0
  40. package/dist/components/panel/brand.js +37 -0
  41. package/dist/components/panel/brand.js.map +1 -0
  42. package/dist/components/panel/index.d.ts +2 -1
  43. package/dist/components/panel/topbar.js +1 -1
  44. package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
  45. package/dist/components/realtime/live-records-pill.js +6 -3
  46. package/dist/components/realtime/live-records-pill.js.map +1 -1
  47. package/dist/components/table/cells.d.ts +6 -0
  48. package/dist/components/table/cells.d.ts.map +1 -1
  49. package/dist/components/table/cells.js +6 -2
  50. package/dist/components/table/cells.js.map +1 -1
  51. package/dist/components/table/column-manager.js +8 -7
  52. package/dist/components/table/column-manager.js.map +1 -1
  53. package/dist/components/table/content-layout.js +2 -1
  54. package/dist/components/table/content-layout.js.map +1 -1
  55. package/dist/components/table/pagination.js +16 -13
  56. package/dist/components/table/pagination.js.map +1 -1
  57. package/dist/components/table/table-block.js +14 -4
  58. package/dist/components/table/table-block.js.map +1 -1
  59. package/dist/components/ui/dialog.d.ts.map +1 -1
  60. package/dist/components/ui/dialog.js +3 -1
  61. package/dist/components/ui/dialog.js.map +1 -1
  62. package/dist/components/ui/popover.d.ts.map +1 -1
  63. package/dist/components/ui/popover.js +3 -1
  64. package/dist/components/ui/popover.js.map +1 -1
  65. package/dist/components/ui/sheet.d.ts.map +1 -1
  66. package/dist/components/ui/sheet.js +3 -1
  67. package/dist/components/ui/sheet.js.map +1 -1
  68. package/dist/components/ui/tooltip.js +3 -1
  69. package/dist/components/ui/tooltip.js.map +1 -1
  70. package/dist/context.d.ts +5 -0
  71. package/dist/context.d.ts.map +1 -1
  72. package/dist/context.js +5 -1
  73. package/dist/context.js.map +1 -1
  74. package/dist/create-alfakit.d.ts.map +1 -1
  75. package/dist/create-alfakit.js +8 -5
  76. package/dist/create-alfakit.js.map +1 -1
  77. package/dist/eject-manifest.json +16 -3
  78. package/dist/index.d.ts +6 -3
  79. package/dist/index.js +24 -21
  80. package/dist/layouts/app-layout.d.ts.map +1 -1
  81. package/dist/layouts/app-layout.js +2 -29
  82. package/dist/layouts/app-layout.js.map +1 -1
  83. package/dist/layouts/auth-layout.js +2 -2
  84. package/dist/layouts/auth-layout.js.map +1 -1
  85. package/dist/layouts/fullscreen-layout.js +1 -1
  86. package/dist/layouts/fullscreen-layout.js.map +1 -1
  87. package/dist/layouts/lazy-layouts.js +15 -0
  88. package/dist/layouts/lazy-layouts.js.map +1 -0
  89. package/dist/layouts/max-widths.js +34 -0
  90. package/dist/layouts/max-widths.js.map +1 -0
  91. package/dist/layouts/panel-layouts.d.ts +10 -0
  92. package/dist/layouts/panel-layouts.d.ts.map +1 -0
  93. package/dist/layouts/panel-layouts.js +20 -0
  94. package/dist/layouts/panel-layouts.js.map +1 -0
  95. package/dist/layouts/top-nav-layout.js +2 -2
  96. package/dist/layouts/top-nav-layout.js.map +1 -1
  97. package/dist/pages/page.d.ts.map +1 -1
  98. package/dist/pages/page.js +12 -7
  99. package/dist/pages/page.js.map +1 -1
  100. package/dist/standalone/alfakit-component.js +11 -6
  101. package/dist/standalone/alfakit-component.js.map +1 -1
  102. package/dist/standalone.css +7 -0
  103. package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
  104. package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
  105. package/dist/starter-kit/dashboard-widgets.js +53 -0
  106. package/dist/starter-kit/dashboard-widgets.js.map +1 -0
  107. package/dist/starter-kit/impersonation-banner.js +34 -0
  108. package/dist/starter-kit/impersonation-banner.js.map +1 -0
  109. package/dist/starter-kit/index.d.ts +3 -2
  110. package/dist/starter-kit/index.js +3 -2
  111. package/dist/starter-kit/starter-kit.d.ts +6 -0
  112. package/dist/starter-kit/starter-kit.d.ts.map +1 -1
  113. package/dist/starter-kit/starter-kit.js +18 -7
  114. package/dist/starter-kit/starter-kit.js.map +1 -1
  115. package/dist/starter-kit/widgets-loaded.js +20 -4
  116. package/dist/starter-kit/widgets-loaded.js.map +1 -1
  117. package/dist/starter-kit/widgets-triggers.js +34 -3
  118. package/dist/starter-kit/widgets-triggers.js.map +1 -1
  119. package/dist/starter-kit/widgets.d.ts +13 -5
  120. package/dist/starter-kit/widgets.d.ts.map +1 -1
  121. package/dist/starter-kit/widgets.js +56 -18
  122. package/dist/starter-kit/widgets.js.map +1 -1
  123. package/dist/styles.css +15 -2
  124. package/dist/themes.css +6 -1
  125. package/package.json +2 -2
  126. package/src/components/actions/action-modals.tsx +10 -0
  127. package/src/components/ai/copilot.tsx +14 -1
  128. package/src/components/cms/live-preview.tsx +5 -2
  129. package/src/components/fields/builder.tsx +14 -5
  130. package/src/components/fields/image-editor-dialog.tsx +7 -2
  131. package/src/components/fields/item-actions.tsx +32 -9
  132. package/src/components/fields/key-value.tsx +3 -2
  133. package/src/components/fields/modal-table-select.tsx +8 -3
  134. package/src/components/fields/repeater.tsx +2 -3
  135. package/src/components/fields/rich-editor.tsx +3 -0
  136. package/src/components/form/form-block.tsx +2 -1
  137. package/src/components/form/form-context.tsx +2 -2
  138. package/src/components/kanban/kanban.tsx +16 -3
  139. package/src/components/layout/section.tsx +2 -2
  140. package/src/components/panel/app-sidebar.tsx +2 -38
  141. package/src/components/panel/brand.tsx +46 -0
  142. package/src/components/realtime/live-records-pill.tsx +4 -3
  143. package/src/components/table/cells.tsx +9 -2
  144. package/src/components/table/column-manager.tsx +12 -10
  145. package/src/components/table/content-layout.tsx +5 -1
  146. package/src/components/table/pagination.tsx +16 -9
  147. package/src/components/table/table-block.tsx +25 -12
  148. package/src/components/ui/dialog.tsx +5 -1
  149. package/src/components/ui/popover.tsx +5 -1
  150. package/src/components/ui/sheet.tsx +5 -1
  151. package/src/components/ui/tooltip.tsx +5 -1
  152. package/src/context.tsx +10 -0
  153. package/src/create-alfakit.tsx +11 -2
  154. package/src/index.ts +2 -0
  155. package/src/layouts/app-layout.tsx +4 -28
  156. package/src/layouts/auth-layout.tsx +2 -2
  157. package/src/layouts/fullscreen-layout.tsx +1 -1
  158. package/src/layouts/lazy-layouts.ts +14 -0
  159. package/src/layouts/max-widths.ts +29 -0
  160. package/src/layouts/panel-layouts.ts +17 -0
  161. package/src/layouts/top-nav-layout.tsx +2 -2
  162. package/src/pages/page.tsx +21 -12
  163. package/src/standalone/alfakit-component.tsx +18 -7
  164. package/src/starter-kit/dashboard-widgets.tsx +78 -0
  165. package/src/starter-kit/impersonation-banner.tsx +48 -0
  166. package/src/starter-kit/index.ts +2 -0
  167. package/src/starter-kit/starter-kit.tsx +31 -4
  168. package/src/starter-kit/widgets-loaded.tsx +22 -0
  169. package/src/starter-kit/widgets-triggers.tsx +36 -1
  170. package/src/starter-kit/widgets.tsx +95 -20
  171. package/src/styles.css +15 -2
@@ -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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"}
@@ -6,7 +6,7 @@ import { Button } from "../ui/button.js";
6
6
  import { extraProps } from "../../lib/attributes.js";
7
7
  import { ActionButton } from "../actions/action-button.js";
8
8
  import { ActionList } from "../actions/action-list.js";
9
- import { ITEM_MOVE_ATTRIBUTE, ItemMoveTracker, addActionJustify, itemActionContext, resolveItemAction, withItemActionState } from "@alfakit/core";
9
+ import { ITEM_MOVE_ATTRIBUTE, ItemMoveTracker, addActionJustify, itemActionContext, itemActionIcon, resolveItemAction, withItemActionState } from "@alfakit/core";
10
10
  import { useEffect, useMemo, useRef, useState } from "react";
11
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
12
12
  import { ChevronDownIcon, ChevronUpIcon, CopyIcon, GripVerticalIcon, PlusIcon, Trash2Icon } from "lucide-react";
@@ -80,8 +80,8 @@ function useRunItemAction(context) {
80
80
  else onClient();
81
81
  };
82
82
  }
83
- /** Default looks of the built-in actions, for payloads without `itemActions`. */
84
- function useItemActionFallbacks(addLabel = "") {
83
+ /** Default looks of the built-in actions, for payloads without `itemActions` (icons follow their `alfakit::` aliases). */
84
+ function useItemActionFallbacks(addLabel = "", kind = "repeater") {
85
85
  const t = useT();
86
86
  return {
87
87
  add: {
@@ -91,32 +91,32 @@ function useItemActionFallbacks(addLabel = "") {
91
91
  },
92
92
  delete: {
93
93
  label: t("form.remove_item", {}, "Remove"),
94
- icon: "trash-2",
94
+ icon: itemActionIcon(kind, "delete", "trash-2"),
95
95
  color: "danger"
96
96
  },
97
97
  clone: {
98
98
  label: t("form.clone", {}, "Clone"),
99
- icon: "copy"
99
+ icon: itemActionIcon(kind, "clone", "copy")
100
100
  },
101
101
  reorder: {
102
102
  label: t("form.reorder_item", {}, "Reorder"),
103
- icon: "grip-vertical"
103
+ icon: itemActionIcon(kind, "reorder", "grip-vertical")
104
104
  },
105
105
  moveUp: {
106
106
  label: t("form.move_up", {}, "Move up"),
107
- icon: "chevron-up"
107
+ icon: itemActionIcon(kind, "move-up", "chevron-up")
108
108
  },
109
109
  moveDown: {
110
110
  label: t("form.move_down", {}, "Move down"),
111
- icon: "chevron-down"
111
+ icon: itemActionIcon(kind, "move-down", "chevron-down")
112
112
  },
113
113
  collapse: {
114
114
  label: t("form.collapse_item", {}, "Collapse"),
115
- icon: "chevron-up"
115
+ icon: itemActionIcon(kind, "collapse", "chevron-up")
116
116
  },
117
117
  expand: {
118
118
  label: t("form.expand_item", {}, "Expand"),
119
- icon: "chevron-down"
119
+ icon: itemActionIcon(kind, "expand", "chevron-down")
120
120
  },
121
121
  collapseAll: {
122
122
  label: t("form.collapse_all", {}, "Collapse all"),
@@ -1 +1 @@
1
- {"version":3,"file":"item-actions.js","names":[],"sources":["../../../src/components/fields/item-actions.tsx"],"sourcesContent":["import type {\n ActionContext,\n ActionSchema,\n ItemActionFallback,\n ItemActionName,\n ItemActionSchema,\n ItemActionsItemProps,\n ItemActionsProps,\n} from '@alfakit/core'\nimport {\n ITEM_MOVE_ATTRIBUTE,\n ItemMoveTracker,\n addActionJustify,\n itemActionContext,\n resolveItemAction,\n withItemActionState,\n} from '@alfakit/core'\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n CopyIcon,\n GripVerticalIcon,\n PlusIcon,\n Trash2Icon,\n type LucideIcon,\n} from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { useActionRunner, useT } from '../../context'\nimport { extraProps } from '../../lib/attributes'\nimport { colorClass, ghostButtonColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ActionButton } from '../actions/action-button'\nimport { ActionList } from '../actions/action-list'\nimport { Button } from '../ui/button'\n\n/**\n * A built-in item action (Repeater, Builder, KeyValue). Uncustomised actions apply their\n * change in the browser (`onClient`); customised ones (`server`) run through the field\n * action endpoint with `args` (`item`, `items`, `block`).\n */\nexport function ItemActionButton({\n action,\n context,\n args,\n onClient,\n disabled,\n attributes,\n size = 'xs',\n className,\n}: {\n action: ItemActionSchema | null\n context: ActionContext\n args?: Record<string, unknown>\n onClient?: () => void\n disabled?: boolean\n attributes?: Record<string, string>\n size?: 'xs' | 'sm' | 'default'\n className?: string\n}) {\n if (!action) return null\n\n // Plain icon buttons that change the state in the browser repeat on every item: render\n // them light (no runner subscription, no tooltip portal, static icons).\n if (!action.server && action.variant === 'icon-button' && !action.badge) {\n const StaticIcon = action.icon ? STATIC_ICONS[action.icon] : undefined\n const label = action.label ?? action.tooltip ?? action.name\n\n return (\n <Button\n type=\"button\"\n variant=\"plain\"\n size={size === 'xs' ? 'icon-xs' : size === 'sm' ? 'icon-sm' : 'icon'}\n disabled={disabled || action.disabled}\n aria-label={label}\n title={action.tooltip ?? label}\n onClick={() => onClient?.()}\n {...extraProps(\n { ...action.extraAttributes, ...attributes },\n cn(colorClass(ghostButtonColorClasses, action.color, 'gray'), className),\n )}\n >\n {StaticIcon ? <StaticIcon /> : <Icon name={action.icon} />}\n </Button>\n )\n }\n\n return (\n <ServerItemActionButton\n action={action}\n context={context}\n args={args}\n onClient={onClient}\n disabled={disabled}\n attributes={attributes}\n size={size}\n className={className}\n />\n )\n}\n\n/** The common item action icons, bundled so headers don't wait for dynamic icons. */\nconst STATIC_ICONS: Record<string, LucideIcon> = {\n 'chevron-up': ChevronUpIcon,\n 'chevron-down': ChevronDownIcon,\n copy: CopyIcon,\n 'trash-2': Trash2Icon,\n 'grip-vertical': GripVerticalIcon,\n plus: PlusIcon,\n}\n\nfunction ServerItemActionButton({\n action,\n context,\n args,\n onClient,\n disabled,\n attributes,\n size = 'xs',\n className,\n}: {\n action: ItemActionSchema\n context: ActionContext\n args?: Record<string, unknown>\n onClient?: () => void\n disabled?: boolean\n attributes?: Record<string, string>\n size?: 'xs' | 'sm' | 'default'\n className?: string\n}) {\n const runner = useActionRunner()\n const scoped = useMemo(() => itemActionContext(context, args ?? {}), [context, args])\n\n return (\n <ActionButton\n action={withItemActionState(action, { disabled, attributes })}\n context={scoped}\n size={size}\n className={className}\n withKeyBindings={false}\n onTrigger={(schema, ctx) => (action.server ? runner.trigger(schema, ctx) : onClient?.())}\n />\n )\n}\n\n/** Run a built-in action: on the server when customised, else `onClient`. */\nexport function useRunItemAction(context: ActionContext) {\n const runner = useActionRunner()\n\n return (action: ItemActionSchema | null, args: Record<string, unknown>, onClient: () => void) => {\n if (!action) return\n if (action.server) void runner.trigger(action, itemActionContext(context, args))\n else onClient()\n }\n}\n\n/** Default looks of the built-in actions, for payloads without `itemActions`. */\nexport function useItemActionFallbacks(addLabel = ''): Record<ItemActionName, ItemActionFallback> {\n const t = useT()\n\n return {\n add: { label: addLabel, icon: 'plus', variant: 'outline' },\n delete: { label: t('form.remove_item', {}, 'Remove'), icon: 'trash-2', color: 'danger' },\n clone: { label: t('form.clone', {}, 'Clone'), icon: 'copy' },\n reorder: { label: t('form.reorder_item', {}, 'Reorder'), icon: 'grip-vertical' },\n moveUp: { label: t('form.move_up', {}, 'Move up'), icon: 'chevron-up' },\n moveDown: { label: t('form.move_down', {}, 'Move down'), icon: 'chevron-down' },\n collapse: { label: t('form.collapse_item', {}, 'Collapse'), icon: 'chevron-up' },\n expand: { label: t('form.expand_item', {}, 'Expand'), icon: 'chevron-down' },\n collapseAll: {\n label: t('form.collapse_all', {}, 'Collapse all'),\n icon: 'chevrons-down-up',\n variant: 'link',\n },\n expandAll: {\n label: t('form.expand_all', {}, 'Expand all'),\n icon: 'chevrons-up-down',\n variant: 'link',\n },\n }\n}\n\n/** Resolve every built-in action of a field (and optionally one of its items). */\nexport function useItemActions(\n field: ItemActionsProps,\n fallbacks: Record<ItemActionName, ItemActionFallback>,\n) {\n return (name: ItemActionName, item?: ItemActionsItemProps): ItemActionSchema | null =>\n resolveItemAction(name, field, item, fallbacks[name])\n}\n\nexport interface ItemHeaderActionsProps {\n context: ActionContext\n itemKey: string\n index: number\n count: number\n resolve: (name: ItemActionName) => ItemActionSchema | null\n extraActions?: ActionSchema[]\n buttons: boolean\n canClone: boolean\n canDelete: boolean\n collapsible: boolean\n collapsed: boolean\n onMove: (from: number, to: number) => void\n onClone: () => void\n onDelete: () => void\n onToggle: () => void\n children?: ReactNode\n}\n\n/** Marks the move buttons so focus can return to them after a move (`ItemMoveTracker`). */\nconst MOVE_UP_ATTRIBUTES = { [ITEM_MOVE_ATTRIBUTE]: 'up' }\nconst MOVE_DOWN_ATTRIBUTES = { [ITEM_MOVE_ATTRIBUTE]: 'down' }\n\n/**\n * The end of an item header: move up / down, extra item actions, clone, delete and the\n * collapse toggle, in Filament's order.\n */\nexport function ItemHeaderActions({\n context,\n itemKey,\n index,\n count,\n resolve,\n extraActions,\n buttons,\n canClone,\n canDelete,\n collapsible,\n collapsed,\n onMove,\n onClone,\n onDelete,\n onToggle,\n children,\n}: ItemHeaderActionsProps) {\n const args = useMemo(() => ({ item: itemKey }), [itemKey])\n const extraContext = useMemo(() => itemActionContext(context, args), [context, args])\n\n return (\n <div className=\"ms-auto flex items-center gap-0.5\" data-slot=\"item-actions\">\n {children}\n {buttons && (\n <>\n <ItemActionButton\n action={resolve('moveUp')}\n context={context}\n args={args}\n disabled={index === 0}\n attributes={MOVE_UP_ATTRIBUTES}\n onClient={() => onMove(index, index - 1)}\n />\n <ItemActionButton\n action={resolve('moveDown')}\n context={context}\n args={args}\n disabled={index === count - 1}\n attributes={MOVE_DOWN_ATTRIBUTES}\n onClient={() => onMove(index, index + 1)}\n />\n </>\n )}\n {extraActions && extraActions.length > 0 && (\n <ActionList actions={extraActions} context={extraContext} size=\"xs\" className=\"gap-0.5\" />\n )}\n {canClone && (\n <ItemActionButton\n action={resolve('clone')}\n context={context}\n args={args}\n onClient={onClone}\n />\n )}\n {canDelete && (\n <ItemActionButton\n action={resolve('delete')}\n context={context}\n args={args}\n className=\"text-destructive hover:text-destructive\"\n onClient={onDelete}\n />\n )}\n {collapsible && (\n <ItemActionButton\n action={resolve(collapsed ? 'expand' : 'collapse')}\n context={context}\n args={args}\n attributes={{ 'aria-expanded': collapsed ? 'false' : 'true' }}\n onClient={onToggle}\n />\n )}\n </div>\n )\n}\n\n/** \"Collapse all\" / \"Expand all\" above collapsible items. */\nexport function CollapseAllActions({\n context,\n resolve,\n onCollapseAll,\n onExpandAll,\n}: {\n context: ActionContext\n resolve: (name: ItemActionName) => ItemActionSchema | null\n onCollapseAll: () => void\n onExpandAll: () => void\n}) {\n const collapseAll = resolve('collapseAll')\n const expandAll = resolve('expandAll')\n\n if (!collapseAll && !expandAll) return null\n\n return (\n <div className=\"flex justify-end gap-3\" data-slot=\"item-collapse-actions\">\n <ItemActionButton action={collapseAll} context={context} onClient={onCollapseAll} />\n <ItemActionButton action={expandAll} context={context} onClient={onExpandAll} />\n </div>\n )\n}\n\nconst JUSTIFY = { start: 'justify-start', center: 'justify-center', end: 'justify-end' } as const\n\n/** The row holding the add button, aligned with `addActionAlignment()`. */\nexport function AddActionRow({ alignment, children }: { alignment?: string; children: ReactNode }) {\n return (\n <div className={cn('flex', JUSTIFY[addActionJustify(alignment)])} data-slot=\"item-add-action\">\n {children}\n </div>\n )\n}\n\n/**\n * Moves made with the item move buttons: one at a time, announced in a live region\n * (\"Moved to position 2 of 5\") and with focus kept on the moved item's move button.\n * Render `region` once in the field and put `containerRef` on an element holding the\n * items (they carry `data-index`).\n */\nexport function useItemMoves<T>(\n value: T,\n move: (from: number, to: number) => void,\n labelFor: (index: number) => string | null | undefined,\n count: number,\n) {\n const t = useT()\n const [tracker] = useState(() => new ItemMoveTracker())\n const [announcement, setAnnouncement] = useState('')\n const containerRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n if (tracker.isMoving) tracker.finish(containerRef.current)\n }, [value, tracker])\n\n const moveWithButton = (from: number, to: number) => {\n if (to < 0 || to >= count || !tracker.begin(from, to)) return\n\n const label = labelFor(from)\n\n move(from, to)\n setAnnouncement(\n label\n ? t(\n 'form.labelled_item_moved',\n { item: label, position: to + 1, total: count },\n ':item moved to position :position of :total',\n )\n : t(\n 'form.item_moved',\n { position: to + 1, total: count },\n 'Item moved to position :position of :total',\n ),\n )\n }\n\n const region = (\n <span aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\" data-slot=\"item-move-status\">\n {announcement}\n </span>\n )\n\n return { moveWithButton, region, containerRef }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"item-actions.js","names":[],"sources":["../../../src/components/fields/item-actions.tsx"],"sourcesContent":["import type {\n ActionContext,\n ActionSchema,\n ItemActionFallback,\n ItemActionName,\n ItemActionSchema,\n ItemActionsItemProps,\n ItemActionsProps,\n} from '@alfakit/core'\nimport {\n ITEM_MOVE_ATTRIBUTE,\n ItemMoveTracker,\n addActionJustify,\n itemActionContext,\n itemActionIcon,\n resolveItemAction,\n withItemActionState,\n} from '@alfakit/core'\nimport {\n ChevronDownIcon,\n ChevronUpIcon,\n CopyIcon,\n GripVerticalIcon,\n PlusIcon,\n Trash2Icon,\n type LucideIcon,\n} from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState, type ReactNode } from 'react'\nimport { useActionRunner, useT } from '../../context'\nimport { extraProps } from '../../lib/attributes'\nimport { colorClass, ghostButtonColorClasses } from '../../lib/colors'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { ActionButton } from '../actions/action-button'\nimport { ActionList } from '../actions/action-list'\nimport { Button } from '../ui/button'\n\n/**\n * A built-in item action (Repeater, Builder, KeyValue). Uncustomised actions apply their\n * change in the browser (`onClient`); customised ones (`server`) run through the field\n * action endpoint with `args` (`item`, `items`, `block`).\n */\nexport function ItemActionButton({\n action,\n context,\n args,\n onClient,\n disabled,\n attributes,\n size = 'xs',\n className,\n}: {\n action: ItemActionSchema | null\n context: ActionContext\n args?: Record<string, unknown>\n onClient?: () => void\n disabled?: boolean\n attributes?: Record<string, string>\n size?: 'xs' | 'sm' | 'default'\n className?: string\n}) {\n if (!action) return null\n\n // Plain icon buttons that change the state in the browser repeat on every item: render\n // them light (no runner subscription, no tooltip portal, static icons).\n if (!action.server && action.variant === 'icon-button' && !action.badge) {\n const StaticIcon = action.icon ? STATIC_ICONS[action.icon] : undefined\n const label = action.label ?? action.tooltip ?? action.name\n\n return (\n <Button\n type=\"button\"\n variant=\"plain\"\n size={size === 'xs' ? 'icon-xs' : size === 'sm' ? 'icon-sm' : 'icon'}\n disabled={disabled || action.disabled}\n aria-label={label}\n title={action.tooltip ?? label}\n onClick={() => onClient?.()}\n {...extraProps(\n { ...action.extraAttributes, ...attributes },\n cn(colorClass(ghostButtonColorClasses, action.color, 'gray'), className),\n )}\n >\n {StaticIcon ? <StaticIcon /> : <Icon name={action.icon} />}\n </Button>\n )\n }\n\n return (\n <ServerItemActionButton\n action={action}\n context={context}\n args={args}\n onClient={onClient}\n disabled={disabled}\n attributes={attributes}\n size={size}\n className={className}\n />\n )\n}\n\n/** The common item action icons, bundled so headers don't wait for dynamic icons. */\nconst STATIC_ICONS: Record<string, LucideIcon> = {\n 'chevron-up': ChevronUpIcon,\n 'chevron-down': ChevronDownIcon,\n copy: CopyIcon,\n 'trash-2': Trash2Icon,\n 'grip-vertical': GripVerticalIcon,\n plus: PlusIcon,\n}\n\nfunction ServerItemActionButton({\n action,\n context,\n args,\n onClient,\n disabled,\n attributes,\n size = 'xs',\n className,\n}: {\n action: ItemActionSchema\n context: ActionContext\n args?: Record<string, unknown>\n onClient?: () => void\n disabled?: boolean\n attributes?: Record<string, string>\n size?: 'xs' | 'sm' | 'default'\n className?: string\n}) {\n const runner = useActionRunner()\n const scoped = useMemo(() => itemActionContext(context, args ?? {}), [context, args])\n\n return (\n <ActionButton\n action={withItemActionState(action, { disabled, attributes })}\n context={scoped}\n size={size}\n className={className}\n withKeyBindings={false}\n onTrigger={(schema, ctx) => (action.server ? runner.trigger(schema, ctx) : onClient?.())}\n />\n )\n}\n\n/** Run a built-in action: on the server when customised, else `onClient`. */\nexport function useRunItemAction(context: ActionContext) {\n const runner = useActionRunner()\n\n return (action: ItemActionSchema | null, args: Record<string, unknown>, onClient: () => void) => {\n if (!action) return\n if (action.server) void runner.trigger(action, itemActionContext(context, args))\n else onClient()\n }\n}\n\n/** Default looks of the built-in actions, for payloads without `itemActions` (icons follow their `alfakit::` aliases). */\nexport function useItemActionFallbacks(\n addLabel = '',\n kind: 'repeater' | 'builder' = 'repeater',\n): Record<ItemActionName, ItemActionFallback> {\n const t = useT()\n\n return {\n add: { label: addLabel, icon: 'plus', variant: 'outline' },\n delete: {\n label: t('form.remove_item', {}, 'Remove'),\n icon: itemActionIcon(kind, 'delete', 'trash-2'),\n color: 'danger',\n },\n clone: { label: t('form.clone', {}, 'Clone'), icon: itemActionIcon(kind, 'clone', 'copy') },\n reorder: {\n label: t('form.reorder_item', {}, 'Reorder'),\n icon: itemActionIcon(kind, 'reorder', 'grip-vertical'),\n },\n moveUp: {\n label: t('form.move_up', {}, 'Move up'),\n icon: itemActionIcon(kind, 'move-up', 'chevron-up'),\n },\n moveDown: {\n label: t('form.move_down', {}, 'Move down'),\n icon: itemActionIcon(kind, 'move-down', 'chevron-down'),\n },\n collapse: {\n label: t('form.collapse_item', {}, 'Collapse'),\n icon: itemActionIcon(kind, 'collapse', 'chevron-up'),\n },\n expand: {\n label: t('form.expand_item', {}, 'Expand'),\n icon: itemActionIcon(kind, 'expand', 'chevron-down'),\n },\n collapseAll: {\n label: t('form.collapse_all', {}, 'Collapse all'),\n icon: 'chevrons-down-up',\n variant: 'link',\n },\n expandAll: {\n label: t('form.expand_all', {}, 'Expand all'),\n icon: 'chevrons-up-down',\n variant: 'link',\n },\n }\n}\n\n/** Resolve every built-in action of a field (and optionally one of its items). */\nexport function useItemActions(\n field: ItemActionsProps,\n fallbacks: Record<ItemActionName, ItemActionFallback>,\n) {\n return (name: ItemActionName, item?: ItemActionsItemProps): ItemActionSchema | null =>\n resolveItemAction(name, field, item, fallbacks[name])\n}\n\nexport interface ItemHeaderActionsProps {\n context: ActionContext\n itemKey: string\n index: number\n count: number\n resolve: (name: ItemActionName) => ItemActionSchema | null\n extraActions?: ActionSchema[]\n buttons: boolean\n canClone: boolean\n canDelete: boolean\n collapsible: boolean\n collapsed: boolean\n onMove: (from: number, to: number) => void\n onClone: () => void\n onDelete: () => void\n onToggle: () => void\n children?: ReactNode\n}\n\n/** Marks the move buttons so focus can return to them after a move (`ItemMoveTracker`). */\nconst MOVE_UP_ATTRIBUTES = { [ITEM_MOVE_ATTRIBUTE]: 'up' }\nconst MOVE_DOWN_ATTRIBUTES = { [ITEM_MOVE_ATTRIBUTE]: 'down' }\n\n/**\n * The end of an item header: move up / down, extra item actions, clone, delete and the\n * collapse toggle, in Filament's order.\n */\nexport function ItemHeaderActions({\n context,\n itemKey,\n index,\n count,\n resolve,\n extraActions,\n buttons,\n canClone,\n canDelete,\n collapsible,\n collapsed,\n onMove,\n onClone,\n onDelete,\n onToggle,\n children,\n}: ItemHeaderActionsProps) {\n const args = useMemo(() => ({ item: itemKey }), [itemKey])\n const extraContext = useMemo(() => itemActionContext(context, args), [context, args])\n\n return (\n <div className=\"ms-auto flex items-center gap-0.5\" data-slot=\"item-actions\">\n {children}\n {buttons && (\n <>\n <ItemActionButton\n action={resolve('moveUp')}\n context={context}\n args={args}\n disabled={index === 0}\n attributes={MOVE_UP_ATTRIBUTES}\n onClient={() => onMove(index, index - 1)}\n />\n <ItemActionButton\n action={resolve('moveDown')}\n context={context}\n args={args}\n disabled={index === count - 1}\n attributes={MOVE_DOWN_ATTRIBUTES}\n onClient={() => onMove(index, index + 1)}\n />\n </>\n )}\n {extraActions && extraActions.length > 0 && (\n <ActionList actions={extraActions} context={extraContext} size=\"xs\" className=\"gap-0.5\" />\n )}\n {canClone && (\n <ItemActionButton\n action={resolve('clone')}\n context={context}\n args={args}\n onClient={onClone}\n />\n )}\n {canDelete && (\n <ItemActionButton\n action={resolve('delete')}\n context={context}\n args={args}\n className=\"text-destructive hover:text-destructive\"\n onClient={onDelete}\n />\n )}\n {collapsible && (\n <ItemActionButton\n action={resolve(collapsed ? 'expand' : 'collapse')}\n context={context}\n args={args}\n attributes={{ 'aria-expanded': collapsed ? 'false' : 'true' }}\n onClient={onToggle}\n />\n )}\n </div>\n )\n}\n\n/** \"Collapse all\" / \"Expand all\" above collapsible items. */\nexport function CollapseAllActions({\n context,\n resolve,\n onCollapseAll,\n onExpandAll,\n}: {\n context: ActionContext\n resolve: (name: ItemActionName) => ItemActionSchema | null\n onCollapseAll: () => void\n onExpandAll: () => void\n}) {\n const collapseAll = resolve('collapseAll')\n const expandAll = resolve('expandAll')\n\n if (!collapseAll && !expandAll) return null\n\n return (\n <div className=\"flex justify-end gap-3\" data-slot=\"item-collapse-actions\">\n <ItemActionButton action={collapseAll} context={context} onClient={onCollapseAll} />\n <ItemActionButton action={expandAll} context={context} onClient={onExpandAll} />\n </div>\n )\n}\n\nconst JUSTIFY = { start: 'justify-start', center: 'justify-center', end: 'justify-end' } as const\n\n/** The row holding the add button, aligned with `addActionAlignment()`. */\nexport function AddActionRow({ alignment, children }: { alignment?: string; children: ReactNode }) {\n return (\n <div className={cn('flex', JUSTIFY[addActionJustify(alignment)])} data-slot=\"item-add-action\">\n {children}\n </div>\n )\n}\n\n/**\n * Moves made with the item move buttons: one at a time, announced in a live region\n * (\"Moved to position 2 of 5\") and with focus kept on the moved item's move button.\n * Render `region` once in the field and put `containerRef` on an element holding the\n * items (they carry `data-index`).\n */\nexport function useItemMoves<T>(\n value: T,\n move: (from: number, to: number) => void,\n labelFor: (index: number) => string | null | undefined,\n count: number,\n) {\n const t = useT()\n const [tracker] = useState(() => new ItemMoveTracker())\n const [announcement, setAnnouncement] = useState('')\n const containerRef = useRef<HTMLDivElement>(null)\n\n useEffect(() => {\n if (tracker.isMoving) tracker.finish(containerRef.current)\n }, [value, tracker])\n\n const moveWithButton = (from: number, to: number) => {\n if (to < 0 || to >= count || !tracker.begin(from, to)) return\n\n const label = labelFor(from)\n\n move(from, to)\n setAnnouncement(\n label\n ? t(\n 'form.labelled_item_moved',\n { item: label, position: to + 1, total: count },\n ':item moved to position :position of :total',\n )\n : t(\n 'form.item_moved',\n { position: to + 1, total: count },\n 'Item moved to position :position of :total',\n ),\n )\n }\n\n const region = (\n <span aria-live=\"polite\" aria-atomic=\"true\" className=\"sr-only\" data-slot=\"item-move-status\">\n {announcement}\n </span>\n )\n\n return { moveWithButton, region, containerRef }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0CA,SAAgB,iBAAiB,EAC/B,QACA,SACA,MACA,UACA,UACA,YACA,OAAO,MACP,aAUC;CACD,IAAI,CAAC,QAAQ,OAAO;CAIpB,IAAI,CAAC,OAAO,UAAU,OAAO,YAAY,iBAAiB,CAAC,OAAO,OAAO;EACvE,MAAM,aAAa,OAAO,OAAO,aAAa,OAAO,QAAQ,KAAA;EAC7D,MAAM,QAAQ,OAAO,SAAS,OAAO,WAAW,OAAO;EAEvD,OACE,oBAAC,QAAD;GACE,MAAK;GACL,SAAQ;GACR,MAAM,SAAS,OAAO,YAAY,SAAS,OAAO,YAAY;GAC9D,UAAU,YAAY,OAAO;GAC7B,cAAY;GACZ,OAAO,OAAO,WAAW;GACzB,eAAe,WAAW;GAC1B,GAAI,WACF;IAAE,GAAG,OAAO;IAAiB,GAAG;GAAW,GAC3C,GAAG,WAAW,yBAAyB,OAAO,OAAO,MAAM,GAAG,SAAS,CACzE;GAEC,UAAA,aAAa,oBAAC,YAAD,CAAa,CAAA,IAAI,oBAAC,MAAD,EAAM,MAAM,OAAO,KAAO,CAAA;EACnD,CAAA;CAEZ;CAEA,OACE,oBAAC,wBAAD;EACU;EACC;EACH;EACI;EACA;EACE;EACN;EACK;CACZ,CAAA;AAEL;;AAGA,MAAM,eAA2C;CAC/C,cAAc;CACd,gBAAgB;CAChB,MAAM;CACN,WAAW;CACX,iBAAiB;CACjB,MAAM;AACR;AAEA,SAAS,uBAAuB,EAC9B,QACA,SACA,MACA,UACA,UACA,YACA,OAAO,MACP,aAUC;CACD,MAAM,SAAS,gBAAgB;CAC/B,MAAM,SAAS,cAAc,kBAAkB,SAAS,QAAQ,CAAC,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC;CAEpF,OACE,oBAAC,cAAD;EACE,QAAQ,oBAAoB,QAAQ;GAAE;GAAU;EAAW,CAAC;EAC5D,SAAS;EACH;EACK;EACX,iBAAiB;EACjB,YAAY,QAAQ,QAAS,OAAO,SAAS,OAAO,QAAQ,QAAQ,GAAG,IAAI,WAAW;CACvF,CAAA;AAEL;;AAGA,SAAgB,iBAAiB,SAAwB;CACvD,MAAM,SAAS,gBAAgB;CAE/B,QAAQ,QAAiC,MAA+B,aAAyB;EAC/F,IAAI,CAAC,QAAQ;EACb,IAAI,OAAO,QAAQ,OAAY,QAAQ,QAAQ,kBAAkB,SAAS,IAAI,CAAC;OAC1E,SAAS;CAChB;AACF;;AAGA,SAAgB,uBACd,WAAW,IACX,OAA+B,YACa;CAC5C,MAAM,IAAI,KAAK;CAEf,OAAO;EACL,KAAK;GAAE,OAAO;GAAU,MAAM;GAAQ,SAAS;EAAU;EACzD,QAAQ;GACN,OAAO,EAAE,oBAAoB,CAAC,GAAG,QAAQ;GACzC,MAAM,eAAe,MAAM,UAAU,SAAS;GAC9C,OAAO;EACT;EACA,OAAO;GAAE,OAAO,EAAE,cAAc,CAAC,GAAG,OAAO;GAAG,MAAM,eAAe,MAAM,SAAS,MAAM;EAAE;EAC1F,SAAS;GACP,OAAO,EAAE,qBAAqB,CAAC,GAAG,SAAS;GAC3C,MAAM,eAAe,MAAM,WAAW,eAAe;EACvD;EACA,QAAQ;GACN,OAAO,EAAE,gBAAgB,CAAC,GAAG,SAAS;GACtC,MAAM,eAAe,MAAM,WAAW,YAAY;EACpD;EACA,UAAU;GACR,OAAO,EAAE,kBAAkB,CAAC,GAAG,WAAW;GAC1C,MAAM,eAAe,MAAM,aAAa,cAAc;EACxD;EACA,UAAU;GACR,OAAO,EAAE,sBAAsB,CAAC,GAAG,UAAU;GAC7C,MAAM,eAAe,MAAM,YAAY,YAAY;EACrD;EACA,QAAQ;GACN,OAAO,EAAE,oBAAoB,CAAC,GAAG,QAAQ;GACzC,MAAM,eAAe,MAAM,UAAU,cAAc;EACrD;EACA,aAAa;GACX,OAAO,EAAE,qBAAqB,CAAC,GAAG,cAAc;GAChD,MAAM;GACN,SAAS;EACX;EACA,WAAW;GACT,OAAO,EAAE,mBAAmB,CAAC,GAAG,YAAY;GAC5C,MAAM;GACN,SAAS;EACX;CACF;AACF;;AAGA,SAAgB,eACd,OACA,WACA;CACA,QAAQ,MAAsB,SAC5B,kBAAkB,MAAM,OAAO,MAAM,UAAU,KAAK;AACxD;;AAsBA,MAAM,qBAAqB,GAAG,sBAAsB,KAAK;AACzD,MAAM,uBAAuB,GAAG,sBAAsB,OAAO;;;;;AAM7D,SAAgB,kBAAkB,EAChC,SACA,SACA,OACA,OACA,SACA,cACA,SACA,UACA,WACA,aACA,WACA,QACA,SACA,UACA,UACA,YACyB;CACzB,MAAM,OAAO,eAAe,EAAE,MAAM,QAAQ,IAAI,CAAC,OAAO,CAAC;CACzD,MAAM,eAAe,cAAc,kBAAkB,SAAS,IAAI,GAAG,CAAC,SAAS,IAAI,CAAC;CAEpF,OACE,qBAAC,OAAD;EAAK,WAAU;EAAoC,aAAU;EAA7D,UAAA;GACG;GACA,WACC,qBAAA,YAAA,EAAA,UAAA,CACE,oBAAC,kBAAD;IACE,QAAQ,QAAQ,QAAQ;IACf;IACH;IACN,UAAU,UAAU;IACpB,YAAY;IACZ,gBAAgB,OAAO,OAAO,QAAQ,CAAC;GACxC,CAAA,GACD,oBAAC,kBAAD;IACE,QAAQ,QAAQ,UAAU;IACjB;IACH;IACN,UAAU,UAAU,QAAQ;IAC5B,YAAY;IACZ,gBAAgB,OAAO,OAAO,QAAQ,CAAC;GACxC,CAAA,CACD,EAAA,CAAA;GAEH,gBAAgB,aAAa,SAAS,KACrC,oBAAC,YAAD;IAAY,SAAS;IAAc,SAAS;IAAc,MAAK;IAAK,WAAU;GAAW,CAAA;GAE1F,YACC,oBAAC,kBAAD;IACE,QAAQ,QAAQ,OAAO;IACd;IACH;IACN,UAAU;GACX,CAAA;GAEF,aACC,oBAAC,kBAAD;IACE,QAAQ,QAAQ,QAAQ;IACf;IACH;IACN,WAAU;IACV,UAAU;GACX,CAAA;GAEF,eACC,oBAAC,kBAAD;IACE,QAAQ,QAAQ,YAAY,WAAW,UAAU;IACxC;IACH;IACN,YAAY,EAAE,iBAAiB,YAAY,UAAU,OAAO;IAC5D,UAAU;GACX,CAAA;EAEA;;AAET;;AAGA,SAAgB,mBAAmB,EACjC,SACA,SACA,eACA,eAMC;CACD,MAAM,cAAc,QAAQ,aAAa;CACzC,MAAM,YAAY,QAAQ,WAAW;CAErC,IAAI,CAAC,eAAe,CAAC,WAAW,OAAO;CAEvC,OACE,qBAAC,OAAD;EAAK,WAAU;EAAyB,aAAU;EAAlD,UAAA,CACE,oBAAC,kBAAD;GAAkB,QAAQ;GAAsB;GAAS,UAAU;EAAgB,CAAA,GACnF,oBAAC,kBAAD;GAAkB,QAAQ;GAAoB;GAAS,UAAU;EAAc,CAAA,CAC5E;;AAET;AAEA,MAAM,UAAU;CAAE,OAAO;CAAiB,QAAQ;CAAkB,KAAK;AAAc;;AAGvF,SAAgB,aAAa,EAAE,WAAW,YAAyD;CACjG,OACE,oBAAC,OAAD;EAAK,WAAW,GAAG,QAAQ,QAAQ,iBAAiB,SAAS,EAAE;EAAG,aAAU;EACzE;CACE,CAAA;AAET;;;;;;;AAQA,SAAgB,aACd,OACA,MACA,UACA,OACA;CACA,MAAM,IAAI,KAAK;CACf,MAAM,CAAC,WAAW,eAAe,IAAI,gBAAgB,CAAC;CACtD,MAAM,CAAC,cAAc,mBAAmB,SAAS,EAAE;CACnD,MAAM,eAAe,OAAuB,IAAI;CAEhD,gBAAgB;EACd,IAAI,QAAQ,UAAU,QAAQ,OAAO,aAAa,OAAO;CAC3D,GAAG,CAAC,OAAO,OAAO,CAAC;CAEnB,MAAM,kBAAkB,MAAc,OAAe;EACnD,IAAI,KAAK,KAAK,MAAM,SAAS,CAAC,QAAQ,MAAM,MAAM,EAAE,GAAG;EAEvD,MAAM,QAAQ,SAAS,IAAI;EAE3B,KAAK,MAAM,EAAE;EACb,gBACE,QACI,EACE,4BACA;GAAE,MAAM;GAAO,UAAU,KAAK;GAAG,OAAO;EAAM,GAC9C,6CACF,IACA,EACE,mBACA;GAAE,UAAU,KAAK;GAAG,OAAO;EAAM,GACjC,4CACF,CACN;CACF;CAQA,OAAO;EAAE;EAAgB,QAAA,oBALtB,QAAD;GAAM,aAAU;GAAS,eAAY;GAAO,WAAU;GAAU,aAAU;GACvE,UAAA;EACG,CAGsB;EAAG;CAAa;AAChD"}
@@ -1 +1 @@
1
- {"version":3,"file":"key-value.d.ts","names":[],"sources":["../../../src/components/fields/key-value.tsx"],"mappings":";;;;;;wBAwBgB,gBAAgB;EAAe,WAAW;oBAAgB,IAAA"}
1
+ {"version":3,"file":"key-value.d.ts","names":[],"sources":["../../../src/components/fields/key-value.tsx"],"mappings":";;;;;;wBAyBgB,gBAAgB;EAAe,WAAW;oBAAgB,IAAA"}
@@ -6,7 +6,7 @@ import { useFieldActionContext } from "../schema/field-actions.js";
6
6
  import { FieldWrapper } from "../schema/field-wrapper.js";
7
7
  import { Input } from "../ui/input.js";
8
8
  import { ItemActionButton, useItemMoves, useRunItemAction } from "./item-actions.js";
9
- import { fieldIds, moveRepeaterItem, reorderedItemKeys, resolveItemAction } from "@alfakit/core";
9
+ import { fieldIds, itemActionIcon, moveRepeaterItem, reorderedItemKeys, resolveItemAction } from "@alfakit/core";
10
10
  import { useMemo, useState } from "react";
11
11
  import { jsx, jsxs } from "react/jsx-runtime";
12
12
  import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
@@ -37,11 +37,11 @@ function KeyValueField({ component }) {
37
37
  });
38
38
  const deleteAction = resolveItemAction("delete", component, void 0, {
39
39
  label: t("form.remove_row", {}, "Remove row"),
40
- icon: "trash-2"
40
+ icon: itemActionIcon("key-value", "delete", "trash-2")
41
41
  });
42
42
  const reorderAction = resolveItemAction("reorder", component, void 0, {
43
43
  label: t("form.reorder_item", {}, "Reorder"),
44
- icon: "grip-vertical"
44
+ icon: itemActionIcon("key-value", "reorder", "grip-vertical")
45
45
  });
46
46
  const update = (next) => setValue(next);
47
47
  const deletable = component.deletable && !readOnly && deleteAction !== null;
@@ -1 +1 @@
1
- {"version":3,"file":"key-value.js","names":[],"sources":["../../../src/components/fields/key-value.tsx"],"sourcesContent":["import {\n fieldIds,\n moveRepeaterItem,\n reorderedItemKeys,\n resolveItemAction,\n type KeyValueSchema,\n} from '@alfakit/core'\nimport { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { useT } from '../../context'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Input } from '../ui/input'\nimport { ItemActionButton, useItemMoves, useRunItemAction } from './item-actions'\n\ntype Row = { key: string; value: string }\n\n/**\n * The server stores an object; while editing we keep an ordered list of rows so keys\n * can be typed freely (including temporarily duplicate or empty keys).\n */\nexport function KeyValueField({ component }: { component: KeyValueSchema }) {\n const t = useT()\n const { value, setValue, blur, error, id, disabled, readOnly, aria } = useField<\n Record<string, unknown> | Row[] | null\n >(component)\n\n const rows = useMemo<Row[]>(() => {\n if (Array.isArray(value)) {\n return value.map((row) => ({\n key: String(row.key ?? ''),\n value: row.value === null || row.value === undefined ? '' : String(row.value),\n }))\n }\n\n return Object.entries(value ?? {}).map(([key, rowValue]) => ({\n key,\n value: rowValue === null || rowValue === undefined ? '' : String(rowValue),\n }))\n }, [value])\n\n const context = useFieldActionContext(component)\n const runItemAction = useRunItemAction(context)\n const addAction = resolveItemAction('add', component, undefined, {\n label: component.addActionLabel,\n icon: 'plus',\n variant: 'ghost',\n })\n const deleteAction = resolveItemAction('delete', component, undefined, {\n label: t('form.remove_row', {}, 'Remove row'),\n icon: 'trash-2',\n })\n const reorderAction = resolveItemAction('reorder', component, undefined, {\n label: t('form.reorder_item', {}, 'Reorder'),\n icon: 'grip-vertical',\n })\n\n const update = (next: Row[]) => setValue(next)\n const deletable = component.deletable && !readOnly && deleteAction !== null\n const reorderable = component.reorderable && !readOnly && !disabled && reorderAction !== null\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n // Moving runs the reorder action: in the browser, or on the server when customised.\n const move = (from: number, to: number) =>\n runItemAction(reorderAction, { items: reorderedItemKeys(rows.length, from, to) }, () =>\n update(moveRepeaterItem(rows, from, to) as Row[]),\n )\n // Move buttons: announced, one at a time, focus kept on the moved row.\n const {\n moveWithButton,\n region: moveRegion,\n containerRef: moveContainerRef,\n } = useItemMoves(value, move, (index) => rows[index]?.key || null, rows.length)\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div ref={moveContainerRef} className=\"overflow-hidden rounded-md border\">\n {moveRegion}\n <table\n className=\"w-full text-sm\"\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n >\n <thead className=\"bg-muted/50 text-muted-foreground text-xs\">\n <tr>\n {reorderable && <th className=\"w-16\" />}\n <th className=\"px-3 py-2 text-start font-medium\">{component.keyLabel}</th>\n <th className=\"px-3 py-2 text-start font-medium\">{component.valueLabel}</th>\n {deletable && <th className=\"w-10\" />}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr\n key={index}\n data-index={index}\n draggable={reorderable}\n onDragStart={() => setDragIndex(index)}\n onDragOver={(event) => reorderable && event.preventDefault()}\n onDrop={() => {\n if (dragIndex !== null && dragIndex !== index) move(dragIndex, index)\n setDragIndex(null)\n }}\n onDragEnd={() => setDragIndex(null)}\n className={cn('border-t', dragIndex === index && 'opacity-50')}\n >\n {reorderable && (\n <td className=\"p-1.5\">\n <div className=\"flex items-center\">\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 data-item-move=\"up\"\n onClick={() => moveWithButton(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 === rows.length - 1}\n data-item-move=\"down\"\n onClick={() => moveWithButton(index, index + 1)}\n >\n <ChevronDownIcon />\n </Button>\n </div>\n </td>\n )}\n <td className=\"p-1.5\">\n <Input\n dir=\"auto\"\n value={row.key}\n readOnly={readOnly || !component.editableKeys}\n disabled={disabled}\n placeholder={component.keyPlaceholder}\n aria-label={component.keyLabel}\n aria-invalid={aria['aria-invalid']}\n className=\"h-8\"\n onChange={(event) =>\n update(\n rows.map((item, i) =>\n i === index ? { ...item, key: event.target.value } : item,\n ),\n )\n }\n onBlur={blur}\n />\n </td>\n <td className=\"p-1.5\">\n <Input\n dir=\"auto\"\n value={row.value}\n readOnly={readOnly || !component.editableValues}\n disabled={disabled}\n placeholder={component.valuePlaceholder}\n aria-label={component.valueLabel}\n aria-invalid={aria['aria-invalid']}\n className=\"h-8\"\n onChange={(event) =>\n update(\n rows.map((item, i) =>\n i === index ? { ...item, value: event.target.value } : item,\n ),\n )\n }\n onBlur={blur}\n />\n </td>\n {deletable && (\n <td className=\"p-1.5\">\n <ItemActionButton\n action={deleteAction}\n context={context}\n args={{ item: String(index) }}\n size=\"sm\"\n disabled={disabled}\n className=\"text-muted-foreground\"\n onClient={() => update(rows.filter((_, i) => i !== index))}\n />\n </td>\n )}\n </tr>\n ))}\n {rows.length === 0 && (\n <tr className=\"border-t\">\n <td\n colSpan={reorderable ? 4 : 3}\n className=\"text-muted-foreground px-3 py-4 text-center text-xs\"\n >\n —\n </td>\n </tr>\n )}\n </tbody>\n </table>\n {component.addable && !readOnly && addAction && (\n <div className=\"border-t p-1.5\">\n <ItemActionButton\n action={addAction}\n context={context}\n size=\"sm\"\n disabled={disabled}\n onClient={() => update([...rows, { key: '', value: '' }])}\n />\n </div>\n )}\n </div>\n </FieldWrapper>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"key-value.js","names":[],"sources":["../../../src/components/fields/key-value.tsx"],"sourcesContent":["import {\n fieldIds,\n itemActionIcon,\n moveRepeaterItem,\n reorderedItemKeys,\n resolveItemAction,\n type KeyValueSchema,\n} from '@alfakit/core'\nimport { ChevronDownIcon, ChevronUpIcon } from 'lucide-react'\nimport { useMemo, useState } from 'react'\nimport { useT } from '../../context'\nimport { cn } from '../../lib/utils'\nimport { useField } from '../form/form-context'\nimport { useFieldActionContext } from '../schema/field-actions'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { Button } from '../ui/button'\nimport { Input } from '../ui/input'\nimport { ItemActionButton, useItemMoves, useRunItemAction } from './item-actions'\n\ntype Row = { key: string; value: string }\n\n/**\n * The server stores an object; while editing we keep an ordered list of rows so keys\n * can be typed freely (including temporarily duplicate or empty keys).\n */\nexport function KeyValueField({ component }: { component: KeyValueSchema }) {\n const t = useT()\n const { value, setValue, blur, error, id, disabled, readOnly, aria } = useField<\n Record<string, unknown> | Row[] | null\n >(component)\n\n const rows = useMemo<Row[]>(() => {\n if (Array.isArray(value)) {\n return value.map((row) => ({\n key: String(row.key ?? ''),\n value: row.value === null || row.value === undefined ? '' : String(row.value),\n }))\n }\n\n return Object.entries(value ?? {}).map(([key, rowValue]) => ({\n key,\n value: rowValue === null || rowValue === undefined ? '' : String(rowValue),\n }))\n }, [value])\n\n const context = useFieldActionContext(component)\n const runItemAction = useRunItemAction(context)\n const addAction = resolveItemAction('add', component, undefined, {\n label: component.addActionLabel,\n icon: 'plus',\n variant: 'ghost',\n })\n const deleteAction = resolveItemAction('delete', component, undefined, {\n label: t('form.remove_row', {}, 'Remove row'),\n icon: itemActionIcon('key-value', 'delete', 'trash-2'),\n })\n const reorderAction = resolveItemAction('reorder', component, undefined, {\n label: t('form.reorder_item', {}, 'Reorder'),\n icon: itemActionIcon('key-value', 'reorder', 'grip-vertical'),\n })\n\n const update = (next: Row[]) => setValue(next)\n const deletable = component.deletable && !readOnly && deleteAction !== null\n const reorderable = component.reorderable && !readOnly && !disabled && reorderAction !== null\n const [dragIndex, setDragIndex] = useState<number | null>(null)\n // Moving runs the reorder action: in the browser, or on the server when customised.\n const move = (from: number, to: number) =>\n runItemAction(reorderAction, { items: reorderedItemKeys(rows.length, from, to) }, () =>\n update(moveRepeaterItem(rows, from, to) as Row[]),\n )\n // Move buttons: announced, one at a time, focus kept on the moved row.\n const {\n moveWithButton,\n region: moveRegion,\n containerRef: moveContainerRef,\n } = useItemMoves(value, move, (index) => rows[index]?.key || null, rows.length)\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div ref={moveContainerRef} className=\"overflow-hidden rounded-md border\">\n {moveRegion}\n <table\n className=\"w-full text-sm\"\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n >\n <thead className=\"bg-muted/50 text-muted-foreground text-xs\">\n <tr>\n {reorderable && <th className=\"w-16\" />}\n <th className=\"px-3 py-2 text-start font-medium\">{component.keyLabel}</th>\n <th className=\"px-3 py-2 text-start font-medium\">{component.valueLabel}</th>\n {deletable && <th className=\"w-10\" />}\n </tr>\n </thead>\n <tbody>\n {rows.map((row, index) => (\n <tr\n key={index}\n data-index={index}\n draggable={reorderable}\n onDragStart={() => setDragIndex(index)}\n onDragOver={(event) => reorderable && event.preventDefault()}\n onDrop={() => {\n if (dragIndex !== null && dragIndex !== index) move(dragIndex, index)\n setDragIndex(null)\n }}\n onDragEnd={() => setDragIndex(null)}\n className={cn('border-t', dragIndex === index && 'opacity-50')}\n >\n {reorderable && (\n <td className=\"p-1.5\">\n <div className=\"flex items-center\">\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 data-item-move=\"up\"\n onClick={() => moveWithButton(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 === rows.length - 1}\n data-item-move=\"down\"\n onClick={() => moveWithButton(index, index + 1)}\n >\n <ChevronDownIcon />\n </Button>\n </div>\n </td>\n )}\n <td className=\"p-1.5\">\n <Input\n dir=\"auto\"\n value={row.key}\n readOnly={readOnly || !component.editableKeys}\n disabled={disabled}\n placeholder={component.keyPlaceholder}\n aria-label={component.keyLabel}\n aria-invalid={aria['aria-invalid']}\n className=\"h-8\"\n onChange={(event) =>\n update(\n rows.map((item, i) =>\n i === index ? { ...item, key: event.target.value } : item,\n ),\n )\n }\n onBlur={blur}\n />\n </td>\n <td className=\"p-1.5\">\n <Input\n dir=\"auto\"\n value={row.value}\n readOnly={readOnly || !component.editableValues}\n disabled={disabled}\n placeholder={component.valuePlaceholder}\n aria-label={component.valueLabel}\n aria-invalid={aria['aria-invalid']}\n className=\"h-8\"\n onChange={(event) =>\n update(\n rows.map((item, i) =>\n i === index ? { ...item, value: event.target.value } : item,\n ),\n )\n }\n onBlur={blur}\n />\n </td>\n {deletable && (\n <td className=\"p-1.5\">\n <ItemActionButton\n action={deleteAction}\n context={context}\n args={{ item: String(index) }}\n size=\"sm\"\n disabled={disabled}\n className=\"text-muted-foreground\"\n onClient={() => update(rows.filter((_, i) => i !== index))}\n />\n </td>\n )}\n </tr>\n ))}\n {rows.length === 0 && (\n <tr className=\"border-t\">\n <td\n colSpan={reorderable ? 4 : 3}\n className=\"text-muted-foreground px-3 py-4 text-center text-xs\"\n >\n —\n </td>\n </tr>\n )}\n </tbody>\n </table>\n {component.addable && !readOnly && addAction && (\n <div className=\"border-t p-1.5\">\n <ItemActionButton\n action={addAction}\n context={context}\n size=\"sm\"\n disabled={disabled}\n onClient={() => update([...rows, { key: '', value: '' }])}\n />\n </div>\n )}\n </div>\n </FieldWrapper>\n )\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { useChoice, useT } from "../../context.js";
1
+ import { useChoice, useFormatter, useT } from "../../context.js";
2
2
  import { cn } from "../../lib/utils.js";
3
3
  import { Icon } from "../../lib/icons.js";
4
4
  import { Badge } from "../ui/badge.js";
@@ -85,6 +85,7 @@ function ModalTableSelectField({ component }) {
85
85
  }
86
86
  function ModalTablePicker({ component, initial, onClose, onConfirm }) {
87
87
  const t = useT();
88
+ const format = useFormatter();
88
89
  const choice = useChoice();
89
90
  const form = useFormStore();
90
91
  const multiple = Boolean(component.multiple);
@@ -283,10 +284,10 @@ function ModalTablePicker({ component, initial, onClose, onConfirm }) {
283
284
  /* @__PURE__ */ jsx("span", {
284
285
  className: "text-muted-foreground text-sm",
285
286
  children: paging.total !== void 0 ? t("table.showing", {
286
- first: paging.from,
287
- last: paging.to,
288
- total: paging.total
289
- }, `Showing ${paging.from}–${paging.to} of ${paging.total}`) : `${paging.from}–${paging.to}`
287
+ first: format.number(paging.from),
288
+ last: format.number(paging.to),
289
+ total: format.number(paging.total)
290
+ }, "Showing :first–:last of :total") : `${paging.from}–${paging.to}`
290
291
  }),
291
292
  /* @__PURE__ */ jsx(Button, {
292
293
  type: "button",
@@ -1 +1 @@
1
- {"version":3,"file":"modal-table-select.js","names":[],"sources":["../../../src/components/fields/modal-table-select.tsx"],"sourcesContent":["import type { ModalTableSelectResponse, ModalTableSelectSchema, TableQuery } from '@alfakit/core'\nimport {\n FILTERS_MODAL_WIDTH_CLASSES,\n FormStore,\n debounce,\n fieldIds,\n filterFormComponents,\n modalTablePaging,\n modalTableSelected,\n pickerFilterFormOptions,\n toggleModalTableKey,\n} from '@alfakit/core'\nimport {\n ArrowDownIcon,\n ArrowUpDownIcon,\n ArrowUpIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n SearchIcon,\n XIcon,\n} from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { useChoice, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { FormProvider, useField, useFormStore } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { TableCellContent } from '../table/cells'\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Checkbox } from '../ui/checkbox'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../ui/dialog'\nimport { Input } from '../ui/input'\nimport { Skeleton } from '../ui/skeleton'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'\n\n/**\n * Select records from a table in a modal. The table (columns, search, filters, sorting,\n * pagination) comes from the server's picker endpoint; the field keeps only keys, with\n * labels from the server.\n */\nexport function ModalTableSelectField({ component }: { component: ModalTableSelectSchema }) {\n const t = useT()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<unknown>(component)\n const multiple = Boolean(component.multiple)\n const selected = modalTableSelected(value, multiple)\n const locked = disabled || readOnly\n const [open, setOpen] = useState(false)\n const [picked, setPicked] = useState<Record<string, string>>({})\n const labels = { ...picked, ...component.selectedLabels }\n\n const store = (keys: string[]) => setValue(multiple ? keys : (keys[0] ?? null))\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n id={id}\n role=\"group\"\n data-slot=\"modal-table-select\"\n className=\"flex flex-wrap items-center gap-2\"\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n >\n {selected.length === 0 ? (\n <span className=\"text-muted-foreground text-sm\">\n {component.placeholder ?? t('table_select.none', {}, 'Nothing selected')}\n </span>\n ) : (\n selected.map((key) => (\n <Badge key={key} variant=\"secondary\" className=\"gap-1 pe-1\">\n <span>{labels[key] ?? key}</span>\n {!locked && (\n <button\n type=\"button\"\n className=\"hover:bg-foreground/10 rounded-sm\"\n aria-label={t(\n 'table_select.remove',\n { label: labels[key] ?? key },\n 'Remove :label',\n )}\n onClick={() => store(selected.filter((candidate) => candidate !== key))}\n >\n <XIcon className=\"size-3\" />\n </button>\n )}\n </Badge>\n ))\n )}\n {!locked && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n aria-haspopup=\"dialog\"\n onClick={() => setOpen(true)}\n >\n {component.selectActionIcon && <Icon name={component.selectActionIcon} />}\n {selected.length > 0 && !multiple\n ? t('table_select.change', {}, 'Change')\n : component.selectActionLabel}\n </Button>\n )}\n </div>\n {open && (\n <ModalTablePicker\n component={component}\n initial={selected}\n onClose={() => setOpen(false)}\n onConfirm={(keys, newLabels) => {\n setPicked((state) => ({ ...state, ...newLabels }))\n store(keys)\n setOpen(false)\n }}\n />\n )}\n </FieldWrapper>\n )\n}\n\ninterface PickerProps {\n component: ModalTableSelectSchema\n initial: string[]\n onClose: () => void\n onConfirm: (keys: string[], labels: Record<string, string>) => void\n}\n\nfunction ModalTablePicker({ component, initial, onClose, onConfirm }: PickerProps) {\n const t = useT()\n const choice = useChoice()\n const form = useFormStore()\n const multiple = Boolean(component.multiple)\n const [query, setQuery] = useState<TableQuery>({})\n const [response, setResponse] = useState<ModalTableSelectResponse | null>(null)\n const [failed, setFailed] = useState(false)\n // The query the shown records answer; anything else means a request is in flight.\n const [loadedQuery, setLoadedQuery] = useState<TableQuery | null>(null)\n const loading = loadedQuery !== query\n const [draft, setDraft] = useState<string[]>(initial)\n const [labels, setLabels] = useState<Record<string, string>>({})\n const [search, setSearch] = useState('')\n\n useEffect(() => {\n const controller = new AbortController()\n\n form\n .modalTable(component.statePath, component.tableStateIdentifier, query, controller.signal)\n .then((next) => {\n if (next) {\n setResponse(next)\n setLabels((state) => ({ ...state, ...next.labels }))\n }\n setFailed(false)\n setLoadedQuery(query)\n })\n .catch((error: unknown) => {\n if ((error as { name?: string }).name === 'AbortError') return\n setFailed(true)\n setLoadedQuery(query)\n })\n\n return () => controller.abort()\n }, [form, component.statePath, component.tableStateIdentifier, query])\n\n const table = response?.table\n const pushSearch = useMemo(\n () =>\n debounce(\n (next: string) => setQuery((state) => ({ ...state, search: next || null, page: null })),\n table?.searchDebounce ?? 400,\n ),\n [table?.searchDebounce],\n )\n useEffect(() => () => pushSearch.cancel(), [pushSearch])\n\n const columns = table\n ? table.columns.filter((column) => table.visibleColumns.includes(column.name))\n : []\n const records = table?.records ?? []\n const paging = modalTablePaging(table?.pagination)\n const atMax = multiple && component.maxItems != null && draft.length >= component.maxItems\n\n const toggle = (key: string) =>\n setDraft((state) => toggleModalTableKey(state, key, multiple, component.maxItems))\n\n const sort = (column: string) => {\n const direction =\n table?.sort?.column === column && table.sort.direction === 'asc' ? 'desc' : 'asc'\n setQuery((state) => ({ ...state, sort: column, direction, page: null }))\n }\n\n return (\n <Dialog open onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n className={cn(\n 'flex max-h-[90vh] flex-col gap-4',\n FILTERS_MODAL_WIDTH_CLASSES[component.modalWidth ?? '3xl'] ?? 'sm:max-w-3xl',\n )}\n data-slot=\"modal-table-select-dialog\"\n >\n <DialogHeader>\n <DialogTitle>{component.modalHeading}</DialogTitle>\n {component.modalDescription && (\n // Escaped on the server (`Html::from()`), like action modal descriptions.\n <p\n className=\"text-muted-foreground text-sm\"\n data-slot=\"modal-table-select-description\"\n dangerouslySetInnerHTML={{ __html: component.modalDescription }}\n />\n )}\n <DialogDescription aria-live=\"polite\">\n {choice(\n 'table.selected',\n draft.length,\n { count: draft.length },\n `${draft.length} selected`,\n )}\n </DialogDescription>\n </DialogHeader>\n {table && (table.searchable || (table.filters?.length ?? 0) > 0) && (\n <div className=\"flex flex-col gap-3\">\n {table.searchable && (\n <div className=\"relative\">\n <SearchIcon className=\"text-muted-foreground pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2\" />\n <Input\n type=\"search\"\n value={search}\n aria-label={table.searchPlaceholder}\n placeholder={table.searchPlaceholder}\n className=\"ps-8\"\n onChange={(event) => {\n setSearch(event.target.value)\n pushSearch(event.target.value)\n }}\n />\n </div>\n )}\n {(table.filters?.length ?? 0) > 0 && (\n <PickerFilters\n table={table}\n owner={form}\n path={component.statePath}\n onChange={(filters) => setQuery((state) => ({ ...state, filters, page: null }))}\n />\n )}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-auto rounded-md border\" aria-busy={loading}>\n {failed ? (\n <p role=\"alert\" className=\"text-destructive p-6 text-center text-sm\">\n {t('table_select.load_failed', {}, \"Couldn't load the records.\")}\n </p>\n ) : !table ? (\n <div className=\"flex flex-col gap-2 p-4\" aria-hidden=\"true\">\n <Skeleton className=\"h-8 w-full\" />\n <Skeleton className=\"h-8 w-full\" />\n <Skeleton className=\"h-8 w-full\" />\n </div>\n ) : records.length === 0 ? (\n <p className=\"text-muted-foreground p-6 text-center text-sm\">\n {table.emptyState.heading}\n </p>\n ) : (\n <Table className={cn(loading && 'opacity-60')}>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-10\">\n <span className=\"sr-only\">{t('table.select_row', {}, 'Select row')}</span>\n </TableHead>\n {columns.map((column) => (\n <TableHead\n key={column.name}\n aria-sort={\n table.sort?.column === column.name\n ? table.sort.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1\"\n onClick={() => sort(column.name)}\n >\n {column.label}\n {table.sort?.column === column.name ? (\n table.sort.direction === 'asc' ? (\n <ArrowUpIcon className=\"size-3.5\" />\n ) : (\n <ArrowDownIcon className=\"size-3.5\" />\n )\n ) : (\n <ArrowUpDownIcon className=\"text-muted-foreground size-3.5\" />\n )}\n </button>\n ) : (\n column.label\n )}\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {records.map((record) => {\n const key = String(record.key)\n const checked = draft.includes(key)\n const selectable = record.selectable === true && (checked || !atMax)\n const label = labels[key] ?? key\n\n return (\n <TableRow\n key={key}\n data-state={checked ? 'selected' : undefined}\n className={cn(selectable && 'cursor-pointer')}\n onClick={() => selectable && toggle(key)}\n >\n <TableCell className=\"w-10\" onClick={(event) => event.stopPropagation()}>\n {multiple ? (\n <Checkbox\n checked={checked}\n disabled={!selectable}\n aria-label={t(\n 'table_select.select_record',\n { label },\n `Select ${label}`,\n )}\n onCheckedChange={() => toggle(key)}\n />\n ) : (\n <input\n type=\"radio\"\n name={`${component.statePath}-picker`}\n className=\"accent-primary size-4\"\n checked={checked}\n disabled={!selectable}\n aria-label={t(\n 'table_select.select_record',\n { label },\n `Select ${label}`,\n )}\n onChange={() => toggle(key)}\n />\n )}\n </TableCell>\n {columns.map((column) => (\n <TableCell key={column.name}>\n {record.cells[column.name] && (\n <TableCellContent\n column={column}\n cell={record.cells[column.name]!}\n recordKey={record.key}\n token=\"\"\n component={`picker:${component.statePath}`}\n />\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </TableBody>\n </Table>\n )}\n </div>\n {atMax && (\n <p className=\"text-muted-foreground text-sm\" role=\"status\">\n {choice(\n 'table_select.max_reached',\n component.maxItems ?? 0,\n { count: component.maxItems ?? 0 },\n 'You can select up to :count records.',\n )}\n </p>\n )}\n <DialogFooter className=\"items-center sm:justify-between\">\n {paging ? (\n <nav className=\"flex items-center gap-2\" aria-label={t('pagination', {}, 'Pagination')}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={!paging.hasPrevious || loading}\n aria-label={t('table.previous', {}, 'Previous')}\n onClick={() => setQuery((state) => ({ ...state, page: paging.page - 1 }))}\n >\n <ChevronLeftIcon className=\"rtl:rotate-180\" />\n </Button>\n <span className=\"text-muted-foreground text-sm\">\n {paging.total !== undefined\n ? t(\n 'table.showing',\n { first: paging.from, last: paging.to, total: paging.total },\n `Showing ${paging.from}–${paging.to} of ${paging.total}`,\n )\n : `${paging.from}–${paging.to}`}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={!paging.hasNext || loading}\n aria-label={t('table.next', {}, 'Next')}\n onClick={() => setQuery((state) => ({ ...state, page: paging.page + 1 }))}\n >\n <ChevronRightIcon className=\"rtl:rotate-180\" />\n </Button>\n </nav>\n ) : (\n <span />\n )}\n <div className=\"flex gap-2\">\n {draft.length > 0 && (\n <Button type=\"button\" variant=\"ghost\" onClick={() => setDraft([])}>\n {t('table_select.clear', {}, 'Clear selection')}\n </Button>\n )}\n <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n {t('cancel', {}, 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={() =>\n onConfirm(\n draft,\n Object.fromEntries(\n draft.filter((key) => labels[key]).map((key) => [key, labels[key]!]),\n ),\n )\n }\n >\n {component.modalSubmitActionLabel ?? t('table_select.confirm', {}, 'Select')}\n </Button>\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\n/**\n * The picker's filters: a small form whose state becomes the table's `filters`. Searchable\n * selects among them load options through the owning form's endpoint (`pickerFilters`).\n */\nfunction PickerFilters({\n table,\n owner,\n path,\n onChange,\n}: {\n table: NonNullable<ModalTableSelectResponse['table']>\n owner: FormStore\n path: string\n onChange: (filters: Record<string, Record<string, unknown>> | null) => void\n}) {\n const t = useT()\n const components = useMemo(() => filterFormComponents(table.filters ?? []), [table.filters])\n const [store] = useState(\n () =>\n new FormStore(\n {\n components,\n state: table.filterState ?? {},\n columns: table.filtersFormColumns ?? { default: 1, sm: 2 },\n },\n pickerFilterFormOptions(owner, path),\n ),\n )\n const state = useStore(store.store)\n const latest = useRef(onChange)\n\n useEffect(() => {\n latest.current = onChange\n })\n\n useEffect(() => {\n if (!state.dirty.size) return\n\n const timer = setTimeout(\n () => latest.current(state.values as Record<string, Record<string, unknown>>),\n 400,\n )\n\n return () => clearTimeout(timer)\n }, [state.values, state.dirty])\n\n useEffect(() => () => store.destroy(), [store])\n\n return (\n <FormProvider store={store}>\n <div className=\"bg-muted/30 flex flex-col gap-2 rounded-lg border p-3\">\n <SchemaRenderer components={state.components} columns={state.columns} className=\"gap-3\" />\n {(table.filterIndicators?.length ?? 0) > 0 && (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"self-end\"\n onClick={() => {\n store.fill({ components, state: {}, columns: state.columns })\n onChange(null)\n }}\n >\n {t('table.reset_filters', {}, 'Reset')}\n </Button>\n )}\n </div>\n </FormProvider>\n )\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"modal-table-select.js","names":[],"sources":["../../../src/components/fields/modal-table-select.tsx"],"sourcesContent":["import type { ModalTableSelectResponse, ModalTableSelectSchema, TableQuery } from '@alfakit/core'\nimport {\n FILTERS_MODAL_WIDTH_CLASSES,\n FormStore,\n debounce,\n fieldIds,\n filterFormComponents,\n modalTablePaging,\n modalTableSelected,\n pickerFilterFormOptions,\n toggleModalTableKey,\n} from '@alfakit/core'\nimport {\n ArrowDownIcon,\n ArrowUpDownIcon,\n ArrowUpIcon,\n ChevronLeftIcon,\n ChevronRightIcon,\n SearchIcon,\n XIcon,\n} from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport { useChoice, useFormatter, useT } from '../../context'\nimport { useStore } from '../../hooks/use-store'\nimport { Icon } from '../../lib/icons'\nimport { cn } from '../../lib/utils'\nimport { FormProvider, useField, useFormStore } from '../form/form-context'\nimport { FieldWrapper } from '../schema/field-wrapper'\nimport { SchemaRenderer } from '../schema/schema-renderer'\nimport { TableCellContent } from '../table/cells'\nimport { Badge } from '../ui/badge'\nimport { Button } from '../ui/button'\nimport { Checkbox } from '../ui/checkbox'\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n} from '../ui/dialog'\nimport { Input } from '../ui/input'\nimport { Skeleton } from '../ui/skeleton'\nimport { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'\n\n/**\n * Select records from a table in a modal. The table (columns, search, filters, sorting,\n * pagination) comes from the server's picker endpoint; the field keeps only keys, with\n * labels from the server.\n */\nexport function ModalTableSelectField({ component }: { component: ModalTableSelectSchema }) {\n const t = useT()\n const { value, setValue, error, id, disabled, readOnly, aria } = useField<unknown>(component)\n const multiple = Boolean(component.multiple)\n const selected = modalTableSelected(value, multiple)\n const locked = disabled || readOnly\n const [open, setOpen] = useState(false)\n const [picked, setPicked] = useState<Record<string, string>>({})\n const labels = { ...picked, ...component.selectedLabels }\n\n const store = (keys: string[]) => setValue(multiple ? keys : (keys[0] ?? null))\n\n return (\n <FieldWrapper field={component} htmlFor={id} error={error}>\n <div\n id={id}\n role=\"group\"\n data-slot=\"modal-table-select\"\n className=\"flex flex-wrap items-center gap-2\"\n aria-labelledby={component.label ? fieldIds(id).label : undefined}\n aria-describedby={aria['aria-describedby']}\n >\n {selected.length === 0 ? (\n <span className=\"text-muted-foreground text-sm\">\n {component.placeholder ?? t('table_select.none', {}, 'Nothing selected')}\n </span>\n ) : (\n selected.map((key) => (\n <Badge key={key} variant=\"secondary\" className=\"gap-1 pe-1\">\n <span>{labels[key] ?? key}</span>\n {!locked && (\n <button\n type=\"button\"\n className=\"hover:bg-foreground/10 rounded-sm\"\n aria-label={t(\n 'table_select.remove',\n { label: labels[key] ?? key },\n 'Remove :label',\n )}\n onClick={() => store(selected.filter((candidate) => candidate !== key))}\n >\n <XIcon className=\"size-3\" />\n </button>\n )}\n </Badge>\n ))\n )}\n {!locked && (\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n aria-haspopup=\"dialog\"\n onClick={() => setOpen(true)}\n >\n {component.selectActionIcon && <Icon name={component.selectActionIcon} />}\n {selected.length > 0 && !multiple\n ? t('table_select.change', {}, 'Change')\n : component.selectActionLabel}\n </Button>\n )}\n </div>\n {open && (\n <ModalTablePicker\n component={component}\n initial={selected}\n onClose={() => setOpen(false)}\n onConfirm={(keys, newLabels) => {\n setPicked((state) => ({ ...state, ...newLabels }))\n store(keys)\n setOpen(false)\n }}\n />\n )}\n </FieldWrapper>\n )\n}\n\ninterface PickerProps {\n component: ModalTableSelectSchema\n initial: string[]\n onClose: () => void\n onConfirm: (keys: string[], labels: Record<string, string>) => void\n}\n\nfunction ModalTablePicker({ component, initial, onClose, onConfirm }: PickerProps) {\n const t = useT()\n const format = useFormatter()\n const choice = useChoice()\n const form = useFormStore()\n const multiple = Boolean(component.multiple)\n const [query, setQuery] = useState<TableQuery>({})\n const [response, setResponse] = useState<ModalTableSelectResponse | null>(null)\n const [failed, setFailed] = useState(false)\n // The query the shown records answer; anything else means a request is in flight.\n const [loadedQuery, setLoadedQuery] = useState<TableQuery | null>(null)\n const loading = loadedQuery !== query\n const [draft, setDraft] = useState<string[]>(initial)\n const [labels, setLabels] = useState<Record<string, string>>({})\n const [search, setSearch] = useState('')\n\n useEffect(() => {\n const controller = new AbortController()\n\n form\n .modalTable(component.statePath, component.tableStateIdentifier, query, controller.signal)\n .then((next) => {\n if (next) {\n setResponse(next)\n setLabels((state) => ({ ...state, ...next.labels }))\n }\n setFailed(false)\n setLoadedQuery(query)\n })\n .catch((error: unknown) => {\n if ((error as { name?: string }).name === 'AbortError') return\n setFailed(true)\n setLoadedQuery(query)\n })\n\n return () => controller.abort()\n }, [form, component.statePath, component.tableStateIdentifier, query])\n\n const table = response?.table\n const pushSearch = useMemo(\n () =>\n debounce(\n (next: string) => setQuery((state) => ({ ...state, search: next || null, page: null })),\n table?.searchDebounce ?? 400,\n ),\n [table?.searchDebounce],\n )\n useEffect(() => () => pushSearch.cancel(), [pushSearch])\n\n const columns = table\n ? table.columns.filter((column) => table.visibleColumns.includes(column.name))\n : []\n const records = table?.records ?? []\n const paging = modalTablePaging(table?.pagination)\n const atMax = multiple && component.maxItems != null && draft.length >= component.maxItems\n\n const toggle = (key: string) =>\n setDraft((state) => toggleModalTableKey(state, key, multiple, component.maxItems))\n\n const sort = (column: string) => {\n const direction =\n table?.sort?.column === column && table.sort.direction === 'asc' ? 'desc' : 'asc'\n setQuery((state) => ({ ...state, sort: column, direction, page: null }))\n }\n\n return (\n <Dialog open onOpenChange={(next) => !next && onClose()}>\n <DialogContent\n className={cn(\n 'flex max-h-[90vh] flex-col gap-4',\n FILTERS_MODAL_WIDTH_CLASSES[component.modalWidth ?? '3xl'] ?? 'sm:max-w-3xl',\n )}\n data-slot=\"modal-table-select-dialog\"\n >\n <DialogHeader>\n <DialogTitle>{component.modalHeading}</DialogTitle>\n {component.modalDescription && (\n // Escaped on the server (`Html::from()`), like action modal descriptions.\n <p\n className=\"text-muted-foreground text-sm\"\n data-slot=\"modal-table-select-description\"\n dangerouslySetInnerHTML={{ __html: component.modalDescription }}\n />\n )}\n <DialogDescription aria-live=\"polite\">\n {choice(\n 'table.selected',\n draft.length,\n { count: draft.length },\n `${draft.length} selected`,\n )}\n </DialogDescription>\n </DialogHeader>\n {table && (table.searchable || (table.filters?.length ?? 0) > 0) && (\n <div className=\"flex flex-col gap-3\">\n {table.searchable && (\n <div className=\"relative\">\n <SearchIcon className=\"text-muted-foreground pointer-events-none absolute start-2.5 top-1/2 size-4 -translate-y-1/2\" />\n <Input\n type=\"search\"\n value={search}\n aria-label={table.searchPlaceholder}\n placeholder={table.searchPlaceholder}\n className=\"ps-8\"\n onChange={(event) => {\n setSearch(event.target.value)\n pushSearch(event.target.value)\n }}\n />\n </div>\n )}\n {(table.filters?.length ?? 0) > 0 && (\n <PickerFilters\n table={table}\n owner={form}\n path={component.statePath}\n onChange={(filters) => setQuery((state) => ({ ...state, filters, page: null }))}\n />\n )}\n </div>\n )}\n <div className=\"min-h-0 flex-1 overflow-auto rounded-md border\" aria-busy={loading}>\n {failed ? (\n <p role=\"alert\" className=\"text-destructive p-6 text-center text-sm\">\n {t('table_select.load_failed', {}, \"Couldn't load the records.\")}\n </p>\n ) : !table ? (\n <div className=\"flex flex-col gap-2 p-4\" aria-hidden=\"true\">\n <Skeleton className=\"h-8 w-full\" />\n <Skeleton className=\"h-8 w-full\" />\n <Skeleton className=\"h-8 w-full\" />\n </div>\n ) : records.length === 0 ? (\n <p className=\"text-muted-foreground p-6 text-center text-sm\">\n {table.emptyState.heading}\n </p>\n ) : (\n <Table className={cn(loading && 'opacity-60')}>\n <TableHeader>\n <TableRow>\n <TableHead className=\"w-10\">\n <span className=\"sr-only\">{t('table.select_row', {}, 'Select row')}</span>\n </TableHead>\n {columns.map((column) => (\n <TableHead\n key={column.name}\n aria-sort={\n table.sort?.column === column.name\n ? table.sort.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined\n }\n >\n {column.sortable ? (\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1\"\n onClick={() => sort(column.name)}\n >\n {column.label}\n {table.sort?.column === column.name ? (\n table.sort.direction === 'asc' ? (\n <ArrowUpIcon className=\"size-3.5\" />\n ) : (\n <ArrowDownIcon className=\"size-3.5\" />\n )\n ) : (\n <ArrowUpDownIcon className=\"text-muted-foreground size-3.5\" />\n )}\n </button>\n ) : (\n column.label\n )}\n </TableHead>\n ))}\n </TableRow>\n </TableHeader>\n <TableBody>\n {records.map((record) => {\n const key = String(record.key)\n const checked = draft.includes(key)\n const selectable = record.selectable === true && (checked || !atMax)\n const label = labels[key] ?? key\n\n return (\n <TableRow\n key={key}\n data-state={checked ? 'selected' : undefined}\n className={cn(selectable && 'cursor-pointer')}\n onClick={() => selectable && toggle(key)}\n >\n <TableCell className=\"w-10\" onClick={(event) => event.stopPropagation()}>\n {multiple ? (\n <Checkbox\n checked={checked}\n disabled={!selectable}\n aria-label={t(\n 'table_select.select_record',\n { label },\n `Select ${label}`,\n )}\n onCheckedChange={() => toggle(key)}\n />\n ) : (\n <input\n type=\"radio\"\n name={`${component.statePath}-picker`}\n className=\"accent-primary size-4\"\n checked={checked}\n disabled={!selectable}\n aria-label={t(\n 'table_select.select_record',\n { label },\n `Select ${label}`,\n )}\n onChange={() => toggle(key)}\n />\n )}\n </TableCell>\n {columns.map((column) => (\n <TableCell key={column.name}>\n {record.cells[column.name] && (\n <TableCellContent\n column={column}\n cell={record.cells[column.name]!}\n recordKey={record.key}\n token=\"\"\n component={`picker:${component.statePath}`}\n />\n )}\n </TableCell>\n ))}\n </TableRow>\n )\n })}\n </TableBody>\n </Table>\n )}\n </div>\n {atMax && (\n <p className=\"text-muted-foreground text-sm\" role=\"status\">\n {choice(\n 'table_select.max_reached',\n component.maxItems ?? 0,\n { count: component.maxItems ?? 0 },\n 'You can select up to :count records.',\n )}\n </p>\n )}\n <DialogFooter className=\"items-center sm:justify-between\">\n {paging ? (\n <nav className=\"flex items-center gap-2\" aria-label={t('pagination', {}, 'Pagination')}>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={!paging.hasPrevious || loading}\n aria-label={t('table.previous', {}, 'Previous')}\n onClick={() => setQuery((state) => ({ ...state, page: paging.page - 1 }))}\n >\n <ChevronLeftIcon className=\"rtl:rotate-180\" />\n </Button>\n <span className=\"text-muted-foreground text-sm\">\n {paging.total !== undefined\n ? t(\n 'table.showing',\n {\n first: format.number(paging.from),\n last: format.number(paging.to),\n total: format.number(paging.total),\n },\n 'Showing :first–:last of :total',\n )\n : `${paging.from}–${paging.to}`}\n </span>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"icon-sm\"\n disabled={!paging.hasNext || loading}\n aria-label={t('table.next', {}, 'Next')}\n onClick={() => setQuery((state) => ({ ...state, page: paging.page + 1 }))}\n >\n <ChevronRightIcon className=\"rtl:rotate-180\" />\n </Button>\n </nav>\n ) : (\n <span />\n )}\n <div className=\"flex gap-2\">\n {draft.length > 0 && (\n <Button type=\"button\" variant=\"ghost\" onClick={() => setDraft([])}>\n {t('table_select.clear', {}, 'Clear selection')}\n </Button>\n )}\n <Button type=\"button\" variant=\"outline\" onClick={onClose}>\n {t('cancel', {}, 'Cancel')}\n </Button>\n <Button\n type=\"button\"\n onClick={() =>\n onConfirm(\n draft,\n Object.fromEntries(\n draft.filter((key) => labels[key]).map((key) => [key, labels[key]!]),\n ),\n )\n }\n >\n {component.modalSubmitActionLabel ?? t('table_select.confirm', {}, 'Select')}\n </Button>\n </div>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n )\n}\n\n/**\n * The picker's filters: a small form whose state becomes the table's `filters`. Searchable\n * selects among them load options through the owning form's endpoint (`pickerFilters`).\n */\nfunction PickerFilters({\n table,\n owner,\n path,\n onChange,\n}: {\n table: NonNullable<ModalTableSelectResponse['table']>\n owner: FormStore\n path: string\n onChange: (filters: Record<string, Record<string, unknown>> | null) => void\n}) {\n const t = useT()\n const components = useMemo(() => filterFormComponents(table.filters ?? []), [table.filters])\n const [store] = useState(\n () =>\n new FormStore(\n {\n components,\n state: table.filterState ?? {},\n columns: table.filtersFormColumns ?? { default: 1, sm: 2 },\n },\n pickerFilterFormOptions(owner, path),\n ),\n )\n const state = useStore(store.store)\n const latest = useRef(onChange)\n\n useEffect(() => {\n latest.current = onChange\n })\n\n useEffect(() => {\n if (!state.dirty.size) return\n\n const timer = setTimeout(\n () => latest.current(state.values as Record<string, Record<string, unknown>>),\n 400,\n )\n\n return () => clearTimeout(timer)\n }, [state.values, state.dirty])\n\n useEffect(() => () => store.destroy(), [store])\n\n return (\n <FormProvider store={store}>\n <div className=\"bg-muted/30 flex flex-col gap-2 rounded-lg border p-3\">\n <SchemaRenderer components={state.components} columns={state.columns} className=\"gap-3\" />\n {(table.filterIndicators?.length ?? 0) > 0 && (\n <Button\n type=\"button\"\n variant=\"link\"\n size=\"sm\"\n className=\"self-end\"\n onClick={() => {\n store.fill({ components, state: {}, columns: state.columns })\n onChange(null)\n }}\n >\n {t('table.reset_filters', {}, 'Reset')}\n </Button>\n )}\n </div>\n </FormProvider>\n )\n}\n"],"mappings":"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"}
@@ -10,7 +10,7 @@ import { FieldWrapper } from "../schema/field-wrapper.js";
10
10
  import { Collapsible, CollapsibleContent } from "../ui/collapsible.js";
11
11
  import { AddActionRow, CollapseAllActions, ItemActionButton, ItemHeaderActions, useItemActionFallbacks, useItemActions, useItemMoves, useRunItemAction } from "./item-actions.js";
12
12
  import { RepeaterTable } from "./repeater-table.js";
13
- import { VIRTUAL_ESTIMATES, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, fieldIds, gridColumnsClasses, isDeferredPlaceholder, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, reorderedItemKeys, replacePlaceholder, virtualizeOptions } from "@alfakit/core";
13
+ import { VIRTUAL_ESTIMATES, appendRepeaterItem, cloneIndexedState, cloneRepeaterItem, fieldIds, gridColumnsClasses, isDeferredPlaceholder, moveIndexedState, moveRepeaterItem, removeIndexedState, removeRepeaterItem, reorderedItemKeys, templateForIndex, virtualizeOptions } from "@alfakit/core";
14
14
  import { useState } from "react";
15
15
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
16
16
  //#region src/components/fields/repeater.tsx
@@ -59,7 +59,7 @@ function RepeaterFieldContent({ component }) {
59
59
  const dropItem = (from, to) => runItemAction(resolve("reorder"), { items: reorderedItemKeys(items.length, from, to) }, () => moveItem(from, to));
60
60
  const setAllCollapsed = (value) => setCollapsed(Object.fromEntries(items.map((_, index) => [index, value])));
61
61
  const addItem = () => setValue(appendRepeaterItem(items, component.simple ? { [Object.keys(component.templateState)[0] ?? "value"]: null } : component.templateState));
62
- const schemaFor = (index) => component.items[index]?.schema ?? replacePlaceholder(component.template, "__INDEX__", String(index));
62
+ const schemaFor = (index) => component.items[index]?.schema ?? templateForIndex(component.template, index);
63
63
  const canAdd = component.addable && !locked && (component.maxItems == null || items.length < component.maxItems);
64
64
  const canDelete = component.deletable && !locked && (component.minItems == null || items.length > component.minItems);
65
65
  const draggable = component.reorderable && component.reorderableWithDragAndDrop !== false && !locked && resolve("reorder") !== null;