@webiny/admin-ui 6.6.0-alpha.0 → 6.6.0-alpha.1
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/Accordion/components/AccordionContent.js +4 -1
- package/Accordion/components/AccordionContent.js.map +1 -1
- package/Accordion/components/AccordionItem.d.ts +1 -0
- package/Accordion/components/AccordionItem.js.map +1 -1
- package/Accordion/components/AccordionItemDragHandle.d.ts +2 -1
- package/Accordion/components/AccordionItemDragHandle.js +2 -1
- package/Accordion/components/AccordionItemDragHandle.js.map +1 -1
- package/Accordion/components/AccordionTrigger.js +4 -2
- package/Accordion/components/AccordionTrigger.js.map +1 -1
- package/BetaBadge/BetaBadge.d.ts +6 -0
- package/BetaBadge/BetaBadge.js +7 -0
- package/BetaBadge/BetaBadge.js.map +1 -0
- package/Breadcrumbs/Breadcrumbs.d.ts +54 -0
- package/Breadcrumbs/Breadcrumbs.js +66 -0
- package/Breadcrumbs/Breadcrumbs.js.map +1 -0
- package/Breadcrumbs/Breadcrumbs.stories.d.ts +9 -0
- package/Breadcrumbs/Breadcrumbs.stories.js +71 -0
- package/Breadcrumbs/Breadcrumbs.stories.js.map +1 -0
- package/Breadcrumbs/index.d.ts +1 -0
- package/Breadcrumbs/index.js +1 -0
- package/Calendar/calendarClassNames.js +1 -1
- package/Calendar/calendarClassNames.js.map +1 -1
- package/CodeEditor/CodeEditorPrimitive.js +3 -1
- package/CodeEditor/CodeEditorPrimitive.js.map +1 -1
- package/DataTable/DataTable.d.ts +16 -6
- package/DataTable/DataTable.js +18 -7
- package/DataTable/DataTable.js.map +1 -1
- package/DataTable/components/ColumnsVisibility.d.ts +5 -4
- package/DataTable/components/ColumnsVisibility.js +7 -3
- package/DataTable/components/ColumnsVisibility.js.map +1 -1
- package/DatePicker/primitives/DateTimePicker.d.ts +1 -1
- package/DatePicker/primitives/DateTimePicker.js +15 -3
- package/DatePicker/primitives/DateTimePicker.js.map +1 -1
- package/DatePicker/primitives/components/DatePickerTrigger.js +1 -1
- package/DatePicker/primitives/components/DatePickerTrigger.js.map +1 -1
- package/DatePicker/utils/types.d.ts +2 -0
- package/DropdownMenu/components/DropdownMenuItem.d.ts +5 -0
- package/DropdownMenu/components/DropdownMenuItem.js +3 -2
- package/DropdownMenu/components/DropdownMenuItem.js.map +1 -1
- package/FilePicker/FilePicker.d.ts +12 -12
- package/FilePicker/primitives/FilePickerPrimitive.d.ts +12 -12
- package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.d.ts +13 -2
- package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js +15 -6
- package/FilePicker/primitives/components/previews/ImagePreview/ImagePreview.js.map +1 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.d.ts +6 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js +3 -2
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemPreview.js.map +1 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.d.ts +10 -1
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js +6 -2
- package/FilePicker/primitives/components/previews/RichItemPreview/RichItemThumbnail.js.map +1 -1
- package/ImageEditor/AspectRatioPreview.d.ts +16 -0
- package/ImageEditor/AspectRatioPreview.js +26 -0
- package/ImageEditor/AspectRatioPreview.js.map +1 -0
- package/ImageEditor/CropHotspotEditor.d.ts +22 -0
- package/ImageEditor/CropHotspotEditor.js +402 -0
- package/ImageEditor/CropHotspotEditor.js.map +1 -0
- package/ImageEditor/ImageEditor.d.ts +18 -0
- package/ImageEditor/ImageEditor.js +151 -0
- package/ImageEditor/ImageEditor.js.map +1 -0
- package/ImageEditor/ImageEditor.stories.d.ts +6 -0
- package/ImageEditor/ImageEditor.stories.js +42 -0
- package/ImageEditor/ImageEditor.stories.js.map +1 -0
- package/ImageEditor/MetaFields.d.ts +12 -0
- package/ImageEditor/MetaFields.js +20 -0
- package/ImageEditor/MetaFields.js.map +1 -0
- package/ImageEditor/SectionLabel.d.ts +8 -0
- package/ImageEditor/SectionLabel.js +7 -0
- package/ImageEditor/SectionLabel.js.map +1 -0
- package/ImageEditor/geometry.d.ts +46 -0
- package/ImageEditor/geometry.js +126 -0
- package/ImageEditor/geometry.js.map +1 -0
- package/ImageEditor/index.d.ts +3 -0
- package/ImageEditor/index.js +3 -0
- package/ImageEditor/types.d.ts +52 -0
- package/ImageEditor/types.js +35 -0
- package/ImageEditor/types.js.map +1 -0
- package/Markdown/Markdown.d.ts +47 -0
- package/Markdown/Markdown.js +17 -0
- package/Markdown/Markdown.js.map +1 -0
- package/Markdown/index.d.ts +1 -0
- package/Markdown/index.js +1 -0
- package/MultiFilePicker/MultiFilePicker.d.ts +12 -12
- package/MultiFilePicker/primitives/MultiFilePickerPrimitive.d.ts +12 -12
- package/MultiSelect/primitives/MultiSelectPrimitive.js +1 -1
- package/MultiSelect/primitives/MultiSelectPrimitive.js.map +1 -1
- package/Select/primitives/components/SelectContent.js +1 -1
- package/Select/primitives/components/SelectContent.js.map +1 -1
- package/Toast/useToast.stories.js.map +1 -1
- package/exports/admin/ui.d.ts +1 -0
- package/exports/admin/ui.js +1 -0
- package/index.d.ts +4 -0
- package/index.js +4 -0
- package/package.json +18 -18
- package/theme.css +9 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageEditor/CropHotspotEditor.js","sources":["../../src/ImageEditor/CropHotspotEditor.tsx"],"sourcesContent":["import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from \"react\";\nimport { Select } from \"~/Select/index.js\";\nimport { Text } from \"~/Text/index.js\";\nimport { SectionLabel } from \"./SectionLabel.js\";\nimport { DEFAULT_ASPECT_RATIOS } from \"./types.js\";\nimport type { ImageEditorCrop, ImageEditorHotspot, ImageEditorImage } from \"./types.js\";\n\ninterface CropHotspotEditorProps {\n image: ImageEditorImage;\n crop: ImageEditorCrop | undefined;\n hotspot: ImageEditorHotspot | undefined;\n /** Currently selected crop shape id (\"free\" or a preset id). Controlled by the parent. */\n aspectId: string;\n onChangeAspect: (id: string) => void;\n onChangeCrop: (crop: ImageEditorCrop) => void;\n onChangeHotspot: (hotspot: ImageEditorHotspot) => void;\n}\n\n/** Crop rectangle in edge positions (0..1): left/top/right/bottom, left<right, top<bottom. */\ninterface Edges {\n left: number;\n top: number;\n right: number;\n bottom: number;\n}\n\ntype Handle = \"nw\" | \"n\" | \"ne\" | \"e\" | \"se\" | \"s\" | \"sw\" | \"w\";\n\nconst MIN = 0.05;\nexport const FREE = \"free\";\n\n/**\n * Infer which crop-shape preset a saved crop corresponds to, by comparing the\n * crop's on-screen (pixel) aspect ratio against the presets. The crop stores only\n * edge insets — not the shape used to draw it — so on reopen we recover the shape\n * here; a crop drawn with a preset matches it closely, while a free crop won't.\n */\nexport const inferAspectId = (\n crop: ImageEditorCrop | undefined,\n width: number,\n height: number\n): string => {\n if (!crop || !width || !height) {\n return FREE;\n }\n const cropWidth = Math.max(0, 1 - (crop.left ?? 0) - (crop.right ?? 0));\n const cropHeight = Math.max(0, 1 - (crop.top ?? 0) - (crop.bottom ?? 0));\n if (cropWidth <= 0 || cropHeight <= 0) {\n return FREE;\n }\n const displayRatio = (cropWidth * width) / (cropHeight * height);\n const match = DEFAULT_ASPECT_RATIOS.find(ar => Math.abs(ar.ratio - displayRatio) < 0.02);\n return match?.id ?? FREE;\n};\n\n// Canvas fits within this box (px), preserving the image aspect ratio, so tall\n// images don't take up the full screen height.\nconst MAX_CANVAS_WIDTH = 640;\nconst MAX_CANVAS_HEIGHT = 460;\n\nconst clamp01 = (n: number) => Math.min(1, Math.max(0, n));\n\nconst cropToEdges = (crop: ImageEditorCrop | undefined): Edges => ({\n left: clamp01(crop?.left ?? 0),\n top: clamp01(crop?.top ?? 0),\n right: 1 - clamp01(crop?.right ?? 0),\n bottom: 1 - clamp01(crop?.bottom ?? 0)\n});\n\nconst edgesToCrop = (e: Edges): ImageEditorCrop => ({\n top: e.top,\n left: e.left,\n bottom: 1 - e.bottom,\n right: 1 - e.right\n});\n\n// Which edges each handle moves.\nconst HANDLE_EDGES: Record<Handle, Array<keyof Edges>> = {\n nw: [\"top\", \"left\"],\n n: [\"top\"],\n ne: [\"top\", \"right\"],\n e: [\"right\"],\n se: [\"bottom\", \"right\"],\n s: [\"bottom\"],\n sw: [\"bottom\", \"left\"],\n w: [\"left\"]\n};\n\nconst CORNER_HANDLES: Handle[] = [\"nw\", \"ne\", \"se\", \"sw\"];\nconst EDGE_HANDLES: Handle[] = [\"n\", \"e\", \"s\", \"w\"];\n\nconst clampHotspot = (h: ImageEditorHotspot | undefined, e: Edges): ImageEditorHotspot => ({\n x: Math.min(e.right, Math.max(e.left, h?.x ?? (e.left + e.right) / 2)),\n y: Math.min(e.bottom, Math.max(e.top, h?.y ?? (e.top + e.bottom) / 2)),\n width: h?.width ?? 1,\n height: h?.height ?? 1\n});\n\nexport const CropHotspotEditor = ({\n image,\n crop,\n hotspot,\n aspectId,\n onChangeAspect,\n onChangeCrop,\n onChangeHotspot\n}: CropHotspotEditorProps) => {\n const surfaceRef = useRef<HTMLDivElement>(null);\n const dragRef = useRef<\n | { mode: \"focal\" }\n | { mode: `resize:${Handle}` }\n | { mode: \"move\"; startPointer: { x: number; y: number }; startEdges: Edges }\n | null\n >(null);\n\n // Measure the available width so we can fit the canvas into a max box while\n // preserving the image aspect ratio (keeps the box == image aspect, which the\n // pointer coordinate mapping relies on).\n const wrapperRef = useRef<HTMLDivElement>(null);\n const [availWidth, setAvailWidth] = useState(0);\n\n useLayoutEffect(() => {\n const el = wrapperRef.current;\n if (!el) {\n return;\n }\n const measure = () => setAvailWidth(el.getBoundingClientRect().width);\n measure();\n if (typeof ResizeObserver === \"undefined\") {\n return;\n }\n const ro = new ResizeObserver(measure);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const imageRatio = (image.width || 1) / (image.height || 1);\n const surfaceStyle = useMemo<React.CSSProperties>(() => {\n if (availWidth <= 0) {\n return {\n width: \"100%\",\n aspectRatio: `${image.width || 1} / ${image.height || 1}`,\n maxHeight: `${MAX_CANVAS_HEIGHT}px`\n };\n }\n let width = Math.min(availWidth, MAX_CANVAS_WIDTH);\n let height = width / imageRatio;\n if (height > MAX_CANVAS_HEIGHT) {\n height = MAX_CANVAS_HEIGHT;\n width = height * imageRatio;\n }\n return { width: `${width}px`, height: `${height}px` };\n }, [availWidth, imageRatio, image.width, image.height]);\n\n const edges = useMemo(() => cropToEdges(crop), [crop]);\n const ratio = DEFAULT_ASPECT_RATIOS.find(ar => ar.id === aspectId)?.ratio ?? null;\n\n const aspectOptions = useMemo(\n () => [\n { label: \"Free\", value: FREE },\n ...DEFAULT_ASPECT_RATIOS.map(ar => ({ label: ar.label, value: ar.id }))\n ],\n []\n );\n\n const toNorm = useCallback((clientX: number, clientY: number) => {\n const rect = surfaceRef.current!.getBoundingClientRect();\n return {\n x: clamp01((clientX - rect.left) / rect.width),\n y: clamp01((clientY - rect.top) / rect.height)\n };\n }, []);\n\n const commitEdges = useCallback(\n (next: Edges) => {\n onChangeCrop(edgesToCrop(next));\n // Keep the focal point inside the (new) crop.\n onChangeHotspot(clampHotspot(hotspot, next));\n },\n [onChangeCrop, onChangeHotspot, hotspot]\n );\n\n const resizeFree = (handle: Handle, p: { x: number; y: number }): Edges => {\n const moves = HANDLE_EDGES[handle];\n const next = { ...edges };\n if (moves.includes(\"left\")) {\n next.left = Math.min(p.x, next.right - MIN);\n }\n if (moves.includes(\"right\")) {\n next.right = Math.max(p.x, next.left + MIN);\n }\n if (moves.includes(\"top\")) {\n next.top = Math.min(p.y, next.bottom - MIN);\n }\n if (moves.includes(\"bottom\")) {\n next.bottom = Math.max(p.y, next.top + MIN);\n }\n return next;\n };\n\n const resizeLocked = (handle: Handle, p: { x: number; y: number }, r: number): Edges => {\n const moves = HANDLE_EDGES[handle];\n // `r` is a display (pixel) aspect ratio, but the crop is stored in normalized\n // image coordinates. Convert so the on-screen crop actually has ratio `r`.\n const rNorm = r / imageRatio;\n // Anchor is the opposite corner (fixed).\n const anchorX = moves.includes(\"left\") ? edges.right : edges.left;\n const anchorY = moves.includes(\"top\") ? edges.bottom : edges.top;\n const dirX = moves.includes(\"left\") ? -1 : 1;\n const dirY = moves.includes(\"top\") ? -1 : 1;\n\n const maxW = dirX > 0 ? 1 - anchorX : anchorX;\n const maxH = dirY > 0 ? 1 - anchorY : anchorY;\n\n let w = Math.abs(p.x - anchorX);\n let h = w / rNorm;\n if (h > Math.abs(p.y - anchorY)) {\n // The vertical drag is the limiting axis.\n h = Math.abs(p.y - anchorY);\n w = h * rNorm;\n }\n // Fit inside the image while keeping the ratio.\n w = Math.min(w, maxW);\n h = w / rNorm;\n if (h > maxH) {\n h = maxH;\n w = h * rNorm;\n }\n w = Math.max(w, MIN);\n h = Math.max(h, MIN);\n\n const next = { ...edges };\n if (dirX > 0) {\n next.left = anchorX;\n next.right = anchorX + w;\n } else {\n next.right = anchorX;\n next.left = anchorX - w;\n }\n if (dirY > 0) {\n next.top = anchorY;\n next.bottom = anchorY + h;\n } else {\n next.bottom = anchorY;\n next.top = anchorY - h;\n }\n return next;\n };\n\n const moveCrop = (startEdges: Edges, dx: number, dy: number): Edges => {\n const w = startEdges.right - startEdges.left;\n const h = startEdges.bottom - startEdges.top;\n const left = Math.min(Math.max(startEdges.left + dx, 0), 1 - w);\n const top = Math.min(Math.max(startEdges.top + dy, 0), 1 - h);\n return { left, top, right: left + w, bottom: top + h };\n };\n\n const onPointerMove = useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n const drag = dragRef.current;\n if (!drag) {\n return;\n }\n const p = toNorm(e.clientX, e.clientY);\n if (drag.mode === \"focal\") {\n onChangeHotspot(clampHotspot({ x: p.x, y: p.y, width: 1, height: 1 }, edges));\n return;\n }\n if (drag.mode === \"move\") {\n const dx = p.x - drag.startPointer.x;\n const dy = p.y - drag.startPointer.y;\n commitEdges(moveCrop(drag.startEdges, dx, dy));\n return;\n }\n const handle = drag.mode.slice(\"resize:\".length) as Handle;\n commitEdges(ratio === null ? resizeFree(handle, p) : resizeLocked(handle, p, ratio));\n },\n [toNorm, onChangeHotspot, edges, commitEdges, ratio]\n );\n\n // Drag inside the crop rectangle repositions the whole crop window.\n const startMoveCrop = useCallback(\n (e: React.PointerEvent<HTMLDivElement>) => {\n dragRef.current = {\n mode: \"move\",\n startPointer: toNorm(e.clientX, e.clientY),\n startEdges: edges\n };\n surfaceRef.current?.setPointerCapture(e.pointerId);\n },\n [toNorm, edges]\n );\n\n // Dragging the marker itself moves the focal point (and only that).\n const startFocal = useCallback((e: React.PointerEvent<HTMLDivElement>) => {\n e.stopPropagation();\n dragRef.current = { mode: \"focal\" };\n surfaceRef.current?.setPointerCapture(e.pointerId);\n }, []);\n\n const startResize = useCallback(\n (handle: Handle) => (e: React.PointerEvent<HTMLDivElement>) => {\n e.stopPropagation();\n dragRef.current = { mode: `resize:${handle}` };\n surfaceRef.current?.setPointerCapture(e.pointerId);\n },\n []\n );\n\n const endDrag = useCallback((e: React.PointerEvent<HTMLDivElement>) => {\n dragRef.current = null;\n surfaceRef.current?.releasePointerCapture(e.pointerId);\n }, []);\n\n const onSelectAspect = useCallback(\n (value: string) => {\n onChangeAspect(value);\n const r = DEFAULT_ASPECT_RATIOS.find(ar => ar.id === value)?.ratio;\n if (value === FREE || !r) {\n return;\n }\n // `r` is a display (pixel) aspect ratio; convert to a normalized ratio\n // so the crop, stored in normalized image coordinates, renders as `r`.\n const rNorm = r / imageRatio;\n // Re-fit the current crop to the chosen ratio, centered within it.\n const cw = edges.right - edges.left;\n const ch = edges.bottom - edges.top;\n let w = cw;\n let h = w / rNorm;\n if (h > ch) {\n h = ch;\n w = h * rNorm;\n }\n const cx = (edges.left + edges.right) / 2;\n const cy = (edges.top + edges.bottom) / 2;\n commitEdges({\n left: clamp01(cx - w / 2),\n right: clamp01(cx + w / 2),\n top: clamp01(cy - h / 2),\n bottom: clamp01(cy + h / 2)\n });\n },\n [edges, commitEdges, imageRatio, onChangeAspect]\n );\n\n const activeHotspot = clampHotspot(hotspot, edges);\n const handles = ratio === null ? [...CORNER_HANDLES, ...EDGE_HANDLES] : CORNER_HANDLES;\n\n const handlePosition = (handle: Handle): { left: string; top: string } => {\n const cx = (edges.left + edges.right) / 2;\n const cy = (edges.top + edges.bottom) / 2;\n const x = handle.includes(\"w\") ? edges.left : handle.includes(\"e\") ? edges.right : cx;\n const y = handle.includes(\"n\") ? edges.top : handle.includes(\"s\") ? edges.bottom : cy;\n return { left: `${x * 100}%`, top: `${y * 100}%` };\n };\n\n const cursorFor = (handle: Handle): string => {\n const map: Record<Handle, string> = {\n nw: \"nwse-resize\",\n se: \"nwse-resize\",\n ne: \"nesw-resize\",\n sw: \"nesw-resize\",\n n: \"ns-resize\",\n s: \"ns-resize\",\n e: \"ew-resize\",\n w: \"ew-resize\"\n };\n return map[handle];\n };\n\n return (\n <div className={\"flex flex-col gap-sm\"}>\n <div className={\"flex items-end justify-between gap-md\"}>\n <SectionLabel>Crop & focal point</SectionLabel>\n <div className={\"w-[200px] shrink-0\"}>\n <Select\n label={\"Crop shape\"}\n value={aspectId}\n options={aspectOptions}\n onChange={onSelectAspect}\n displayResetAction={false}\n />\n </div>\n </div>\n\n {/* Dark editing stage: the image sits centered on a near-black backdrop\n (like a pro image editor), so letterboxing around portrait images\n reads as intentional and the image stays the focal element. */}\n <div className={\"flex justify-center rounded-md bg-neutral-dark p-lg\"}>\n <div ref={wrapperRef} className={\"w-full\"}>\n <div\n ref={surfaceRef}\n onPointerMove={onPointerMove}\n onPointerUp={endDrag}\n className={\n \"relative mx-auto touch-none select-none overflow-hidden rounded-sm\"\n }\n style={surfaceStyle}\n >\n <img\n src={image.src}\n alt={\"\"}\n draggable={false}\n className={\n \"pointer-events-none absolute inset-0 h-full w-full object-cover\"\n }\n />\n\n {/* Crop rectangle: dims outside via box-shadow; interior drag moves the\n crop. The border is two-tone (white edge + a 1px dark ring) so it stays\n visible over both light and dark image areas. */}\n <div\n onPointerDown={startMoveCrop}\n className={\"absolute cursor-move border border-white/90\"}\n style={{\n left: `${edges.left * 100}%`,\n top: `${edges.top * 100}%`,\n width: `${(edges.right - edges.left) * 100}%`,\n height: `${(edges.bottom - edges.top) * 100}%`,\n boxShadow:\n \"0 0 0 1px rgba(0, 0, 0, 0.55), 0 0 0 9999px rgba(0, 0, 0, 0.45)\"\n }}\n >\n {/* Rule-of-thirds guides — a compositional aid and a cue for placing\n the focal point. */}\n <div className={\"pointer-events-none absolute inset-0\"}>\n <div className={\"absolute inset-y-0 left-1/3 w-px bg-white/30\"} />\n <div className={\"absolute inset-y-0 left-2/3 w-px bg-white/30\"} />\n <div className={\"absolute inset-x-0 top-1/3 h-px bg-white/30\"} />\n <div className={\"absolute inset-x-0 top-2/3 h-px bg-white/30\"} />\n </div>\n\n {/* Focal marker — the visible ring is small, but the draggable hit\n area around it is larger for easier grabbing. */}\n <div\n onPointerDown={startFocal}\n className={\n \"absolute flex h-8 w-8 -translate-x-1/2 -translate-y-1/2 cursor-grab touch-none items-center justify-center active:cursor-grabbing\"\n }\n style={{\n left: `${((activeHotspot.x - edges.left) / Math.max(MIN, edges.right - edges.left)) * 100}%`,\n top: `${((activeHotspot.y - edges.top) / Math.max(MIN, edges.bottom - edges.top)) * 100}%`\n }}\n >\n <div\n className={\n \"pointer-events-none relative h-6 w-6 rounded-full border-2 border-white bg-white/25 shadow-lg\"\n }\n >\n <div\n className={\n \"absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white\"\n }\n />\n </div>\n </div>\n </div>\n\n {/* Resize handles — small visible dot inside a larger transparent hit\n area so the corners/edges are easy to grab. */}\n {handles.map(handle => {\n const pos = handlePosition(handle);\n return (\n <div\n key={handle}\n onPointerDown={startResize(handle)}\n className={\n \"absolute flex h-6 w-6 -translate-x-1/2 -translate-y-1/2 touch-none items-center justify-center\"\n }\n style={{\n left: pos.left,\n top: pos.top,\n cursor: cursorFor(handle)\n }}\n >\n <div\n className={\n \"pointer-events-none h-3 w-3 rounded-sm border border-neutral-dark bg-white shadow\"\n }\n />\n </div>\n );\n })}\n </div>\n </div>\n </div>\n\n <Text as={\"div\"} size={\"sm\"} className={\"text-neutral-strong\"}>\n Drag the handles to crop, drag inside to reposition, and drag the circle to set the\n focal point.\n </Text>\n </div>\n );\n};\n"],"names":["MIN","FREE","inferAspectId","crop","width","height","cropWidth","Math","cropHeight","displayRatio","match","DEFAULT_ASPECT_RATIOS","ar","MAX_CANVAS_WIDTH","MAX_CANVAS_HEIGHT","clamp01","n","cropToEdges","edgesToCrop","e","HANDLE_EDGES","CORNER_HANDLES","EDGE_HANDLES","clampHotspot","h","CropHotspotEditor","image","hotspot","aspectId","onChangeAspect","onChangeCrop","onChangeHotspot","surfaceRef","useRef","dragRef","wrapperRef","availWidth","setAvailWidth","useState","useLayoutEffect","el","measure","ResizeObserver","ro","imageRatio","surfaceStyle","useMemo","edges","ratio","aspectOptions","toNorm","useCallback","clientX","clientY","rect","commitEdges","next","resizeFree","handle","p","moves","resizeLocked","r","rNorm","anchorX","anchorY","dirX","dirY","maxW","maxH","w","moveCrop","startEdges","dx","dy","left","top","onPointerMove","drag","startMoveCrop","startFocal","startResize","endDrag","onSelectAspect","value","cw","ch","cx","cy","activeHotspot","handles","handlePosition","x","y","cursorFor","map","SectionLabel","Select","pos","Text"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { type ImageEditorAspectRatio, type ImageEditorImage, type ImageEditorValue } from "./types.js";
|
|
3
|
+
export interface ImageEditorProps {
|
|
4
|
+
open: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
/** The source image to edit. */
|
|
7
|
+
image: ImageEditorImage;
|
|
8
|
+
/** The current edit (crop/hotspot/alt/caption), if any. */
|
|
9
|
+
value?: ImageEditorValue;
|
|
10
|
+
/** Called with the new edit when the user saves. */
|
|
11
|
+
onSave: (value: ImageEditorValue) => void;
|
|
12
|
+
/** Preview ratios shown in the strip. Defaults to square / 4:3 / 16:9. */
|
|
13
|
+
aspectRatios?: ImageEditorAspectRatio[];
|
|
14
|
+
showAlt?: boolean;
|
|
15
|
+
showCaption?: boolean;
|
|
16
|
+
title?: React.ReactNode;
|
|
17
|
+
}
|
|
18
|
+
export declare const ImageEditor: ({ open, onClose, image, value, onSave, aspectRatios, showAlt, showCaption, title }: ImageEditorProps) => React.JSX.Element;
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import react, { useEffect, useState } from "react";
|
|
2
|
+
import { ReactComponent } from "@webiny/icons/arrow_drop_down.svg";
|
|
3
|
+
import { Dialog } from "../Dialog/index.js";
|
|
4
|
+
import { Button } from "../Button/index.js";
|
|
5
|
+
import { DropdownMenu } from "../DropdownMenu/index.js";
|
|
6
|
+
import { CropHotspotEditor, FREE, inferAspectId } from "./CropHotspotEditor.js";
|
|
7
|
+
import { AspectRatioPreview } from "./AspectRatioPreview.js";
|
|
8
|
+
import { MetaFields } from "./MetaFields.js";
|
|
9
|
+
import { SectionLabel } from "./SectionLabel.js";
|
|
10
|
+
import { DEFAULT_ASPECT_RATIOS, DEFAULT_PREVIEW_RATIO_IDS } from "./types.js";
|
|
11
|
+
const isFullCrop = (crop)=>{
|
|
12
|
+
if (!crop) return true;
|
|
13
|
+
return 0 === crop.top && 0 === crop.left && 0 === crop.bottom && 0 === crop.right;
|
|
14
|
+
};
|
|
15
|
+
const isCenteredHotspot = (hotspot)=>!hotspot || 0.5 === hotspot.x && 0.5 === hotspot.y;
|
|
16
|
+
const ImageEditor = ({ open, onClose, image, value, onSave, aspectRatios = DEFAULT_ASPECT_RATIOS, showAlt = true, showCaption = true, title = "Edit image" })=>{
|
|
17
|
+
const [crop, setCrop] = useState(value?.crop);
|
|
18
|
+
const [hotspot, setHotspot] = useState(value?.hotspot);
|
|
19
|
+
const [alt, setAlt] = useState(value?.alt ?? "");
|
|
20
|
+
const [caption, setCaption] = useState(value?.caption ?? "");
|
|
21
|
+
const [aspectId, setAspectId] = useState(FREE);
|
|
22
|
+
const [selectedRatioIds, setSelectedRatioIds] = useState(()=>{
|
|
23
|
+
const defaults = aspectRatios.filter((ar)=>DEFAULT_PREVIEW_RATIO_IDS.includes(ar.id)).map((ar)=>ar.id);
|
|
24
|
+
return defaults.length > 0 ? defaults : aspectRatios.map((ar)=>ar.id);
|
|
25
|
+
});
|
|
26
|
+
const toggleRatio = (id)=>setSelectedRatioIds((prev)=>prev.includes(id) ? prev.filter((x)=>x !== id) : [
|
|
27
|
+
...prev,
|
|
28
|
+
id
|
|
29
|
+
]);
|
|
30
|
+
const visibleRatios = aspectRatios.filter((ar)=>selectedRatioIds.includes(ar.id));
|
|
31
|
+
const [naturalSize, setNaturalSize] = useState(null);
|
|
32
|
+
useEffect(()=>{
|
|
33
|
+
if (!open || !image.src || image.width > 0 && image.height > 0) return;
|
|
34
|
+
if ("u" < typeof document) return;
|
|
35
|
+
const probe = document.createElement("img");
|
|
36
|
+
probe.onload = ()=>setNaturalSize({
|
|
37
|
+
width: probe.naturalWidth,
|
|
38
|
+
height: probe.naturalHeight
|
|
39
|
+
});
|
|
40
|
+
probe.src = image.src;
|
|
41
|
+
return ()=>{
|
|
42
|
+
probe.onload = null;
|
|
43
|
+
};
|
|
44
|
+
}, [
|
|
45
|
+
open,
|
|
46
|
+
image.src,
|
|
47
|
+
image.width,
|
|
48
|
+
image.height
|
|
49
|
+
]);
|
|
50
|
+
const effectiveImage = {
|
|
51
|
+
src: image.src,
|
|
52
|
+
width: image.width > 0 ? image.width : naturalSize?.width ?? 0,
|
|
53
|
+
height: image.height > 0 ? image.height : naturalSize?.height ?? 0
|
|
54
|
+
};
|
|
55
|
+
useEffect(()=>{
|
|
56
|
+
if (open) {
|
|
57
|
+
setCrop(value?.crop);
|
|
58
|
+
setHotspot(value?.hotspot);
|
|
59
|
+
setAlt(value?.alt ?? "");
|
|
60
|
+
setCaption(value?.caption ?? "");
|
|
61
|
+
}
|
|
62
|
+
}, [
|
|
63
|
+
open,
|
|
64
|
+
value
|
|
65
|
+
]);
|
|
66
|
+
useEffect(()=>{
|
|
67
|
+
if (open) setAspectId(inferAspectId(value?.crop, effectiveImage.width, effectiveImage.height));
|
|
68
|
+
}, [
|
|
69
|
+
open,
|
|
70
|
+
value,
|
|
71
|
+
effectiveImage.width,
|
|
72
|
+
effectiveImage.height
|
|
73
|
+
]);
|
|
74
|
+
const handleSave = ()=>{
|
|
75
|
+
const next = {};
|
|
76
|
+
if (!isFullCrop(crop)) next.crop = crop;
|
|
77
|
+
if (!isCenteredHotspot(hotspot)) next.hotspot = hotspot;
|
|
78
|
+
if (showAlt && alt.trim()) next.alt = alt.trim();
|
|
79
|
+
if (showCaption && caption.trim()) next.caption = caption.trim();
|
|
80
|
+
onSave(next);
|
|
81
|
+
onClose();
|
|
82
|
+
};
|
|
83
|
+
return /*#__PURE__*/ react.createElement(Dialog, {
|
|
84
|
+
open: open,
|
|
85
|
+
onClose: onClose,
|
|
86
|
+
title: title,
|
|
87
|
+
size: "lg",
|
|
88
|
+
actions: /*#__PURE__*/ react.createElement("div", {
|
|
89
|
+
className: "flex w-full items-center justify-end gap-sm"
|
|
90
|
+
}, /*#__PURE__*/ react.createElement(Button, {
|
|
91
|
+
variant: "secondary",
|
|
92
|
+
text: "Cancel",
|
|
93
|
+
onClick: onClose
|
|
94
|
+
}), /*#__PURE__*/ react.createElement(Button, {
|
|
95
|
+
variant: "primary",
|
|
96
|
+
text: "Save",
|
|
97
|
+
onClick: handleSave
|
|
98
|
+
}))
|
|
99
|
+
}, /*#__PURE__*/ react.createElement("div", {
|
|
100
|
+
className: "flex flex-col gap-lg"
|
|
101
|
+
}, /*#__PURE__*/ react.createElement(CropHotspotEditor, {
|
|
102
|
+
image: effectiveImage,
|
|
103
|
+
crop: crop,
|
|
104
|
+
hotspot: hotspot,
|
|
105
|
+
aspectId: aspectId,
|
|
106
|
+
onChangeAspect: setAspectId,
|
|
107
|
+
onChangeCrop: setCrop,
|
|
108
|
+
onChangeHotspot: setHotspot
|
|
109
|
+
}), /*#__PURE__*/ react.createElement("div", {
|
|
110
|
+
className: "flex flex-col gap-sm"
|
|
111
|
+
}, /*#__PURE__*/ react.createElement("div", {
|
|
112
|
+
className: "flex items-center justify-between"
|
|
113
|
+
}, /*#__PURE__*/ react.createElement(SectionLabel, null, "Preview"), /*#__PURE__*/ react.createElement(DropdownMenu, {
|
|
114
|
+
trigger: /*#__PURE__*/ react.createElement(Button, {
|
|
115
|
+
variant: "secondary",
|
|
116
|
+
text: `Preview at (${visibleRatios.length})`,
|
|
117
|
+
icon: /*#__PURE__*/ react.createElement(ReactComponent, null),
|
|
118
|
+
iconPosition: "end"
|
|
119
|
+
})
|
|
120
|
+
}, aspectRatios.map((ar)=>/*#__PURE__*/ react.createElement(DropdownMenu.CheckboxItem, {
|
|
121
|
+
key: ar.id,
|
|
122
|
+
text: ar.label,
|
|
123
|
+
checked: selectedRatioIds.includes(ar.id),
|
|
124
|
+
onCheckedChange: ()=>toggleRatio(ar.id),
|
|
125
|
+
onSelect: (e)=>e.preventDefault()
|
|
126
|
+
})))), visibleRatios.length > 0 ? /*#__PURE__*/ react.createElement("div", {
|
|
127
|
+
className: "flex flex-wrap items-start gap-md"
|
|
128
|
+
}, visibleRatios.map((ar)=>/*#__PURE__*/ react.createElement(AspectRatioPreview, {
|
|
129
|
+
key: ar.id,
|
|
130
|
+
src: effectiveImage.src,
|
|
131
|
+
imageWidth: effectiveImage.width,
|
|
132
|
+
imageHeight: effectiveImage.height,
|
|
133
|
+
crop: crop,
|
|
134
|
+
hotspot: hotspot,
|
|
135
|
+
aspectRatio: ar
|
|
136
|
+
}))) : /*#__PURE__*/ react.createElement("div", {
|
|
137
|
+
className: "text-sm text-neutral-strong"
|
|
138
|
+
}, "Select at least one shape to preview.")), showAlt || showCaption ? /*#__PURE__*/ react.createElement("div", {
|
|
139
|
+
className: "flex flex-col gap-sm"
|
|
140
|
+
}, /*#__PURE__*/ react.createElement(SectionLabel, null, "Details"), /*#__PURE__*/ react.createElement(MetaFields, {
|
|
141
|
+
alt: alt,
|
|
142
|
+
caption: caption,
|
|
143
|
+
showAlt: showAlt,
|
|
144
|
+
showCaption: showCaption,
|
|
145
|
+
onChangeAlt: setAlt,
|
|
146
|
+
onChangeCaption: setCaption
|
|
147
|
+
})) : null));
|
|
148
|
+
};
|
|
149
|
+
export { ImageEditor };
|
|
150
|
+
|
|
151
|
+
//# sourceMappingURL=ImageEditor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageEditor/ImageEditor.js","sources":["../../src/ImageEditor/ImageEditor.tsx"],"sourcesContent":["import React, { useEffect, useState } from \"react\";\nimport { ReactComponent as ArrowDropDownIcon } from \"@webiny/icons/arrow_drop_down.svg\";\nimport { Dialog } from \"~/Dialog/index.js\";\nimport { Button } from \"~/Button/index.js\";\nimport { DropdownMenu } from \"~/DropdownMenu/index.js\";\nimport { CropHotspotEditor, FREE, inferAspectId } from \"./CropHotspotEditor.js\";\nimport { AspectRatioPreview } from \"./AspectRatioPreview.js\";\nimport { MetaFields } from \"./MetaFields.js\";\nimport { SectionLabel } from \"./SectionLabel.js\";\nimport {\n DEFAULT_ASPECT_RATIOS,\n DEFAULT_PREVIEW_RATIO_IDS,\n type ImageEditorAspectRatio,\n type ImageEditorCrop,\n type ImageEditorHotspot,\n type ImageEditorImage,\n type ImageEditorValue\n} from \"./types.js\";\n\nexport interface ImageEditorProps {\n open: boolean;\n onClose: () => void;\n /** The source image to edit. */\n image: ImageEditorImage;\n /** The current edit (crop/hotspot/alt/caption), if any. */\n value?: ImageEditorValue;\n /** Called with the new edit when the user saves. */\n onSave: (value: ImageEditorValue) => void;\n /** Preview ratios shown in the strip. Defaults to square / 4:3 / 16:9. */\n aspectRatios?: ImageEditorAspectRatio[];\n showAlt?: boolean;\n showCaption?: boolean;\n title?: React.ReactNode;\n}\n\nconst isFullCrop = (crop: ImageEditorCrop | undefined): boolean => {\n if (!crop) {\n return true;\n }\n return crop.top === 0 && crop.left === 0 && crop.bottom === 0 && crop.right === 0;\n};\n\nconst isCenteredHotspot = (hotspot: ImageEditorHotspot | undefined): boolean => {\n return !hotspot || (hotspot.x === 0.5 && hotspot.y === 0.5);\n};\n\nexport const ImageEditor = ({\n open,\n onClose,\n image,\n value,\n onSave,\n aspectRatios = DEFAULT_ASPECT_RATIOS,\n showAlt = true,\n showCaption = true,\n title = \"Edit image\"\n}: ImageEditorProps) => {\n const [crop, setCrop] = useState<ImageEditorCrop | undefined>(value?.crop);\n const [hotspot, setHotspot] = useState<ImageEditorHotspot | undefined>(value?.hotspot);\n const [alt, setAlt] = useState(value?.alt ?? \"\");\n const [caption, setCaption] = useState(value?.caption ?? \"\");\n // Selected crop shape. Inferred from the saved crop on open so the selector\n // reflects the shape the crop was made with (rather than always \"free\").\n const [aspectId, setAspectId] = useState<string>(FREE);\n const [selectedRatioIds, setSelectedRatioIds] = useState<string[]>(() => {\n const defaults = aspectRatios\n .filter(ar => DEFAULT_PREVIEW_RATIO_IDS.includes(ar.id))\n .map(ar => ar.id);\n return defaults.length > 0 ? defaults : aspectRatios.map(ar => ar.id);\n });\n\n const toggleRatio = (id: string) =>\n setSelectedRatioIds(prev =>\n prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]\n );\n\n const visibleRatios = aspectRatios.filter(ar => selectedRatioIds.includes(ar.id));\n\n // Fall back to the image's natural dimensions when the caller didn't provide\n // them (e.g. missing metadata). Without real dimensions the canvas and previews\n // would assume a square and distort the image / mis-map the crop coordinates.\n const [naturalSize, setNaturalSize] = useState<{ width: number; height: number } | null>(null);\n\n useEffect(() => {\n if (!open || !image.src || (image.width > 0 && image.height > 0)) {\n return;\n }\n if (typeof document === \"undefined\") {\n return;\n }\n const probe = document.createElement(\"img\");\n probe.onload = () => {\n return setNaturalSize({ width: probe.naturalWidth, height: probe.naturalHeight });\n };\n probe.src = image.src;\n return () => {\n probe.onload = null;\n };\n }, [open, image.src, image.width, image.height]);\n\n const effectiveImage: ImageEditorImage = {\n src: image.src,\n width: image.width > 0 ? image.width : (naturalSize?.width ?? 0),\n height: image.height > 0 ? image.height : (naturalSize?.height ?? 0)\n };\n\n // Re-seed local state whenever the dialog is (re)opened for a given value.\n useEffect(() => {\n if (open) {\n setCrop(value?.crop);\n setHotspot(value?.hotspot);\n setAlt(value?.alt ?? \"\");\n setCaption(value?.caption ?? \"\");\n }\n }, [open, value]);\n\n // Recover the crop shape from the saved crop on open (and once the intrinsic\n // dimensions resolve). Keyed on `value`, not the live `crop`, so it never snaps\n // the selector while the user is dragging a free-form crop.\n useEffect(() => {\n if (open) {\n setAspectId(inferAspectId(value?.crop, effectiveImage.width, effectiveImage.height));\n }\n }, [open, value, effectiveImage.width, effectiveImage.height]);\n\n const handleSave = () => {\n const next: ImageEditorValue = {};\n if (!isFullCrop(crop)) {\n next.crop = crop;\n }\n if (!isCenteredHotspot(hotspot)) {\n next.hotspot = hotspot;\n }\n if (showAlt && alt.trim()) {\n next.alt = alt.trim();\n }\n if (showCaption && caption.trim()) {\n next.caption = caption.trim();\n }\n onSave(next);\n onClose();\n };\n\n return (\n <Dialog\n open={open}\n onClose={onClose}\n title={title}\n size={\"lg\"}\n actions={\n <div className={\"flex w-full items-center justify-end gap-sm\"}>\n <Button variant={\"secondary\"} text={\"Cancel\"} onClick={onClose} />\n <Button variant={\"primary\"} text={\"Save\"} onClick={handleSave} />\n </div>\n }\n >\n <div className={\"flex flex-col gap-lg\"}>\n <CropHotspotEditor\n image={effectiveImage}\n crop={crop}\n hotspot={hotspot}\n aspectId={aspectId}\n onChangeAspect={setAspectId}\n onChangeCrop={setCrop}\n onChangeHotspot={setHotspot}\n />\n\n <div className={\"flex flex-col gap-sm\"}>\n <div className={\"flex items-center justify-between\"}>\n <SectionLabel>Preview</SectionLabel>\n <DropdownMenu\n trigger={\n <Button\n variant={\"secondary\"}\n text={`Preview at (${visibleRatios.length})`}\n icon={<ArrowDropDownIcon />}\n iconPosition={\"end\"}\n />\n }\n >\n {aspectRatios.map(ar => (\n <DropdownMenu.CheckboxItem\n key={ar.id}\n text={ar.label}\n checked={selectedRatioIds.includes(ar.id)}\n onCheckedChange={() => toggleRatio(ar.id)}\n onSelect={e => e.preventDefault()}\n />\n ))}\n </DropdownMenu>\n </div>\n {visibleRatios.length > 0 ? (\n <div className={\"flex flex-wrap items-start gap-md\"}>\n {visibleRatios.map(ar => (\n <AspectRatioPreview\n key={ar.id}\n src={effectiveImage.src}\n imageWidth={effectiveImage.width}\n imageHeight={effectiveImage.height}\n crop={crop}\n hotspot={hotspot}\n aspectRatio={ar}\n />\n ))}\n </div>\n ) : (\n <div className={\"text-sm text-neutral-strong\"}>\n Select at least one shape to preview.\n </div>\n )}\n </div>\n\n {showAlt || showCaption ? (\n <div className={\"flex flex-col gap-sm\"}>\n <SectionLabel>Details</SectionLabel>\n <MetaFields\n alt={alt}\n caption={caption}\n showAlt={showAlt}\n showCaption={showCaption}\n onChangeAlt={setAlt}\n onChangeCaption={setCaption}\n />\n </div>\n ) : null}\n </div>\n </Dialog>\n );\n};\n"],"names":["isFullCrop","crop","isCenteredHotspot","hotspot","ImageEditor","open","onClose","image","value","onSave","aspectRatios","DEFAULT_ASPECT_RATIOS","showAlt","showCaption","title","setCrop","useState","setHotspot","alt","setAlt","caption","setCaption","aspectId","setAspectId","FREE","selectedRatioIds","setSelectedRatioIds","defaults","ar","DEFAULT_PREVIEW_RATIO_IDS","toggleRatio","id","prev","x","visibleRatios","naturalSize","setNaturalSize","useEffect","document","probe","effectiveImage","inferAspectId","handleSave","next","Dialog","Button","CropHotspotEditor","SectionLabel","DropdownMenu","ArrowDropDownIcon","e","AspectRatioPreview","MetaFields"],"mappings":"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"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import react, { useState } from "react";
|
|
2
|
+
import { Button } from "../Button/index.js";
|
|
3
|
+
import { ImageEditor } from "./index.js";
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/ImageEditor",
|
|
6
|
+
component: ImageEditor,
|
|
7
|
+
parameters: {
|
|
8
|
+
layout: "fullscreen"
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const ImageEditor_stories = meta;
|
|
12
|
+
const DemoImage = {
|
|
13
|
+
src: "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1600",
|
|
14
|
+
width: 1600,
|
|
15
|
+
height: 1067
|
|
16
|
+
};
|
|
17
|
+
const Demo = ()=>{
|
|
18
|
+
const [open, setOpen] = useState(false);
|
|
19
|
+
const [value, setValue] = useState(void 0);
|
|
20
|
+
return /*#__PURE__*/ react.createElement("div", {
|
|
21
|
+
className: "p-lg"
|
|
22
|
+
}, /*#__PURE__*/ react.createElement(Button, {
|
|
23
|
+
variant: "primary",
|
|
24
|
+
text: "Edit image",
|
|
25
|
+
onClick: ()=>setOpen(true)
|
|
26
|
+
}), /*#__PURE__*/ react.createElement("pre", {
|
|
27
|
+
className: "mt-md text-sm"
|
|
28
|
+
}, JSON.stringify(value, null, 2)), /*#__PURE__*/ react.createElement(ImageEditor, {
|
|
29
|
+
open: open,
|
|
30
|
+
onClose: ()=>setOpen(false),
|
|
31
|
+
image: DemoImage,
|
|
32
|
+
value: value,
|
|
33
|
+
onSave: setValue
|
|
34
|
+
}));
|
|
35
|
+
};
|
|
36
|
+
const Default = {
|
|
37
|
+
render: ()=>/*#__PURE__*/ react.createElement(Demo, null)
|
|
38
|
+
};
|
|
39
|
+
export default ImageEditor_stories;
|
|
40
|
+
export { Default };
|
|
41
|
+
|
|
42
|
+
//# sourceMappingURL=ImageEditor.stories.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageEditor/ImageEditor.stories.js","sources":["../../src/ImageEditor/ImageEditor.stories.tsx"],"sourcesContent":["import React, { useState } from \"react\";\nimport type { Meta, StoryObj } from \"@storybook/react\";\nimport { Button } from \"~/Button/index.js\";\nimport { ImageEditor, type ImageEditorValue } from \"./index.js\";\n\nconst meta: Meta<typeof ImageEditor> = {\n title: \"Components/ImageEditor\",\n component: ImageEditor,\n parameters: {\n layout: \"fullscreen\"\n }\n};\n\nexport default meta;\n\ntype Story = StoryObj<typeof ImageEditor>;\n\nconst DemoImage = {\n src: \"https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1600\",\n width: 1600,\n height: 1067\n};\n\nconst Demo = () => {\n const [open, setOpen] = useState(false);\n const [value, setValue] = useState<ImageEditorValue | undefined>(undefined);\n\n return (\n <div className={\"p-lg\"}>\n <Button variant={\"primary\"} text={\"Edit image\"} onClick={() => setOpen(true)} />\n <pre className={\"mt-md text-sm\"}>{JSON.stringify(value, null, 2)}</pre>\n <ImageEditor\n open={open}\n onClose={() => setOpen(false)}\n image={DemoImage}\n value={value}\n onSave={setValue}\n />\n </div>\n );\n};\n\nexport const Default: Story = {\n render: () => <Demo />\n};\n"],"names":["meta","ImageEditor","DemoImage","Demo","open","setOpen","useState","value","setValue","undefined","Button","JSON","Default"],"mappings":";;;AAKA,MAAMA,OAAiC;IACnC,OAAO;IACP,WAAWC;IACX,YAAY;QACR,QAAQ;IACZ;AACJ;AAEA,4BAAeD;AAIf,MAAME,YAAY;IACd,KAAK;IACL,OAAO;IACP,QAAQ;AACZ;AAEA,MAAMC,OAAO;IACT,MAAM,CAACC,MAAMC,QAAQ,GAAGC,SAAS;IACjC,MAAM,CAACC,OAAOC,SAAS,GAAGF,SAAuCG;IAEjE,OAAO,WAAP,GACI,oBAAC;QAAI,WAAW;qBACZ,oBAACC,QAAMA;QAAC,SAAS;QAAW,MAAM;QAAc,SAAS,IAAML,QAAQ;sBACvE,oBAAC;QAAI,WAAW;OAAkBM,KAAK,SAAS,CAACJ,OAAO,MAAM,mBAC9D,oBAACN,aAAWA;QACR,MAAMG;QACN,SAAS,IAAMC,QAAQ;QACvB,OAAOH;QACP,OAAOK;QACP,QAAQC;;AAIxB;AAEO,MAAMI,UAAiB;IAC1B,QAAQ,kBAAM,oBAACT,MAAIA;AACvB"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface MetaFieldsProps {
|
|
3
|
+
alt: string;
|
|
4
|
+
caption: string;
|
|
5
|
+
showAlt: boolean;
|
|
6
|
+
showCaption: boolean;
|
|
7
|
+
onChangeAlt: (value: string) => void;
|
|
8
|
+
onChangeCaption: (value: string) => void;
|
|
9
|
+
}
|
|
10
|
+
/** Alt text + optional caption, captured alongside the crop/hotspot. */
|
|
11
|
+
export declare const MetaFields: ({ alt, caption, showAlt, showCaption, onChangeAlt, onChangeCaption }: MetaFieldsProps) => React.JSX.Element | null;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import react from "react";
|
|
2
|
+
import { Input } from "../Input/index.js";
|
|
3
|
+
const MetaFields = ({ alt, caption, showAlt, showCaption, onChangeAlt, onChangeCaption })=>{
|
|
4
|
+
if (!showAlt && !showCaption) return null;
|
|
5
|
+
return /*#__PURE__*/ react.createElement("div", {
|
|
6
|
+
className: "flex flex-col gap-md"
|
|
7
|
+
}, showAlt ? /*#__PURE__*/ react.createElement(Input, {
|
|
8
|
+
label: "Alternative text",
|
|
9
|
+
description: "Describe the image for screen readers and search engines.",
|
|
10
|
+
value: alt,
|
|
11
|
+
onChange: onChangeAlt
|
|
12
|
+
}) : null, showCaption ? /*#__PURE__*/ react.createElement(Input, {
|
|
13
|
+
label: "Caption",
|
|
14
|
+
value: caption,
|
|
15
|
+
onChange: onChangeCaption
|
|
16
|
+
}) : null);
|
|
17
|
+
};
|
|
18
|
+
export { MetaFields };
|
|
19
|
+
|
|
20
|
+
//# sourceMappingURL=MetaFields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageEditor/MetaFields.js","sources":["../../src/ImageEditor/MetaFields.tsx"],"sourcesContent":["import React from \"react\";\nimport { Input } from \"~/Input/index.js\";\n\ninterface MetaFieldsProps {\n alt: string;\n caption: string;\n showAlt: boolean;\n showCaption: boolean;\n onChangeAlt: (value: string) => void;\n onChangeCaption: (value: string) => void;\n}\n\n/** Alt text + optional caption, captured alongside the crop/hotspot. */\nexport const MetaFields = ({\n alt,\n caption,\n showAlt,\n showCaption,\n onChangeAlt,\n onChangeCaption\n}: MetaFieldsProps) => {\n if (!showAlt && !showCaption) {\n return null;\n }\n\n return (\n <div className={\"flex flex-col gap-md\"}>\n {showAlt ? (\n <Input\n label={\"Alternative text\"}\n description={\"Describe the image for screen readers and search engines.\"}\n value={alt}\n onChange={onChangeAlt}\n />\n ) : null}\n {showCaption ? (\n <Input label={\"Caption\"} value={caption} onChange={onChangeCaption} />\n ) : null}\n </div>\n );\n};\n"],"names":["MetaFields","alt","caption","showAlt","showCaption","onChangeAlt","onChangeCaption","Input"],"mappings":";;AAaO,MAAMA,aAAa,CAAC,EACvBC,GAAG,EACHC,OAAO,EACPC,OAAO,EACPC,WAAW,EACXC,WAAW,EACXC,eAAe,EACD;IACd,IAAI,CAACH,WAAW,CAACC,aACb,OAAO;IAGX,OAAO,WAAP,GACI,oBAAC;QAAI,WAAW;OACXD,UAAU,WAAVA,GACG,oBAACI,OAAKA;QACF,OAAO;QACP,aAAa;QACb,OAAON;QACP,UAAUI;SAEd,MACHD,cAAc,WAAdA,GACG,oBAACG,OAAKA;QAAC,OAAO;QAAW,OAAOL;QAAS,UAAUI;SACnD;AAGhB"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Consistent section heading for the image editor. Uses the primary (darkest)
|
|
4
|
+
* text token so it reads as a heading, distinct from the muted body/caption text.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SectionLabel: ({ children }: {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}) => React.JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageEditor/SectionLabel.js","sources":["../../src/ImageEditor/SectionLabel.tsx"],"sourcesContent":["import React from \"react\";\n\n/**\n * Consistent section heading for the image editor. Uses the primary (darkest)\n * text token so it reads as a heading, distinct from the muted body/caption text.\n */\nexport const SectionLabel = ({ children }: { children: React.ReactNode }) => {\n return <div className={\"text-sm font-semibold text-neutral-primary\"}>{children}</div>;\n};\n"],"names":["SectionLabel","children"],"mappings":";AAMO,MAAMA,eAAe,CAAC,EAAEC,QAAQ,EAAiC,GAC7D,WAAP,GAAO,oBAAC;QAAI,WAAW;OAA+CA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview geometry for the image editor.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT: this is a deliberate, minimal mirror of the canonical geometry core
|
|
5
|
+
* in `@webiny/website-builder-sdk` (`src/image/geometry.ts`). The SDK copy is the
|
|
6
|
+
* source of truth used for live frontend rendering; this copy exists only so the
|
|
7
|
+
* design-system layer stays free of an SDK dependency. Keep the two in sync — the
|
|
8
|
+
* whole point is that the editor preview matches what the site renders.
|
|
9
|
+
*/
|
|
10
|
+
import type { ImageEditorCrop, ImageEditorHotspot } from "./types.js";
|
|
11
|
+
export interface PreviewStyles {
|
|
12
|
+
container: React.CSSProperties;
|
|
13
|
+
image: React.CSSProperties;
|
|
14
|
+
}
|
|
15
|
+
interface Rect {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The largest rectangle of `ratio` (w/h) that fits inside the crop, centered on
|
|
23
|
+
* the hotspot and clamped to the crop bounds.
|
|
24
|
+
*/
|
|
25
|
+
export declare const getVisibleRect: (imageWidth: number, imageHeight: number, crop: ImageEditorCrop | undefined, hotspot: ImageEditorHotspot | undefined, ratio: number) => Rect;
|
|
26
|
+
/**
|
|
27
|
+
* CSS for an overflow-clipped container + absolutely positioned image that shows
|
|
28
|
+
* exactly the visible rect for a given aspect ratio.
|
|
29
|
+
*/
|
|
30
|
+
export declare const getPreviewStyles: (imageWidth: number, imageHeight: number, crop: ImageEditorCrop | undefined, hotspot: ImageEditorHotspot | undefined, ratio: number) => PreviewStyles;
|
|
31
|
+
export interface CroppedImageRenderStyles {
|
|
32
|
+
wrapper: React.CSSProperties;
|
|
33
|
+
image: React.CSSProperties;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Styles to render an already-cropped image inside a measured box, honoring the
|
|
37
|
+
* crop (and, for `cover`, the hotspot). Use when you know the box's pixel size:
|
|
38
|
+
* - `cover`: fills the box, cover-cropping the crop region toward the hotspot;
|
|
39
|
+
* - `contain`: fits the whole crop region inside the box (centered by the parent).
|
|
40
|
+
*/
|
|
41
|
+
export declare const getCroppedImageRenderStyles: (imageWidth: number, imageHeight: number, crop: ImageEditorCrop | undefined, hotspot: ImageEditorHotspot | undefined, opts: {
|
|
42
|
+
boxWidth: number;
|
|
43
|
+
boxHeight: number;
|
|
44
|
+
fit: "cover" | "contain";
|
|
45
|
+
}) => CroppedImageRenderStyles;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const clamp = (value, min = 0, max = 1)=>{
|
|
2
|
+
if (Number.isNaN(value)) return min;
|
|
3
|
+
return Math.min(max, Math.max(min, value));
|
|
4
|
+
};
|
|
5
|
+
const round = (n)=>Math.round(1000 * n) / 1000;
|
|
6
|
+
const getCropRect = (crop)=>{
|
|
7
|
+
const top = clamp(crop?.top ?? 0);
|
|
8
|
+
const left = clamp(crop?.left ?? 0);
|
|
9
|
+
const bottom = clamp(crop?.bottom ?? 0);
|
|
10
|
+
const right = clamp(crop?.right ?? 0);
|
|
11
|
+
return {
|
|
12
|
+
x: left,
|
|
13
|
+
y: top,
|
|
14
|
+
width: Math.max(0, 1 - left - right),
|
|
15
|
+
height: Math.max(0, 1 - top - bottom)
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
const getVisibleRect = (imageWidth, imageHeight, crop, hotspot, ratio)=>{
|
|
19
|
+
const cropRect = getCropRect(crop);
|
|
20
|
+
if (cropRect.width <= 0 || cropRect.height <= 0) return cropRect;
|
|
21
|
+
const iw = imageWidth > 0 ? imageWidth : 1;
|
|
22
|
+
const ih = imageHeight > 0 ? imageHeight : 1;
|
|
23
|
+
const targetAR = ratio > 0 ? ratio : 1;
|
|
24
|
+
const cropWpx = cropRect.width * iw;
|
|
25
|
+
const cropHpx = cropRect.height * ih;
|
|
26
|
+
const cropAR = cropWpx / cropHpx;
|
|
27
|
+
let finalWpx;
|
|
28
|
+
let finalHpx;
|
|
29
|
+
if (targetAR > cropAR) {
|
|
30
|
+
finalWpx = cropWpx;
|
|
31
|
+
finalHpx = cropWpx / targetAR;
|
|
32
|
+
} else {
|
|
33
|
+
finalHpx = cropHpx;
|
|
34
|
+
finalWpx = cropHpx * targetAR;
|
|
35
|
+
}
|
|
36
|
+
const finalW = finalWpx / iw;
|
|
37
|
+
const finalH = finalHpx / ih;
|
|
38
|
+
const hx = clamp(hotspot?.x ?? 0.5);
|
|
39
|
+
const hy = clamp(hotspot?.y ?? 0.5);
|
|
40
|
+
const maxX = cropRect.x + cropRect.width - finalW;
|
|
41
|
+
const maxY = cropRect.y + cropRect.height - finalH;
|
|
42
|
+
const x = clamp(hx - finalW / 2, cropRect.x, Math.max(cropRect.x, maxX));
|
|
43
|
+
const y = clamp(hy - finalH / 2, cropRect.y, Math.max(cropRect.y, maxY));
|
|
44
|
+
return {
|
|
45
|
+
x,
|
|
46
|
+
y,
|
|
47
|
+
width: finalW,
|
|
48
|
+
height: finalH
|
|
49
|
+
};
|
|
50
|
+
};
|
|
51
|
+
const getPreviewStyles = (imageWidth, imageHeight, crop, hotspot, ratio)=>{
|
|
52
|
+
const rect = getVisibleRect(imageWidth, imageHeight, crop, hotspot, ratio);
|
|
53
|
+
const safeW = rect.width > 0 ? rect.width : 1;
|
|
54
|
+
const safeH = rect.height > 0 ? rect.height : 1;
|
|
55
|
+
return {
|
|
56
|
+
container: {
|
|
57
|
+
position: "relative",
|
|
58
|
+
width: "100%",
|
|
59
|
+
aspectRatio: `${round(ratio > 0 ? ratio : 1)}`,
|
|
60
|
+
overflow: "hidden"
|
|
61
|
+
},
|
|
62
|
+
image: {
|
|
63
|
+
position: "absolute",
|
|
64
|
+
width: `${round(100 / safeW)}%`,
|
|
65
|
+
height: `${round(100 / safeH)}%`,
|
|
66
|
+
left: `${round(100 * -(rect.x / safeW))}%`,
|
|
67
|
+
top: `${round(100 * -(rect.y / safeH))}%`,
|
|
68
|
+
maxWidth: "none",
|
|
69
|
+
objectFit: "cover",
|
|
70
|
+
objectPosition: `${round(100 * clamp(hotspot?.x ?? 0.5))}% ${round(100 * clamp(hotspot?.y ?? 0.5))}%`
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
const getCroppedImageRenderStyles = (imageWidth, imageHeight, crop, hotspot, opts)=>{
|
|
75
|
+
const imageStyle = (rect)=>{
|
|
76
|
+
const safeW = rect.width > 0 ? rect.width : 1;
|
|
77
|
+
const safeH = rect.height > 0 ? rect.height : 1;
|
|
78
|
+
return {
|
|
79
|
+
position: "absolute",
|
|
80
|
+
width: `${round(100 / safeW)}%`,
|
|
81
|
+
height: `${round(100 / safeH)}%`,
|
|
82
|
+
left: `${round(100 * -(rect.x / safeW))}%`,
|
|
83
|
+
top: `${round(100 * -(rect.y / safeH))}%`,
|
|
84
|
+
maxWidth: "none",
|
|
85
|
+
objectFit: "cover",
|
|
86
|
+
objectPosition: `${round(100 * clamp(hotspot?.x ?? 0.5))}% ${round(100 * clamp(hotspot?.y ?? 0.5))}%`
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
if ("cover" === opts.fit) {
|
|
90
|
+
const targetAspect = opts.boxHeight > 0 ? opts.boxWidth / opts.boxHeight : 1;
|
|
91
|
+
const rect = getVisibleRect(imageWidth, imageHeight, crop, hotspot, targetAspect);
|
|
92
|
+
return {
|
|
93
|
+
wrapper: {
|
|
94
|
+
position: "relative",
|
|
95
|
+
width: "100%",
|
|
96
|
+
height: "100%",
|
|
97
|
+
overflow: "hidden"
|
|
98
|
+
},
|
|
99
|
+
image: imageStyle(rect)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const cropRect = getCropRect(crop);
|
|
103
|
+
const iw = imageWidth > 0 ? imageWidth : 1;
|
|
104
|
+
const ih = imageHeight > 0 ? imageHeight : 1;
|
|
105
|
+
const cropWpx = (cropRect.width > 0 ? cropRect.width : 1) * iw;
|
|
106
|
+
const cropHpx = (cropRect.height > 0 ? cropRect.height : 1) * ih;
|
|
107
|
+
const cropAspect = cropWpx / cropHpx;
|
|
108
|
+
let width = opts.boxWidth;
|
|
109
|
+
let height = opts.boxWidth / cropAspect;
|
|
110
|
+
if (height > opts.boxHeight) {
|
|
111
|
+
height = opts.boxHeight;
|
|
112
|
+
width = opts.boxHeight * cropAspect;
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
wrapper: {
|
|
116
|
+
position: "relative",
|
|
117
|
+
width: `${round(width)}px`,
|
|
118
|
+
height: `${round(height)}px`,
|
|
119
|
+
overflow: "hidden"
|
|
120
|
+
},
|
|
121
|
+
image: imageStyle(cropRect)
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
export { getCroppedImageRenderStyles, getPreviewStyles, getVisibleRect };
|
|
125
|
+
|
|
126
|
+
//# sourceMappingURL=geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ImageEditor/geometry.js","sources":["../../src/ImageEditor/geometry.ts"],"sourcesContent":["/**\n * Preview geometry for the image editor.\n *\n * IMPORTANT: this is a deliberate, minimal mirror of the canonical geometry core\n * in `@webiny/website-builder-sdk` (`src/image/geometry.ts`). The SDK copy is the\n * source of truth used for live frontend rendering; this copy exists only so the\n * design-system layer stays free of an SDK dependency. Keep the two in sync — the\n * whole point is that the editor preview matches what the site renders.\n */\nimport type { ImageEditorCrop, ImageEditorHotspot } from \"./types.js\";\n\nexport interface PreviewStyles {\n container: React.CSSProperties;\n image: React.CSSProperties;\n}\n\nconst clamp = (value: number, min = 0, max = 1): number => {\n if (Number.isNaN(value)) {\n return min;\n }\n return Math.min(max, Math.max(min, value));\n};\n\nconst round = (n: number): number => Math.round(n * 1000) / 1000;\n\ninterface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nconst getCropRect = (crop?: ImageEditorCrop | null): Rect => {\n const top = clamp(crop?.top ?? 0);\n const left = clamp(crop?.left ?? 0);\n const bottom = clamp(crop?.bottom ?? 0);\n const right = clamp(crop?.right ?? 0);\n return {\n x: left,\n y: top,\n width: Math.max(0, 1 - left - right),\n height: Math.max(0, 1 - top - bottom)\n };\n};\n\n/**\n * The largest rectangle of `ratio` (w/h) that fits inside the crop, centered on\n * the hotspot and clamped to the crop bounds.\n */\nexport const getVisibleRect = (\n imageWidth: number,\n imageHeight: number,\n crop: ImageEditorCrop | undefined,\n hotspot: ImageEditorHotspot | undefined,\n ratio: number\n): Rect => {\n const cropRect = getCropRect(crop);\n if (cropRect.width <= 0 || cropRect.height <= 0) {\n return cropRect;\n }\n\n const iw = imageWidth > 0 ? imageWidth : 1;\n const ih = imageHeight > 0 ? imageHeight : 1;\n const targetAR = ratio > 0 ? ratio : 1;\n\n const cropWpx = cropRect.width * iw;\n const cropHpx = cropRect.height * ih;\n const cropAR = cropWpx / cropHpx;\n\n let finalWpx: number;\n let finalHpx: number;\n if (targetAR > cropAR) {\n finalWpx = cropWpx;\n finalHpx = cropWpx / targetAR;\n } else {\n finalHpx = cropHpx;\n finalWpx = cropHpx * targetAR;\n }\n\n const finalW = finalWpx / iw;\n const finalH = finalHpx / ih;\n\n const hx = clamp(hotspot?.x ?? 0.5);\n const hy = clamp(hotspot?.y ?? 0.5);\n const maxX = cropRect.x + cropRect.width - finalW;\n const maxY = cropRect.y + cropRect.height - finalH;\n const x = clamp(hx - finalW / 2, cropRect.x, Math.max(cropRect.x, maxX));\n const y = clamp(hy - finalH / 2, cropRect.y, Math.max(cropRect.y, maxY));\n\n return { x, y, width: finalW, height: finalH };\n};\n\n/**\n * CSS for an overflow-clipped container + absolutely positioned image that shows\n * exactly the visible rect for a given aspect ratio.\n */\nexport const getPreviewStyles = (\n imageWidth: number,\n imageHeight: number,\n crop: ImageEditorCrop | undefined,\n hotspot: ImageEditorHotspot | undefined,\n ratio: number\n): PreviewStyles => {\n const rect = getVisibleRect(imageWidth, imageHeight, crop, hotspot, ratio);\n const safeW = rect.width > 0 ? rect.width : 1;\n const safeH = rect.height > 0 ? rect.height : 1;\n\n return {\n container: {\n position: \"relative\",\n width: \"100%\",\n aspectRatio: `${round(ratio > 0 ? ratio : 1)}`,\n overflow: \"hidden\"\n },\n image: {\n position: \"absolute\",\n width: `${round(100 / safeW)}%`,\n height: `${round(100 / safeH)}%`,\n left: `${round(-(rect.x / safeW) * 100)}%`,\n top: `${round(-(rect.y / safeH) * 100)}%`,\n maxWidth: \"none\",\n // Safety net: when the intrinsic dimensions match the image, the box\n // already has the image's aspect ratio so `cover` is a no-op. If the\n // dimensions are wrong/missing, this crops toward the focal point\n // instead of hard-stretching the image.\n objectFit: \"cover\",\n objectPosition: `${round(clamp(hotspot?.x ?? 0.5) * 100)}% ${round(clamp(hotspot?.y ?? 0.5) * 100)}%`\n }\n };\n};\n\nexport interface CroppedImageRenderStyles {\n wrapper: React.CSSProperties;\n image: React.CSSProperties;\n}\n\n/**\n * Styles to render an already-cropped image inside a measured box, honoring the\n * crop (and, for `cover`, the hotspot). Use when you know the box's pixel size:\n * - `cover`: fills the box, cover-cropping the crop region toward the hotspot;\n * - `contain`: fits the whole crop region inside the box (centered by the parent).\n */\nexport const getCroppedImageRenderStyles = (\n imageWidth: number,\n imageHeight: number,\n crop: ImageEditorCrop | undefined,\n hotspot: ImageEditorHotspot | undefined,\n opts: { boxWidth: number; boxHeight: number; fit: \"cover\" | \"contain\" }\n): CroppedImageRenderStyles => {\n const imageStyle = (rect: {\n x: number;\n y: number;\n width: number;\n height: number;\n }): React.CSSProperties => {\n const safeW = rect.width > 0 ? rect.width : 1;\n const safeH = rect.height > 0 ? rect.height : 1;\n return {\n position: \"absolute\",\n width: `${round(100 / safeW)}%`,\n height: `${round(100 / safeH)}%`,\n left: `${round(-(rect.x / safeW) * 100)}%`,\n top: `${round(-(rect.y / safeH) * 100)}%`,\n maxWidth: \"none\",\n // See getPreviewStyles: no-op when dimensions are correct, graceful\n // crop toward the focal point when they aren't (instead of stretching).\n objectFit: \"cover\",\n objectPosition: `${round(clamp(hotspot?.x ?? 0.5) * 100)}% ${round(clamp(hotspot?.y ?? 0.5) * 100)}%`\n };\n };\n\n if (opts.fit === \"cover\") {\n const targetAspect = opts.boxHeight > 0 ? opts.boxWidth / opts.boxHeight : 1;\n const rect = getVisibleRect(imageWidth, imageHeight, crop, hotspot, targetAspect);\n return {\n wrapper: { position: \"relative\", width: \"100%\", height: \"100%\", overflow: \"hidden\" },\n image: imageStyle(rect)\n };\n }\n\n // contain: fit the whole crop region within the box, preserving its aspect.\n const cropRect = getCropRect(crop);\n const iw = imageWidth > 0 ? imageWidth : 1;\n const ih = imageHeight > 0 ? imageHeight : 1;\n const cropWpx = (cropRect.width > 0 ? cropRect.width : 1) * iw;\n const cropHpx = (cropRect.height > 0 ? cropRect.height : 1) * ih;\n const cropAspect = cropWpx / cropHpx;\n\n let width = opts.boxWidth;\n let height = opts.boxWidth / cropAspect;\n if (height > opts.boxHeight) {\n height = opts.boxHeight;\n width = opts.boxHeight * cropAspect;\n }\n\n return {\n wrapper: {\n position: \"relative\",\n width: `${round(width)}px`,\n height: `${round(height)}px`,\n overflow: \"hidden\"\n },\n image: imageStyle(cropRect)\n };\n};\n"],"names":["clamp","value","min","max","Number","Math","round","n","getCropRect","crop","top","left","bottom","right","getVisibleRect","imageWidth","imageHeight","hotspot","ratio","cropRect","iw","ih","targetAR","cropWpx","cropHpx","cropAR","finalWpx","finalHpx","finalW","finalH","hx","hy","maxX","maxY","x","y","getPreviewStyles","rect","safeW","safeH","getCroppedImageRenderStyles","opts","imageStyle","targetAspect","cropAspect","width","height"],"mappings":"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"}
|