@alfakit/react 0.1.2 → 0.1.7
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.
- package/CHANGELOG.md +16 -0
- package/dist/components/actions/action-modals.js +4 -0
- package/dist/components/actions/action-modals.js.map +1 -1
- package/dist/components/ai/copilot.d.ts +3 -1
- package/dist/components/ai/copilot.d.ts.map +1 -1
- package/dist/components/ai/copilot.js +4 -1
- package/dist/components/ai/copilot.js.map +1 -1
- package/dist/components/cms/live-preview.js +3 -2
- package/dist/components/cms/live-preview.js.map +1 -1
- package/dist/components/cms/page.js +1 -1
- package/dist/components/fields/builder.js +15 -6
- package/dist/components/fields/builder.js.map +1 -1
- package/dist/components/fields/image-editor-dialog.js +4 -2
- package/dist/components/fields/image-editor-dialog.js.map +1 -1
- package/dist/components/fields/item-actions.js +10 -10
- package/dist/components/fields/item-actions.js.map +1 -1
- package/dist/components/fields/key-value.d.ts.map +1 -1
- package/dist/components/fields/key-value.js +3 -3
- package/dist/components/fields/key-value.js.map +1 -1
- package/dist/components/fields/modal-table-select.js +6 -5
- package/dist/components/fields/modal-table-select.js.map +1 -1
- package/dist/components/fields/repeater.js +2 -2
- package/dist/components/fields/repeater.js.map +1 -1
- package/dist/components/fields/rich-editor.js +2 -1
- package/dist/components/fields/rich-editor.js.map +1 -1
- package/dist/components/form/form-block.js +1 -1
- package/dist/components/form/form-block.js.map +1 -1
- package/dist/components/form/form-context.js +2 -2
- package/dist/components/form/form-context.js.map +1 -1
- package/dist/components/kanban/kanban.js +4 -3
- package/dist/components/kanban/kanban.js.map +1 -1
- package/dist/components/layout/section.js +2 -2
- package/dist/components/layout/section.js.map +1 -1
- package/dist/components/panel/app-sidebar.d.ts +2 -3
- package/dist/components/panel/app-sidebar.d.ts.map +1 -1
- package/dist/components/panel/app-sidebar.js +1 -24
- package/dist/components/panel/app-sidebar.js.map +1 -1
- package/dist/components/panel/brand.d.ts +10 -0
- package/dist/components/panel/brand.d.ts.map +1 -0
- package/dist/components/panel/brand.js +37 -0
- package/dist/components/panel/brand.js.map +1 -0
- package/dist/components/panel/index.d.ts +2 -1
- package/dist/components/panel/topbar.js +1 -1
- package/dist/components/realtime/live-records-pill.d.ts.map +1 -1
- package/dist/components/realtime/live-records-pill.js +6 -3
- package/dist/components/realtime/live-records-pill.js.map +1 -1
- package/dist/components/table/cells.d.ts +6 -0
- package/dist/components/table/cells.d.ts.map +1 -1
- package/dist/components/table/cells.js +6 -2
- package/dist/components/table/cells.js.map +1 -1
- package/dist/components/table/column-manager.js +8 -7
- package/dist/components/table/column-manager.js.map +1 -1
- package/dist/components/table/content-layout.js +2 -1
- package/dist/components/table/content-layout.js.map +1 -1
- package/dist/components/table/pagination.js +16 -13
- package/dist/components/table/pagination.js.map +1 -1
- package/dist/components/table/table-block.js +14 -4
- package/dist/components/table/table-block.js.map +1 -1
- package/dist/components/ui/dialog.d.ts.map +1 -1
- package/dist/components/ui/dialog.js +3 -1
- package/dist/components/ui/dialog.js.map +1 -1
- package/dist/components/ui/popover.d.ts.map +1 -1
- package/dist/components/ui/popover.js +3 -1
- package/dist/components/ui/popover.js.map +1 -1
- package/dist/components/ui/sheet.d.ts.map +1 -1
- package/dist/components/ui/sheet.js +3 -1
- package/dist/components/ui/sheet.js.map +1 -1
- package/dist/components/ui/tooltip.js +3 -1
- package/dist/components/ui/tooltip.js.map +1 -1
- package/dist/context.d.ts +5 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +5 -1
- package/dist/context.js.map +1 -1
- package/dist/create-alfakit.d.ts.map +1 -1
- package/dist/create-alfakit.js +8 -5
- package/dist/create-alfakit.js.map +1 -1
- package/dist/eject-manifest.json +16 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +24 -21
- package/dist/layouts/app-layout.d.ts.map +1 -1
- package/dist/layouts/app-layout.js +2 -29
- package/dist/layouts/app-layout.js.map +1 -1
- package/dist/layouts/auth-layout.js +2 -2
- package/dist/layouts/auth-layout.js.map +1 -1
- package/dist/layouts/fullscreen-layout.js +1 -1
- package/dist/layouts/fullscreen-layout.js.map +1 -1
- package/dist/layouts/lazy-layouts.js +15 -0
- package/dist/layouts/lazy-layouts.js.map +1 -0
- package/dist/layouts/max-widths.js +34 -0
- package/dist/layouts/max-widths.js.map +1 -0
- package/dist/layouts/panel-layouts.d.ts +10 -0
- package/dist/layouts/panel-layouts.d.ts.map +1 -0
- package/dist/layouts/panel-layouts.js +20 -0
- package/dist/layouts/panel-layouts.js.map +1 -0
- package/dist/layouts/top-nav-layout.js +2 -2
- package/dist/layouts/top-nav-layout.js.map +1 -1
- package/dist/pages/page.d.ts.map +1 -1
- package/dist/pages/page.js +12 -7
- package/dist/pages/page.js.map +1 -1
- package/dist/standalone/alfakit-component.js +11 -6
- package/dist/standalone/alfakit-component.js.map +1 -1
- package/dist/standalone.css +7 -0
- package/dist/starter-kit/dashboard-widgets.d.ts +33 -0
- package/dist/starter-kit/dashboard-widgets.d.ts.map +1 -0
- package/dist/starter-kit/dashboard-widgets.js +53 -0
- package/dist/starter-kit/dashboard-widgets.js.map +1 -0
- package/dist/starter-kit/impersonation-banner.js +34 -0
- package/dist/starter-kit/impersonation-banner.js.map +1 -0
- package/dist/starter-kit/index.d.ts +3 -2
- package/dist/starter-kit/index.js +3 -2
- package/dist/starter-kit/starter-kit.d.ts +6 -0
- package/dist/starter-kit/starter-kit.d.ts.map +1 -1
- package/dist/starter-kit/starter-kit.js +18 -7
- package/dist/starter-kit/starter-kit.js.map +1 -1
- package/dist/starter-kit/widgets-loaded.js +20 -4
- package/dist/starter-kit/widgets-loaded.js.map +1 -1
- package/dist/starter-kit/widgets-triggers.js +34 -3
- package/dist/starter-kit/widgets-triggers.js.map +1 -1
- package/dist/starter-kit/widgets.d.ts +13 -5
- package/dist/starter-kit/widgets.d.ts.map +1 -1
- package/dist/starter-kit/widgets.js +56 -18
- package/dist/starter-kit/widgets.js.map +1 -1
- package/dist/styles.css +15 -2
- package/dist/themes.css +6 -1
- package/package.json +2 -2
- package/src/components/actions/action-modals.tsx +10 -0
- package/src/components/ai/copilot.tsx +14 -1
- package/src/components/cms/live-preview.tsx +5 -2
- package/src/components/fields/builder.tsx +14 -5
- package/src/components/fields/image-editor-dialog.tsx +7 -2
- package/src/components/fields/item-actions.tsx +32 -9
- package/src/components/fields/key-value.tsx +3 -2
- package/src/components/fields/modal-table-select.tsx +8 -3
- package/src/components/fields/repeater.tsx +2 -3
- package/src/components/fields/rich-editor.tsx +3 -0
- package/src/components/form/form-block.tsx +2 -1
- package/src/components/form/form-context.tsx +2 -2
- package/src/components/kanban/kanban.tsx +16 -3
- package/src/components/layout/section.tsx +2 -2
- package/src/components/panel/app-sidebar.tsx +2 -38
- package/src/components/panel/brand.tsx +46 -0
- package/src/components/realtime/live-records-pill.tsx +4 -3
- package/src/components/table/cells.tsx +9 -2
- package/src/components/table/column-manager.tsx +12 -10
- package/src/components/table/content-layout.tsx +5 -1
- package/src/components/table/pagination.tsx +16 -9
- package/src/components/table/table-block.tsx +25 -12
- package/src/components/ui/dialog.tsx +5 -1
- package/src/components/ui/popover.tsx +5 -1
- package/src/components/ui/sheet.tsx +5 -1
- package/src/components/ui/tooltip.tsx +5 -1
- package/src/context.tsx +10 -0
- package/src/create-alfakit.tsx +11 -2
- package/src/index.ts +2 -0
- package/src/layouts/app-layout.tsx +4 -28
- package/src/layouts/auth-layout.tsx +2 -2
- package/src/layouts/fullscreen-layout.tsx +1 -1
- package/src/layouts/lazy-layouts.ts +14 -0
- package/src/layouts/max-widths.ts +29 -0
- package/src/layouts/panel-layouts.ts +17 -0
- package/src/layouts/top-nav-layout.tsx +2 -2
- package/src/pages/page.tsx +21 -12
- package/src/standalone/alfakit-component.tsx +18 -7
- package/src/starter-kit/dashboard-widgets.tsx +78 -0
- package/src/starter-kit/impersonation-banner.tsx +48 -0
- package/src/starter-kit/index.ts +2 -0
- package/src/starter-kit/starter-kit.tsx +31 -4
- package/src/starter-kit/widgets-loaded.tsx +22 -0
- package/src/starter-kit/widgets-triggers.tsx +36 -1
- package/src/starter-kit/widgets.tsx +95 -20
- 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":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"key-value.d.ts","names":[],"sources":["../../../src/components/fields/key-value.tsx"],"mappings":";;;;;;
|
|
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
|
-
},
|
|
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,
|
|
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 ??
|
|
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;
|