@olenbetong/synergi-react 2.3.8 → 2.4.0
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/es/components/FilePond.d.ts +1 -1
- package/es/components/FilePond.js +1 -1
- package/es/components/ImageEditor/CropCanvas.d.ts +16 -0
- package/es/components/ImageEditor/CropCanvas.js +79 -0
- package/es/components/ImageEditor/DragHandle.d.ts +14 -0
- package/es/components/ImageEditor/DragHandle.js +40 -0
- package/es/components/ImageEditor/EdgeHandle.d.ts +16 -0
- package/es/components/ImageEditor/EdgeHandle.js +43 -0
- package/es/components/ImageEditor/ImageEditor.css +224 -0
- package/es/components/ImageEditor/ImageEditor.d.ts +24 -0
- package/es/components/ImageEditor/ImageEditor.js +108 -0
- package/es/components/ImageEditor/ResponsiveButton.d.ts +11 -0
- package/es/components/ImageEditor/ResponsiveButton.js +9 -0
- package/es/components/ImageEditor/defaultPlugins.d.ts +4 -0
- package/es/components/ImageEditor/defaultPlugins.js +8 -0
- package/es/components/ImageEditor/index.d.ts +6 -0
- package/es/components/ImageEditor/index.js +4 -0
- package/es/components/ImageEditor/plugins/cropPlugin/CropContext.d.ts +10 -0
- package/es/components/ImageEditor/plugins/cropPlugin/CropContext.js +14 -0
- package/es/components/ImageEditor/plugins/cropPlugin/CropOverlay.d.ts +2 -0
- package/es/components/ImageEditor/plugins/cropPlugin/CropOverlay.js +30 -0
- package/es/components/ImageEditor/plugins/cropPlugin/CropToolbar.d.ts +2 -0
- package/es/components/ImageEditor/plugins/cropPlugin/CropToolbar.js +16 -0
- package/es/components/ImageEditor/plugins/cropPlugin/index.d.ts +6 -0
- package/es/components/ImageEditor/plugins/cropPlugin/index.js +16 -0
- package/es/components/ImageEditor/plugins/cropPlugin/useCropTool.d.ts +34 -0
- package/es/components/ImageEditor/plugins/cropPlugin/useCropTool.js +138 -0
- package/es/components/ImageEditor/plugins/redactPlugin/RedactContext.d.ts +10 -0
- package/es/components/ImageEditor/plugins/redactPlugin/RedactContext.js +14 -0
- package/es/components/ImageEditor/plugins/redactPlugin/RedactOverlay.d.ts +6 -0
- package/es/components/ImageEditor/plugins/redactPlugin/RedactOverlay.js +29 -0
- package/es/components/ImageEditor/plugins/redactPlugin/RedactToolbar.d.ts +2 -0
- package/es/components/ImageEditor/plugins/redactPlugin/RedactToolbar.js +29 -0
- package/es/components/ImageEditor/plugins/redactPlugin/index.d.ts +6 -0
- package/es/components/ImageEditor/plugins/redactPlugin/index.js +16 -0
- package/es/components/ImageEditor/plugins/redactPlugin/useRedactTool.d.ts +49 -0
- package/es/components/ImageEditor/plugins/redactPlugin/useRedactTool.js +142 -0
- package/es/components/ImageEditor/plugins/rotatePlugin.d.ts +3 -0
- package/es/components/ImageEditor/plugins/rotatePlugin.js +39 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/ZoomCanvas.d.ts +20 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/ZoomCanvas.js +91 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/ZoomContext.d.ts +6 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/ZoomContext.js +14 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/ZoomControls.d.ts +6 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/ZoomControls.js +11 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/index.d.ts +2 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/index.js +10 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/useZoomPan.d.ts +33 -0
- package/es/components/ImageEditor/plugins/zoomPlugin/useZoomPan.js +80 -0
- package/es/components/ImageEditor/types.d.ts +69 -0
- package/es/components/ImageEditor/types.js +1 -0
- package/es/components/ImageEditor/useActiveTool.d.ts +9 -0
- package/es/components/ImageEditor/useActiveTool.js +11 -0
- package/es/components/ImageEditor/useEdgeDetection.d.ts +18 -0
- package/es/components/ImageEditor/useEdgeDetection.js +125 -0
- package/es/components/ImageEditor/useImageEditor.d.ts +12 -0
- package/es/components/ImageEditor/useImageEditor.js +28 -0
- package/es/components/ImageEditor/useImageHistory.d.ts +21 -0
- package/es/components/ImageEditor/useImageHistory.js +153 -0
- package/es/components/ImageEditor/useOpenCV.d.ts +14 -0
- package/es/components/ImageEditor/useOpenCV.js +92 -0
- package/es/components/MergeProgressDialog/index.d.ts +8 -0
- package/es/components/MergeProgressDialog/index.js +136 -0
- package/es/components/PdfViewer/PdfViewer.css +12 -0
- package/es/components/PdfViewer/PdfViewer.d.ts +7 -0
- package/es/components/PdfViewer/PdfViewer.js +108 -0
- package/es/components/PdfViewer/index.d.ts +1 -0
- package/es/components/PdfViewer/index.js +1 -0
- package/es/index.d.ts +3 -0
- package/es/index.js +3 -0
- package/package.json +8 -3
- package/src/components/FilePond.tsx +1 -1
- package/src/components/ImageEditor/CropCanvas.tsx +100 -0
- package/src/components/ImageEditor/DragHandle.tsx +65 -0
- package/src/components/ImageEditor/EdgeHandle.tsx +65 -0
- package/src/components/ImageEditor/ImageEditor.css +224 -0
- package/src/components/ImageEditor/ImageEditor.tsx +310 -0
- package/src/components/ImageEditor/ResponsiveButton.tsx +32 -0
- package/src/components/ImageEditor/defaultPlugins.ts +10 -0
- package/src/components/ImageEditor/index.ts +6 -0
- package/src/components/ImageEditor/plugins/cropPlugin/CropContext.tsx +24 -0
- package/src/components/ImageEditor/plugins/cropPlugin/CropOverlay.tsx +119 -0
- package/src/components/ImageEditor/plugins/cropPlugin/CropToolbar.tsx +36 -0
- package/src/components/ImageEditor/plugins/cropPlugin/index.ts +20 -0
- package/src/components/ImageEditor/plugins/cropPlugin/useCropTool.ts +176 -0
- package/src/components/ImageEditor/plugins/redactPlugin/RedactContext.tsx +23 -0
- package/src/components/ImageEditor/plugins/redactPlugin/RedactOverlay.tsx +144 -0
- package/src/components/ImageEditor/plugins/redactPlugin/RedactToolbar.tsx +56 -0
- package/src/components/ImageEditor/plugins/redactPlugin/index.ts +20 -0
- package/src/components/ImageEditor/plugins/redactPlugin/useRedactTool.ts +197 -0
- package/src/components/ImageEditor/plugins/rotatePlugin.ts +43 -0
- package/src/components/ImageEditor/plugins/zoomPlugin/ZoomCanvas.tsx +123 -0
- package/src/components/ImageEditor/plugins/zoomPlugin/ZoomContext.tsx +15 -0
- package/src/components/ImageEditor/plugins/zoomPlugin/ZoomControls.tsx +43 -0
- package/src/components/ImageEditor/plugins/zoomPlugin/index.ts +12 -0
- package/src/components/ImageEditor/plugins/zoomPlugin/useZoomPan.ts +126 -0
- package/src/components/ImageEditor/types.ts +69 -0
- package/src/components/ImageEditor/useActiveTool.ts +21 -0
- package/src/components/ImageEditor/useEdgeDetection.ts +167 -0
- package/src/components/ImageEditor/useImageEditor.ts +39 -0
- package/src/components/ImageEditor/useImageHistory.ts +189 -0
- package/src/components/ImageEditor/useOpenCV.ts +134 -0
- package/src/components/MergeProgressDialog/index.tsx +249 -0
- package/src/components/PdfViewer/PdfViewer.css +12 -0
- package/src/components/PdfViewer/PdfViewer.tsx +139 -0
- package/src/components/PdfViewer/index.ts +1 -0
- package/src/index.ts +3 -0
|
@@ -4,7 +4,7 @@ import "filepond/dist/filepond.css";
|
|
|
4
4
|
import type { DataObject } from "@olenbetong/appframe-data";
|
|
5
5
|
import { type FilePondErrorDescription, type FilePondFile, type FilePondOptions, FileStatus, registerPlugin } from "filepond";
|
|
6
6
|
import { type RefObject } from "react";
|
|
7
|
-
export {
|
|
7
|
+
export { FileStatus, registerPlugin };
|
|
8
8
|
type FilePondProps<T extends Record<string, unknown>> = React.HTMLProps<HTMLInputElement> & {
|
|
9
9
|
dataObject: DataObject<T>;
|
|
10
10
|
fields: Partial<T>;
|
|
@@ -10,7 +10,7 @@ import FilePondPluginImagePreview from "filepond-plugin-image-preview";
|
|
|
10
10
|
import FilePondPluginImageResize from "filepond-plugin-image-resize";
|
|
11
11
|
import FilePondPluginImageTransform from "filepond-plugin-image-transform";
|
|
12
12
|
import { useEffect, useMemo, useRef } from "react";
|
|
13
|
-
export {
|
|
13
|
+
export { FileStatus, registerPlugin };
|
|
14
14
|
registerPlugin(FilePondPluginImageExifOrientation, FilePondPluginImageResize, FilePondPluginImagePreview, FilePondPluginImageTransform);
|
|
15
15
|
let isMostLikelyMobile = navigator.userAgent.match(/Android/i) ||
|
|
16
16
|
navigator.userAgent.match(/iPhone/i) ||
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Quad } from "./useEdgeDetection.js";
|
|
2
|
+
export interface CropCanvasHandle {
|
|
3
|
+
/** Returns a canvas with the clean image (no overlay) for use with cv.imread. */
|
|
4
|
+
getCleanCanvas(): HTMLCanvasElement | null;
|
|
5
|
+
}
|
|
6
|
+
interface Props {
|
|
7
|
+
imageUrl: string;
|
|
8
|
+
corners: Quad;
|
|
9
|
+
onReady?: (canvas: HTMLCanvasElement, width: number, height: number) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Renders the image on a canvas with a perspective-crop overlay.
|
|
13
|
+
* Exposes `getImageData()` so callers can extract pixels for off-thread processing.
|
|
14
|
+
*/
|
|
15
|
+
export declare let CropCanvas: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<CropCanvasHandle>>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
|
|
3
|
+
const MAX_DISPLAY_WIDTH = 1600;
|
|
4
|
+
function drawOverlay(ctx, width, height, corners) {
|
|
5
|
+
let [tl, tr, br, bl] = corners;
|
|
6
|
+
ctx.save();
|
|
7
|
+
// Dark overlay with polygon cutout (even-odd fill rule creates a "window")
|
|
8
|
+
ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
|
|
9
|
+
ctx.beginPath();
|
|
10
|
+
ctx.rect(0, 0, width, height);
|
|
11
|
+
ctx.moveTo(tl.x, tl.y);
|
|
12
|
+
ctx.lineTo(tr.x, tr.y);
|
|
13
|
+
ctx.lineTo(br.x, br.y);
|
|
14
|
+
ctx.lineTo(bl.x, bl.y);
|
|
15
|
+
ctx.closePath();
|
|
16
|
+
ctx.fill("evenodd");
|
|
17
|
+
// Selection border
|
|
18
|
+
ctx.strokeStyle = "#2196f3";
|
|
19
|
+
ctx.lineWidth = Math.max(2, width * 0.003);
|
|
20
|
+
ctx.beginPath();
|
|
21
|
+
ctx.moveTo(tl.x, tl.y);
|
|
22
|
+
ctx.lineTo(tr.x, tr.y);
|
|
23
|
+
ctx.lineTo(br.x, br.y);
|
|
24
|
+
ctx.lineTo(bl.x, bl.y);
|
|
25
|
+
ctx.closePath();
|
|
26
|
+
ctx.stroke();
|
|
27
|
+
ctx.restore();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Renders the image on a canvas with a perspective-crop overlay.
|
|
31
|
+
* Exposes `getImageData()` so callers can extract pixels for off-thread processing.
|
|
32
|
+
*/
|
|
33
|
+
export let CropCanvas = forwardRef(function CropCanvas({ imageUrl, corners, onReady }, ref) {
|
|
34
|
+
let canvasRef = useRef(null);
|
|
35
|
+
let imageRef = useRef(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
let img = new Image();
|
|
38
|
+
img.onload = () => {
|
|
39
|
+
imageRef.current = img;
|
|
40
|
+
let canvas = canvasRef.current;
|
|
41
|
+
if (!canvas)
|
|
42
|
+
return;
|
|
43
|
+
let scale = Math.min(1, MAX_DISPLAY_WIDTH / img.naturalWidth);
|
|
44
|
+
canvas.width = Math.round(img.naturalWidth * scale);
|
|
45
|
+
canvas.height = Math.round(img.naturalHeight * scale);
|
|
46
|
+
let ctx = canvas.getContext("2d");
|
|
47
|
+
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
48
|
+
drawOverlay(ctx, canvas.width, canvas.height, corners);
|
|
49
|
+
onReady?.(canvas, canvas.width, canvas.height);
|
|
50
|
+
};
|
|
51
|
+
img.src = imageUrl;
|
|
52
|
+
}, [imageUrl]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
53
|
+
// Redraw overlay whenever corners change
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let canvas = canvasRef.current;
|
|
56
|
+
let img = imageRef.current;
|
|
57
|
+
if (!canvas || !img)
|
|
58
|
+
return;
|
|
59
|
+
let ctx = canvas.getContext("2d");
|
|
60
|
+
if (!ctx)
|
|
61
|
+
return;
|
|
62
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
63
|
+
drawOverlay(ctx, canvas.width, canvas.height, corners);
|
|
64
|
+
}, [corners]);
|
|
65
|
+
useImperativeHandle(ref, () => ({
|
|
66
|
+
getCleanCanvas() {
|
|
67
|
+
let canvas = canvasRef.current;
|
|
68
|
+
let img = imageRef.current;
|
|
69
|
+
if (!canvas || !img)
|
|
70
|
+
return null;
|
|
71
|
+
let clean = document.createElement("canvas");
|
|
72
|
+
clean.width = canvas.width;
|
|
73
|
+
clean.height = canvas.height;
|
|
74
|
+
clean.getContext("2d")?.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
75
|
+
return clean;
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
return _jsx("canvas", { ref: canvasRef, style: { display: "block", maxWidth: "100%", height: "auto" } });
|
|
79
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
/** Called with new position in pixels (relative to container). */
|
|
5
|
+
onChange: (x: number, y: number) => void;
|
|
6
|
+
containerWidth: number;
|
|
7
|
+
containerHeight: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Absolutely-positioned draggable corner handle.
|
|
11
|
+
* Position is in CSS pixels relative to the nearest `position: relative` ancestor.
|
|
12
|
+
*/
|
|
13
|
+
export declare function DragHandle({ x, y, onChange, containerWidth, containerHeight }: Props): import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useZoom } from "./plugins/zoomPlugin/ZoomContext.js";
|
|
3
|
+
/**
|
|
4
|
+
* Absolutely-positioned draggable corner handle.
|
|
5
|
+
* Position is in CSS pixels relative to the nearest `position: relative` ancestor.
|
|
6
|
+
*/
|
|
7
|
+
export function DragHandle({ x, y, onChange, containerWidth, containerHeight }) {
|
|
8
|
+
let { zoom } = useZoom();
|
|
9
|
+
function onPointerDown(e) {
|
|
10
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
11
|
+
}
|
|
12
|
+
function onPointerMove(e) {
|
|
13
|
+
if (!(e.buttons & 1))
|
|
14
|
+
return;
|
|
15
|
+
let container = e.currentTarget.parentElement;
|
|
16
|
+
if (!container)
|
|
17
|
+
return;
|
|
18
|
+
// getBoundingClientRect() returns screen-space coords (affected by CSS transform/zoom).
|
|
19
|
+
// Divide by zoom to convert back to CSS-pixel space (the space onChange expects).
|
|
20
|
+
let rect = container.getBoundingClientRect();
|
|
21
|
+
let newX = Math.max(0, Math.min(containerWidth, (e.clientX - rect.left) / zoom));
|
|
22
|
+
let newY = Math.max(0, Math.min(containerHeight, (e.clientY - rect.top) / zoom));
|
|
23
|
+
onChange(newX, newY);
|
|
24
|
+
}
|
|
25
|
+
return (_jsx("div", { tabIndex: 0, role: "slider", "aria-label": "Corner handle", "aria-valuenow": Math.round(x), "aria-valuemin": 0, "aria-valuemax": containerWidth, className: "drag-handle", style: {
|
|
26
|
+
position: "absolute",
|
|
27
|
+
left: x,
|
|
28
|
+
top: y,
|
|
29
|
+
transform: "translate(-50%, -50%)",
|
|
30
|
+
width: 28,
|
|
31
|
+
height: 28,
|
|
32
|
+
borderRadius: "50%",
|
|
33
|
+
background: "rgba(33, 150, 243, 0.9)",
|
|
34
|
+
border: "3px solid white",
|
|
35
|
+
boxShadow: "0 2px 8px rgba(0,0,0,0.4)",
|
|
36
|
+
cursor: "grab",
|
|
37
|
+
touchAction: "none",
|
|
38
|
+
zIndex: 10,
|
|
39
|
+
}, onPointerDown: onPointerDown, onPointerMove: onPointerMove }));
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** CSS-pixel position of the handle center, relative to the crop-inner container. */
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
/** Controls cursor style and handle orientation only — movement is unconstrained (delta-based). */
|
|
6
|
+
axis: "x" | "y";
|
|
7
|
+
/** Called with the pointer delta in CSS pixels so both corners can be shifted equally. */
|
|
8
|
+
onChange: (dx: number, dy: number) => void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Absolutely-positioned draggable edge handle.
|
|
12
|
+
* Tracks the pointer delta so both corners on this edge move by the same amount,
|
|
13
|
+
* preserving the edge angle.
|
|
14
|
+
*/
|
|
15
|
+
export declare function EdgeHandle({ x, y, axis, onChange }: Props): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef } from "react";
|
|
3
|
+
import { useZoom } from "./plugins/zoomPlugin/ZoomContext.js";
|
|
4
|
+
/**
|
|
5
|
+
* Absolutely-positioned draggable edge handle.
|
|
6
|
+
* Tracks the pointer delta so both corners on this edge move by the same amount,
|
|
7
|
+
* preserving the edge angle.
|
|
8
|
+
*/
|
|
9
|
+
export function EdgeHandle({ x, y, axis, onChange }) {
|
|
10
|
+
let { zoom } = useZoom();
|
|
11
|
+
let lastPos = useRef(null);
|
|
12
|
+
function onPointerDown(e) {
|
|
13
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
14
|
+
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
15
|
+
}
|
|
16
|
+
function onPointerMove(e) {
|
|
17
|
+
if (!(e.buttons & 1) || !lastPos.current)
|
|
18
|
+
return;
|
|
19
|
+
// Convert screen-space delta to CSS-pixel delta by dividing by zoom.
|
|
20
|
+
let dx = (e.clientX - lastPos.current.x) / zoom;
|
|
21
|
+
let dy = (e.clientY - lastPos.current.y) / zoom;
|
|
22
|
+
lastPos.current = { x: e.clientX, y: e.clientY };
|
|
23
|
+
onChange(dx, dy);
|
|
24
|
+
}
|
|
25
|
+
function onPointerUp() {
|
|
26
|
+
lastPos.current = null;
|
|
27
|
+
}
|
|
28
|
+
return (_jsx("div", { className: "edge-handle", style: {
|
|
29
|
+
position: "absolute",
|
|
30
|
+
left: x,
|
|
31
|
+
top: y,
|
|
32
|
+
transform: "translate(-50%, -50%)",
|
|
33
|
+
width: axis === "x" ? 6 : 32,
|
|
34
|
+
height: axis === "x" ? 32 : 6,
|
|
35
|
+
borderRadius: 3,
|
|
36
|
+
background: "rgba(33, 150, 243, 0.85)",
|
|
37
|
+
border: "2px solid white",
|
|
38
|
+
boxShadow: "0 2px 6px rgba(0,0,0,0.35)",
|
|
39
|
+
cursor: axis === "x" ? "ew-resize" : "ns-resize",
|
|
40
|
+
touchAction: "none",
|
|
41
|
+
zIndex: 9,
|
|
42
|
+
}, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp }));
|
|
43
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
.image-editor {
|
|
2
|
+
height: 100%;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
background: #111;
|
|
6
|
+
color: white;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.image-editor__topbar {
|
|
10
|
+
display: flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
gap: 1rem;
|
|
14
|
+
padding: 0.5rem 1rem;
|
|
15
|
+
background: rgba(0, 0, 0, 0.4);
|
|
16
|
+
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.image-editor__canvas {
|
|
20
|
+
flex: 1;
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
position: relative;
|
|
25
|
+
overflow: hidden;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.image-editor__replay-overlay {
|
|
29
|
+
position: absolute;
|
|
30
|
+
inset: 0;
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
justify-content: center;
|
|
34
|
+
background: rgba(0, 0, 0, 0.4);
|
|
35
|
+
color: white;
|
|
36
|
+
z-index: 10;
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.image-editor__img {
|
|
41
|
+
max-width: 100%;
|
|
42
|
+
max-height: calc(100dvh - 160px); /* fallback when not inside zoom-canvas */
|
|
43
|
+
object-fit: contain;
|
|
44
|
+
border-radius: 4px;
|
|
45
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.5);
|
|
46
|
+
pointer-events: none;
|
|
47
|
+
user-select: none;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.image-editor__toolbar {
|
|
51
|
+
display: flex;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
padding: 0.75rem 1rem;
|
|
55
|
+
background: #1a1a1a;
|
|
56
|
+
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.image-editor__toolbar-group {
|
|
60
|
+
display: flex;
|
|
61
|
+
gap: 0.5rem;
|
|
62
|
+
align-items: center;
|
|
63
|
+
flex-wrap: wrap;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.image-editor__topbar-divider {
|
|
67
|
+
width: 1px;
|
|
68
|
+
height: 1.5rem;
|
|
69
|
+
background: rgba(255, 255, 255, 0.15);
|
|
70
|
+
flex-shrink: 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.image-editor__zoom-level {
|
|
74
|
+
font-size: 0.8125rem;
|
|
75
|
+
color: rgba(255, 255, 255, 0.6);
|
|
76
|
+
min-width: 3.25ch;
|
|
77
|
+
text-align: center;
|
|
78
|
+
font-variant-numeric: tabular-nums;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── Zoom canvas ─────────────────────────────────────────────── */
|
|
82
|
+
|
|
83
|
+
.zoom-canvas-root {
|
|
84
|
+
position: absolute;
|
|
85
|
+
inset: 0;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
justify-content: center;
|
|
89
|
+
overflow: hidden;
|
|
90
|
+
touch-action: none;
|
|
91
|
+
cursor: grab;
|
|
92
|
+
user-select: none;
|
|
93
|
+
/* Named size container so descendants can use cqh/cqw units. */
|
|
94
|
+
container-type: size;
|
|
95
|
+
container-name: zoom-canvas;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.zoom-canvas-root:active {
|
|
99
|
+
cursor: grabbing;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.zoom-canvas-inner {
|
|
103
|
+
transform-origin: center;
|
|
104
|
+
padding: 1rem;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/*
|
|
108
|
+
* Size all editor content relative to the canvas container rather than the
|
|
109
|
+
* viewport. cqh/cqw refer to zoom-canvas-root which has known dimensions
|
|
110
|
+
* (position: absolute; inset: 0), bypassing the percentage-height chain problem.
|
|
111
|
+
*
|
|
112
|
+
* Subtract padding so the element fits within its visual parent:
|
|
113
|
+
* • 2rem = zoom-canvas-inner top + bottom padding (1rem each)
|
|
114
|
+
* • 32px = crop-root top + bottom padding (16px each)
|
|
115
|
+
* • 4rem = zoom-canvas-inner + redact-root padding (2 × 2rem)
|
|
116
|
+
*/
|
|
117
|
+
@container zoom-canvas (min-height: 1px) {
|
|
118
|
+
.image-editor__img {
|
|
119
|
+
max-height: calc(100cqh - 2rem);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.crop-root {
|
|
123
|
+
max-height: calc(100cqh - 2rem);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Canvas has an intrinsic aspect ratio from its width/height attrs,
|
|
127
|
+
so both max constraints scale it to fit like an <img>. */
|
|
128
|
+
.crop-inner canvas {
|
|
129
|
+
max-height: calc(100cqh - 2rem - 32px);
|
|
130
|
+
width: auto;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.redact-image {
|
|
134
|
+
max-height: calc(100cqh - 4rem);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ── Crop plugin ─────────────────────────────────────────────── */
|
|
139
|
+
|
|
140
|
+
.crop-root {
|
|
141
|
+
position: relative;
|
|
142
|
+
width: 100%;
|
|
143
|
+
max-width: 900px;
|
|
144
|
+
max-height: calc(100dvh - 160px); /* fallback when not inside zoom-canvas */
|
|
145
|
+
/* Handles at the image corners are 28px circles — reserve half their size as padding
|
|
146
|
+
so they're never clipped at the edges. */
|
|
147
|
+
padding: 16px;
|
|
148
|
+
box-sizing: border-box;
|
|
149
|
+
background: #111;
|
|
150
|
+
touch-action: none;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.crop-inner {
|
|
154
|
+
position: relative;
|
|
155
|
+
display: inline-block;
|
|
156
|
+
line-height: 0; /* remove gap under canvas */
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/* ── Redact plugin ───────────────────────────────────────────── */
|
|
160
|
+
|
|
161
|
+
.redact-root {
|
|
162
|
+
display: flex;
|
|
163
|
+
align-items: center;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
width: 100%;
|
|
166
|
+
padding: 1rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.redact-inner {
|
|
170
|
+
position: relative;
|
|
171
|
+
display: inline-block;
|
|
172
|
+
line-height: 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.redact-image {
|
|
176
|
+
max-width: 100%;
|
|
177
|
+
max-height: calc(100dvh - 160px); /* fallback when not inside zoom-canvas */
|
|
178
|
+
display: block;
|
|
179
|
+
user-select: none;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.redact-svg {
|
|
183
|
+
position: absolute;
|
|
184
|
+
inset: 0;
|
|
185
|
+
cursor: crosshair;
|
|
186
|
+
touch-action: none;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* ── Handle hit-area expansion ───────────────────────────────── */
|
|
190
|
+
/* Invisible ::before expands the touch target without affecting layout */
|
|
191
|
+
|
|
192
|
+
.drag-handle::before {
|
|
193
|
+
content: "";
|
|
194
|
+
position: absolute;
|
|
195
|
+
inset: -12px; /* 28px visual → 52px hit area */
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.edge-handle::before {
|
|
199
|
+
content: "";
|
|
200
|
+
position: absolute;
|
|
201
|
+
/* Expand whichever dimension is thin (6px) to ≥44px hit area */
|
|
202
|
+
inset: -19px;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.crop-loading {
|
|
206
|
+
align-items: center;
|
|
207
|
+
justify-content: center;
|
|
208
|
+
gap: 0.75rem;
|
|
209
|
+
padding: 0.75rem 1rem;
|
|
210
|
+
color: rgba(255, 255, 255, 0.7);
|
|
211
|
+
position: absolute;
|
|
212
|
+
inset: auto 0 0 0;
|
|
213
|
+
background: linear-gradient(0deg, rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.LayoutSmall-root {
|
|
217
|
+
height: 100dvh;
|
|
218
|
+
overflow: hidden;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.LayoutSmall-root {
|
|
222
|
+
display: grid;
|
|
223
|
+
grid-template-rows: auto 1fr;
|
|
224
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import "./ImageEditor.css";
|
|
2
|
+
import type { Plugin, ViewPlugin } from "./types.js";
|
|
3
|
+
export type ImageEditorRef = {
|
|
4
|
+
/** Returns the current edited blob, or null if no edits have been applied since the last source image. */
|
|
5
|
+
getCurrentBlob: () => Blob | null;
|
|
6
|
+
};
|
|
7
|
+
interface Props {
|
|
8
|
+
imageUrl: string;
|
|
9
|
+
/** Called when the user clicks the Done button. When omitted, the Done button is not shown. */
|
|
10
|
+
onConfirm?: (blob: Blob) => void;
|
|
11
|
+
/** Called when the user wants to exit without saving. Shows a back button in the top bar. */
|
|
12
|
+
onCancel?: () => void;
|
|
13
|
+
plugins: Plugin[];
|
|
14
|
+
/** Optional view plugin (e.g. zoom/pan) that wraps the canvas and contributes top-bar controls. */
|
|
15
|
+
viewPlugin?: ViewPlugin;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Full-screen image editor shell.
|
|
19
|
+
* Does not know about specific tools — behaviour is entirely driven by the `plugins` prop.
|
|
20
|
+
* Tool plugins contribute an overlay and toolbar; action plugins execute immediately.
|
|
21
|
+
* Supports undo/redo: each applied transformation is recorded with its EditAction list.
|
|
22
|
+
*/
|
|
23
|
+
export declare const ImageEditor: import("react").ForwardRefExoticComponent<Props & import("react").RefAttributes<ImageEditorRef>>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
|
|
3
|
+
import CheckIcon from "@mui/icons-material/Check";
|
|
4
|
+
import RedoIcon from "@mui/icons-material/Redo";
|
|
5
|
+
import RestartAltIcon from "@mui/icons-material/RestartAlt";
|
|
6
|
+
import UndoIcon from "@mui/icons-material/Undo";
|
|
7
|
+
import { Button, CircularProgress, Fab, IconButton, Tooltip, useMediaQuery } from "@mui/material";
|
|
8
|
+
import { getLocalizedString } from "@olenbetong/appframe-core";
|
|
9
|
+
import { forwardRef, useImperativeHandle, useMemo, useState } from "react";
|
|
10
|
+
import "./ImageEditor.css";
|
|
11
|
+
import { ResponsiveButton } from "./ResponsiveButton.js";
|
|
12
|
+
import { useActiveTool } from "./useActiveTool.js";
|
|
13
|
+
import { useImageHistory } from "./useImageHistory.js";
|
|
14
|
+
/**
|
|
15
|
+
* Full-screen image editor shell.
|
|
16
|
+
* Does not know about specific tools — behaviour is entirely driven by the `plugins` prop.
|
|
17
|
+
* Tool plugins contribute an overlay and toolbar; action plugins execute immediately.
|
|
18
|
+
* Supports undo/redo: each applied transformation is recorded with its EditAction list.
|
|
19
|
+
*/
|
|
20
|
+
export const ImageEditor = forwardRef(function ImageEditor({ imageUrl, onConfirm, onCancel, plugins, viewPlugin }, ref) {
|
|
21
|
+
let registry = useMemo(() => buildActionRegistry(plugins), [plugins]);
|
|
22
|
+
let history = useImageHistory(imageUrl, registry);
|
|
23
|
+
let { activePlugin, activateTool, deactivateTool } = useActiveTool(plugins);
|
|
24
|
+
let [busy, setBusy] = useState(false);
|
|
25
|
+
let disabled = busy || history.replaying;
|
|
26
|
+
let isSmall = useMediaQuery("(max-width: 600px)");
|
|
27
|
+
useImperativeHandle(ref, () => ({
|
|
28
|
+
getCurrentBlob: () => history.currentBlob,
|
|
29
|
+
}));
|
|
30
|
+
async function executeAction(plugin) {
|
|
31
|
+
setBusy(true);
|
|
32
|
+
try {
|
|
33
|
+
let blob = await plugin.execute(history.currentImageUrl);
|
|
34
|
+
let actions = [{ type: plugin.id, params: {} }];
|
|
35
|
+
history.applyActions(blob, actions);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
setBusy(false);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function handleToolApply(blob, actions) {
|
|
42
|
+
history.applyActions(blob, actions);
|
|
43
|
+
deactivateTool();
|
|
44
|
+
}
|
|
45
|
+
function confirm() {
|
|
46
|
+
if (!onConfirm)
|
|
47
|
+
return;
|
|
48
|
+
if (history.currentBlob) {
|
|
49
|
+
onConfirm(history.currentBlob);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
fetch(history.currentImageUrl)
|
|
53
|
+
.then((r) => r.blob())
|
|
54
|
+
.then(onConfirm);
|
|
55
|
+
}
|
|
56
|
+
let CanvasWrapper = viewPlugin?.CanvasWrapper;
|
|
57
|
+
let TopBarControls = viewPlugin?.TopBarControls;
|
|
58
|
+
let inner = (_jsxs("div", { className: "image-editor", children: [_jsxs("div", { className: "image-editor__topbar", children: [_jsxs("div", { className: "image-editor__toolbar-group", children: [onCancel && (_jsxs(_Fragment, { children: [_jsx(ResponsiveButton, { icon: _jsx(ArrowBackIcon, {}), label: getLocalizedString("Back"), onClick: onCancel, isSmall: isSmall }), _jsx("span", { className: "image-editor__topbar-divider", "aria-hidden": true })] })), _jsx(ResponsiveButton, { icon: _jsx(UndoIcon, {}), label: getLocalizedString("Undo"), onClick: history.undo, disabled: disabled || !history.canUndo || !!activePlugin, isSmall: isSmall }), _jsx(ResponsiveButton, { icon: _jsx(RedoIcon, {}), label: getLocalizedString("Redo"), onClick: history.redo, disabled: disabled || !history.canRedo || !!activePlugin, isSmall: isSmall }), _jsx(ResponsiveButton, { icon: _jsx(RestartAltIcon, {}), label: getLocalizedString("Reset"), onClick: history.reset, disabled: disabled || !history.canUndo || !!activePlugin, isSmall: isSmall })] }), TopBarControls && (_jsx("div", { className: "image-editor__toolbar-group", children: _jsx(TopBarControls, { disabled: disabled, isSmall: isSmall }) }))] }), _jsxs("div", { className: "image-editor__canvas", children: [CanvasWrapper ? (_jsx(CanvasWrapper, { panEnabled: !activePlugin, children: activePlugin ? (_jsx(ActiveOverlay, { plugin: activePlugin })) : (_jsx("img", { src: history.currentImageUrl, alt: getLocalizedString("Image to edit"), className: "image-editor__img" })) })) : activePlugin ? (_jsx(ActiveOverlay, { plugin: activePlugin })) : (_jsx("img", { src: history.currentImageUrl, alt: getLocalizedString("Image to edit"), className: "image-editor__img" })), !activePlugin && onConfirm && (_jsxs(Fab, { color: "primary", variant: isSmall ? "circular" : "extended", onClick: confirm, disabled: disabled, sx: { position: "absolute", bottom: "1rem", right: "1rem", zIndex: 5 }, children: [_jsx(CheckIcon, { sx: isSmall ? undefined : { mr: 1 } }), !isSmall && getLocalizedString("Done")] })), history.replaying && (_jsx("div", { className: "image-editor__replay-overlay", children: _jsx(CircularProgress, { color: "inherit" }) }))] }), _jsx("div", { className: "image-editor__toolbar", children: activePlugin ? (_jsx(ActiveToolbar, { plugin: activePlugin })) : (_jsx("div", { className: "image-editor__toolbar-group", children: plugins.map((p) => p.type === "tool" ? (_jsx(ToolButton, { plugin: p, busy: disabled, onActivate: () => activateTool(p.id), isSmall: isSmall }, p.id)) : (_jsx(ActionButton, { plugin: p, busy: disabled, onExecute: () => executeAction(p), isSmall: isSmall }, p.id))) })) })] }));
|
|
59
|
+
// Wrap with the active tool's provider so Overlay and Toolbar share context.
|
|
60
|
+
if (activePlugin) {
|
|
61
|
+
let ActiveProvider = activePlugin.Provider;
|
|
62
|
+
inner = (_jsx(ActiveProvider, { imageUrl: history.currentImageUrl, onApply: handleToolApply, onCancel: deactivateTool, children: inner }));
|
|
63
|
+
}
|
|
64
|
+
// Wrap with the view plugin's provider (outermost) so both CanvasWrapper and TopBarControls share state.
|
|
65
|
+
if (viewPlugin) {
|
|
66
|
+
let ViewProvider = viewPlugin.Provider;
|
|
67
|
+
return _jsx(ViewProvider, { children: inner });
|
|
68
|
+
}
|
|
69
|
+
return inner;
|
|
70
|
+
});
|
|
71
|
+
function ToolButton({ plugin, busy, onActivate, isSmall, }) {
|
|
72
|
+
let Icon = plugin.Icon;
|
|
73
|
+
if (isSmall) {
|
|
74
|
+
return (_jsx(Tooltip, { title: getLocalizedString(plugin.label), children: _jsx("span", { children: _jsx(IconButton, { onClick: onActivate, disabled: busy, color: "inherit", children: _jsx(Icon, {}) }) }) }));
|
|
75
|
+
}
|
|
76
|
+
return (_jsx(Button, { startIcon: _jsx(Icon, {}), onClick: onActivate, disabled: busy, variant: "outlined", color: "inherit", children: getLocalizedString(plugin.label) }));
|
|
77
|
+
}
|
|
78
|
+
function ActionButton({ plugin, busy, onExecute, isSmall, }) {
|
|
79
|
+
let Icon = plugin.Icon;
|
|
80
|
+
if (isSmall) {
|
|
81
|
+
return (_jsx(Tooltip, { title: getLocalizedString(plugin.label), children: _jsx("span", { children: _jsx(IconButton, { onClick: onExecute, disabled: busy, color: "inherit", children: busy ? _jsx(CircularProgress, { size: 18, color: "inherit" }) : _jsx(Icon, {}) }) }) }));
|
|
82
|
+
}
|
|
83
|
+
return (_jsx(Button, { startIcon: busy ? _jsx(CircularProgress, { size: 18, color: "inherit" }) : _jsx(Icon, {}), onClick: onExecute, disabled: busy, color: "inherit", children: getLocalizedString(plugin.label) }));
|
|
84
|
+
}
|
|
85
|
+
function ActiveOverlay({ plugin }) {
|
|
86
|
+
let Overlay = plugin.Overlay;
|
|
87
|
+
return _jsx(Overlay, {});
|
|
88
|
+
}
|
|
89
|
+
function ActiveToolbar({ plugin }) {
|
|
90
|
+
let Toolbar = plugin.Toolbar;
|
|
91
|
+
return _jsx(Toolbar, {});
|
|
92
|
+
}
|
|
93
|
+
/** Builds the action registry from all registered plugins. */
|
|
94
|
+
function buildActionRegistry(plugins) {
|
|
95
|
+
let registry = new Map();
|
|
96
|
+
for (let plugin of plugins) {
|
|
97
|
+
if (plugin.type === "action") {
|
|
98
|
+
// Action plugins have no params — their execute function IS the replay handler.
|
|
99
|
+
registry.set(plugin.id, (imageUrl, _params) => plugin.execute(imageUrl));
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
for (let action of plugin.actions) {
|
|
103
|
+
registry.set(action.type, action.apply);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return registry;
|
|
108
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface Props {
|
|
3
|
+
icon: ReactNode;
|
|
4
|
+
label: string;
|
|
5
|
+
onClick: () => void;
|
|
6
|
+
disabled?: boolean;
|
|
7
|
+
isSmall: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Renders a labelled Button on large screens and a Tooltip-wrapped IconButton on small ones. */
|
|
10
|
+
export declare function ResponsiveButton({ icon, label, onClick, disabled, isSmall }: Props): import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Button, IconButton, Tooltip } from "@mui/material";
|
|
3
|
+
/** Renders a labelled Button on large screens and a Tooltip-wrapped IconButton on small ones. */
|
|
4
|
+
export function ResponsiveButton({ icon, label, onClick, disabled, isSmall }) {
|
|
5
|
+
if (isSmall) {
|
|
6
|
+
return (_jsx(Tooltip, { title: label, children: _jsx("span", { children: _jsx(IconButton, { onClick: onClick, disabled: disabled, color: "inherit", size: "small", children: icon }) }) }));
|
|
7
|
+
}
|
|
8
|
+
return (_jsx(Button, { startIcon: icon, onClick: onClick, color: "inherit", disabled: disabled, children: label }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Default set of image editing plugins in display order. */
|
|
2
|
+
export declare const defaultPlugins: (import("./types.js").ToolPlugin | import("./types.js").ActionPlugin)[];
|
|
3
|
+
/** Default view plugin providing zoom and pan. */
|
|
4
|
+
export declare const defaultViewPlugin: import("./types.js").ViewPlugin;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { cropPlugin } from "./plugins/cropPlugin/index.js";
|
|
2
|
+
import { redactPlugin } from "./plugins/redactPlugin/index.js";
|
|
3
|
+
import { rotateCcwPlugin, rotateCwPlugin } from "./plugins/rotatePlugin.js";
|
|
4
|
+
import { zoomPlugin } from "./plugins/zoomPlugin/index.js";
|
|
5
|
+
/** Default set of image editing plugins in display order. */
|
|
6
|
+
export const defaultPlugins = [rotateCcwPlugin, rotateCwPlugin, cropPlugin, redactPlugin];
|
|
7
|
+
/** Default view plugin providing zoom and pan. */
|
|
8
|
+
export const defaultViewPlugin = zoomPlugin;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { defaultPlugins, defaultViewPlugin } from "./defaultPlugins.js";
|
|
2
|
+
export type { ImageEditorRef } from "./ImageEditor.js";
|
|
3
|
+
export { ImageEditor } from "./ImageEditor.js";
|
|
4
|
+
export { cropPlugin } from "./plugins/cropPlugin/index.js";
|
|
5
|
+
export { rotateCcwPlugin, rotateCwPlugin } from "./plugins/rotatePlugin.js";
|
|
6
|
+
export type { ActionPlugin, EditAction, Plugin, ToolPlugin, ViewPlugin } from "./types.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EditAction, ToolProviderProps } from "../../types.js";
|
|
2
|
+
import type { CropToolState } from "./useCropTool.js";
|
|
3
|
+
interface CropContextValue extends CropToolState {
|
|
4
|
+
imageUrl: string;
|
|
5
|
+
onApply: (blob: Blob, actions: EditAction[]) => void;
|
|
6
|
+
onCancel: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function CropProvider({ imageUrl, onApply, onCancel, children }: ToolProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function useCropContext(): CropContextValue;
|
|
10
|
+
export {};
|