@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.
Files changed (107) hide show
  1. package/es/components/FilePond.d.ts +1 -1
  2. package/es/components/FilePond.js +1 -1
  3. package/es/components/ImageEditor/CropCanvas.d.ts +16 -0
  4. package/es/components/ImageEditor/CropCanvas.js +79 -0
  5. package/es/components/ImageEditor/DragHandle.d.ts +14 -0
  6. package/es/components/ImageEditor/DragHandle.js +40 -0
  7. package/es/components/ImageEditor/EdgeHandle.d.ts +16 -0
  8. package/es/components/ImageEditor/EdgeHandle.js +43 -0
  9. package/es/components/ImageEditor/ImageEditor.css +224 -0
  10. package/es/components/ImageEditor/ImageEditor.d.ts +24 -0
  11. package/es/components/ImageEditor/ImageEditor.js +108 -0
  12. package/es/components/ImageEditor/ResponsiveButton.d.ts +11 -0
  13. package/es/components/ImageEditor/ResponsiveButton.js +9 -0
  14. package/es/components/ImageEditor/defaultPlugins.d.ts +4 -0
  15. package/es/components/ImageEditor/defaultPlugins.js +8 -0
  16. package/es/components/ImageEditor/index.d.ts +6 -0
  17. package/es/components/ImageEditor/index.js +4 -0
  18. package/es/components/ImageEditor/plugins/cropPlugin/CropContext.d.ts +10 -0
  19. package/es/components/ImageEditor/plugins/cropPlugin/CropContext.js +14 -0
  20. package/es/components/ImageEditor/plugins/cropPlugin/CropOverlay.d.ts +2 -0
  21. package/es/components/ImageEditor/plugins/cropPlugin/CropOverlay.js +30 -0
  22. package/es/components/ImageEditor/plugins/cropPlugin/CropToolbar.d.ts +2 -0
  23. package/es/components/ImageEditor/plugins/cropPlugin/CropToolbar.js +16 -0
  24. package/es/components/ImageEditor/plugins/cropPlugin/index.d.ts +6 -0
  25. package/es/components/ImageEditor/plugins/cropPlugin/index.js +16 -0
  26. package/es/components/ImageEditor/plugins/cropPlugin/useCropTool.d.ts +34 -0
  27. package/es/components/ImageEditor/plugins/cropPlugin/useCropTool.js +138 -0
  28. package/es/components/ImageEditor/plugins/redactPlugin/RedactContext.d.ts +10 -0
  29. package/es/components/ImageEditor/plugins/redactPlugin/RedactContext.js +14 -0
  30. package/es/components/ImageEditor/plugins/redactPlugin/RedactOverlay.d.ts +6 -0
  31. package/es/components/ImageEditor/plugins/redactPlugin/RedactOverlay.js +29 -0
  32. package/es/components/ImageEditor/plugins/redactPlugin/RedactToolbar.d.ts +2 -0
  33. package/es/components/ImageEditor/plugins/redactPlugin/RedactToolbar.js +29 -0
  34. package/es/components/ImageEditor/plugins/redactPlugin/index.d.ts +6 -0
  35. package/es/components/ImageEditor/plugins/redactPlugin/index.js +16 -0
  36. package/es/components/ImageEditor/plugins/redactPlugin/useRedactTool.d.ts +49 -0
  37. package/es/components/ImageEditor/plugins/redactPlugin/useRedactTool.js +142 -0
  38. package/es/components/ImageEditor/plugins/rotatePlugin.d.ts +3 -0
  39. package/es/components/ImageEditor/plugins/rotatePlugin.js +39 -0
  40. package/es/components/ImageEditor/plugins/zoomPlugin/ZoomCanvas.d.ts +20 -0
  41. package/es/components/ImageEditor/plugins/zoomPlugin/ZoomCanvas.js +91 -0
  42. package/es/components/ImageEditor/plugins/zoomPlugin/ZoomContext.d.ts +6 -0
  43. package/es/components/ImageEditor/plugins/zoomPlugin/ZoomContext.js +14 -0
  44. package/es/components/ImageEditor/plugins/zoomPlugin/ZoomControls.d.ts +6 -0
  45. package/es/components/ImageEditor/plugins/zoomPlugin/ZoomControls.js +11 -0
  46. package/es/components/ImageEditor/plugins/zoomPlugin/index.d.ts +2 -0
  47. package/es/components/ImageEditor/plugins/zoomPlugin/index.js +10 -0
  48. package/es/components/ImageEditor/plugins/zoomPlugin/useZoomPan.d.ts +33 -0
  49. package/es/components/ImageEditor/plugins/zoomPlugin/useZoomPan.js +80 -0
  50. package/es/components/ImageEditor/types.d.ts +69 -0
  51. package/es/components/ImageEditor/types.js +1 -0
  52. package/es/components/ImageEditor/useActiveTool.d.ts +9 -0
  53. package/es/components/ImageEditor/useActiveTool.js +11 -0
  54. package/es/components/ImageEditor/useEdgeDetection.d.ts +18 -0
  55. package/es/components/ImageEditor/useEdgeDetection.js +125 -0
  56. package/es/components/ImageEditor/useImageEditor.d.ts +12 -0
  57. package/es/components/ImageEditor/useImageEditor.js +28 -0
  58. package/es/components/ImageEditor/useImageHistory.d.ts +21 -0
  59. package/es/components/ImageEditor/useImageHistory.js +153 -0
  60. package/es/components/ImageEditor/useOpenCV.d.ts +14 -0
  61. package/es/components/ImageEditor/useOpenCV.js +92 -0
  62. package/es/components/MergeProgressDialog/index.d.ts +8 -0
  63. package/es/components/MergeProgressDialog/index.js +136 -0
  64. package/es/components/PdfViewer/PdfViewer.css +12 -0
  65. package/es/components/PdfViewer/PdfViewer.d.ts +7 -0
  66. package/es/components/PdfViewer/PdfViewer.js +108 -0
  67. package/es/components/PdfViewer/index.d.ts +1 -0
  68. package/es/components/PdfViewer/index.js +1 -0
  69. package/es/index.d.ts +3 -0
  70. package/es/index.js +3 -0
  71. package/package.json +8 -3
  72. package/src/components/FilePond.tsx +1 -1
  73. package/src/components/ImageEditor/CropCanvas.tsx +100 -0
  74. package/src/components/ImageEditor/DragHandle.tsx +65 -0
  75. package/src/components/ImageEditor/EdgeHandle.tsx +65 -0
  76. package/src/components/ImageEditor/ImageEditor.css +224 -0
  77. package/src/components/ImageEditor/ImageEditor.tsx +310 -0
  78. package/src/components/ImageEditor/ResponsiveButton.tsx +32 -0
  79. package/src/components/ImageEditor/defaultPlugins.ts +10 -0
  80. package/src/components/ImageEditor/index.ts +6 -0
  81. package/src/components/ImageEditor/plugins/cropPlugin/CropContext.tsx +24 -0
  82. package/src/components/ImageEditor/plugins/cropPlugin/CropOverlay.tsx +119 -0
  83. package/src/components/ImageEditor/plugins/cropPlugin/CropToolbar.tsx +36 -0
  84. package/src/components/ImageEditor/plugins/cropPlugin/index.ts +20 -0
  85. package/src/components/ImageEditor/plugins/cropPlugin/useCropTool.ts +176 -0
  86. package/src/components/ImageEditor/plugins/redactPlugin/RedactContext.tsx +23 -0
  87. package/src/components/ImageEditor/plugins/redactPlugin/RedactOverlay.tsx +144 -0
  88. package/src/components/ImageEditor/plugins/redactPlugin/RedactToolbar.tsx +56 -0
  89. package/src/components/ImageEditor/plugins/redactPlugin/index.ts +20 -0
  90. package/src/components/ImageEditor/plugins/redactPlugin/useRedactTool.ts +197 -0
  91. package/src/components/ImageEditor/plugins/rotatePlugin.ts +43 -0
  92. package/src/components/ImageEditor/plugins/zoomPlugin/ZoomCanvas.tsx +123 -0
  93. package/src/components/ImageEditor/plugins/zoomPlugin/ZoomContext.tsx +15 -0
  94. package/src/components/ImageEditor/plugins/zoomPlugin/ZoomControls.tsx +43 -0
  95. package/src/components/ImageEditor/plugins/zoomPlugin/index.ts +12 -0
  96. package/src/components/ImageEditor/plugins/zoomPlugin/useZoomPan.ts +126 -0
  97. package/src/components/ImageEditor/types.ts +69 -0
  98. package/src/components/ImageEditor/useActiveTool.ts +21 -0
  99. package/src/components/ImageEditor/useEdgeDetection.ts +167 -0
  100. package/src/components/ImageEditor/useImageEditor.ts +39 -0
  101. package/src/components/ImageEditor/useImageHistory.ts +189 -0
  102. package/src/components/ImageEditor/useOpenCV.ts +134 -0
  103. package/src/components/MergeProgressDialog/index.tsx +249 -0
  104. package/src/components/PdfViewer/PdfViewer.css +12 -0
  105. package/src/components/PdfViewer/PdfViewer.tsx +139 -0
  106. package/src/components/PdfViewer/index.ts +1 -0
  107. package/src/index.ts +3 -0
@@ -0,0 +1,100 @@
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
2
+
3
+ import type { Quad } from "./useEdgeDetection.js";
4
+
5
+ const MAX_DISPLAY_WIDTH = 1600;
6
+
7
+ export interface CropCanvasHandle {
8
+ /** Returns a canvas with the clean image (no overlay) for use with cv.imread. */
9
+ getCleanCanvas(): HTMLCanvasElement | null;
10
+ }
11
+
12
+ interface Props {
13
+ imageUrl: string;
14
+ corners: Quad;
15
+ onReady?: (canvas: HTMLCanvasElement, width: number, height: number) => void;
16
+ }
17
+
18
+ function drawOverlay(ctx: CanvasRenderingContext2D, width: number, height: number, corners: Quad) {
19
+ let [tl, tr, br, bl] = corners;
20
+
21
+ ctx.save();
22
+
23
+ // Dark overlay with polygon cutout (even-odd fill rule creates a "window")
24
+ ctx.fillStyle = "rgba(0, 0, 0, 0.5)";
25
+ ctx.beginPath();
26
+ ctx.rect(0, 0, width, height);
27
+ ctx.moveTo(tl.x, tl.y);
28
+ ctx.lineTo(tr.x, tr.y);
29
+ ctx.lineTo(br.x, br.y);
30
+ ctx.lineTo(bl.x, bl.y);
31
+ ctx.closePath();
32
+ ctx.fill("evenodd");
33
+
34
+ // Selection border
35
+ ctx.strokeStyle = "#2196f3";
36
+ ctx.lineWidth = Math.max(2, width * 0.003);
37
+ ctx.beginPath();
38
+ ctx.moveTo(tl.x, tl.y);
39
+ ctx.lineTo(tr.x, tr.y);
40
+ ctx.lineTo(br.x, br.y);
41
+ ctx.lineTo(bl.x, bl.y);
42
+ ctx.closePath();
43
+ ctx.stroke();
44
+
45
+ ctx.restore();
46
+ }
47
+
48
+ /**
49
+ * Renders the image on a canvas with a perspective-crop overlay.
50
+ * Exposes `getImageData()` so callers can extract pixels for off-thread processing.
51
+ */
52
+ export let CropCanvas = forwardRef<CropCanvasHandle, Props>(function CropCanvas({ imageUrl, corners, onReady }, ref) {
53
+ let canvasRef = useRef<HTMLCanvasElement>(null);
54
+ let imageRef = useRef<HTMLImageElement | null>(null);
55
+
56
+ useEffect(() => {
57
+ let img = new Image();
58
+ img.onload = () => {
59
+ imageRef.current = img;
60
+ let canvas = canvasRef.current;
61
+ if (!canvas) return;
62
+
63
+ let scale = Math.min(1, MAX_DISPLAY_WIDTH / img.naturalWidth);
64
+ canvas.width = Math.round(img.naturalWidth * scale);
65
+ canvas.height = Math.round(img.naturalHeight * scale);
66
+
67
+ let ctx = canvas.getContext("2d");
68
+ ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
69
+ drawOverlay(ctx!, canvas.width, canvas.height, corners);
70
+ onReady?.(canvas, canvas.width, canvas.height);
71
+ };
72
+ img.src = imageUrl;
73
+ }, [imageUrl]); // eslint-disable-line react-hooks/exhaustive-deps
74
+
75
+ // Redraw overlay whenever corners change
76
+ useEffect(() => {
77
+ let canvas = canvasRef.current;
78
+ let img = imageRef.current;
79
+ if (!canvas || !img) return;
80
+ let ctx = canvas.getContext("2d");
81
+ if (!ctx) return;
82
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
83
+ drawOverlay(ctx, canvas.width, canvas.height, corners);
84
+ }, [corners]);
85
+
86
+ useImperativeHandle(ref, () => ({
87
+ getCleanCanvas(): HTMLCanvasElement | null {
88
+ let canvas = canvasRef.current;
89
+ let img = imageRef.current;
90
+ if (!canvas || !img) return null;
91
+ let clean = document.createElement("canvas");
92
+ clean.width = canvas.width;
93
+ clean.height = canvas.height;
94
+ clean.getContext("2d")?.drawImage(img, 0, 0, canvas.width, canvas.height);
95
+ return clean;
96
+ },
97
+ }));
98
+
99
+ return <canvas ref={canvasRef} style={{ display: "block", maxWidth: "100%", height: "auto" }} />;
100
+ });
@@ -0,0 +1,65 @@
1
+ import { useZoom } from "./plugins/zoomPlugin/ZoomContext.js";
2
+
3
+ interface Props {
4
+ x: number;
5
+ y: number;
6
+ /** Called with new position in pixels (relative to container). */
7
+ onChange: (x: number, y: number) => void;
8
+ containerWidth: number;
9
+ containerHeight: number;
10
+ }
11
+
12
+ /**
13
+ * Absolutely-positioned draggable corner handle.
14
+ * Position is in CSS pixels relative to the nearest `position: relative` ancestor.
15
+ */
16
+ export function DragHandle({ x, y, onChange, containerWidth, containerHeight }: Props) {
17
+ let { zoom } = useZoom();
18
+
19
+ function onPointerDown(e: React.PointerEvent<HTMLDivElement>) {
20
+ e.currentTarget.setPointerCapture(e.pointerId);
21
+ }
22
+
23
+ function onPointerMove(e: React.PointerEvent<HTMLDivElement>) {
24
+ if (!(e.buttons & 1)) return;
25
+
26
+ let container = e.currentTarget.parentElement;
27
+ if (!container) return;
28
+
29
+ // getBoundingClientRect() returns screen-space coords (affected by CSS transform/zoom).
30
+ // Divide by zoom to convert back to CSS-pixel space (the space onChange expects).
31
+ let rect = container.getBoundingClientRect();
32
+ let newX = Math.max(0, Math.min(containerWidth, (e.clientX - rect.left) / zoom));
33
+ let newY = Math.max(0, Math.min(containerHeight, (e.clientY - rect.top) / zoom));
34
+ onChange(newX, newY);
35
+ }
36
+
37
+ return (
38
+ <div
39
+ tabIndex={0}
40
+ role="slider"
41
+ aria-label="Corner handle"
42
+ aria-valuenow={Math.round(x)}
43
+ aria-valuemin={0}
44
+ aria-valuemax={containerWidth}
45
+ className="drag-handle"
46
+ style={{
47
+ position: "absolute",
48
+ left: x,
49
+ top: y,
50
+ transform: "translate(-50%, -50%)",
51
+ width: 28,
52
+ height: 28,
53
+ borderRadius: "50%",
54
+ background: "rgba(33, 150, 243, 0.9)",
55
+ border: "3px solid white",
56
+ boxShadow: "0 2px 8px rgba(0,0,0,0.4)",
57
+ cursor: "grab",
58
+ touchAction: "none",
59
+ zIndex: 10,
60
+ }}
61
+ onPointerDown={onPointerDown}
62
+ onPointerMove={onPointerMove}
63
+ />
64
+ );
65
+ }
@@ -0,0 +1,65 @@
1
+ import { useRef } from "react";
2
+ import { useZoom } from "./plugins/zoomPlugin/ZoomContext.js";
3
+
4
+ interface Props {
5
+ /** CSS-pixel position of the handle center, relative to the crop-inner container. */
6
+ x: number;
7
+ y: number;
8
+ /** Controls cursor style and handle orientation only — movement is unconstrained (delta-based). */
9
+ axis: "x" | "y";
10
+ /** Called with the pointer delta in CSS pixels so both corners can be shifted equally. */
11
+ onChange: (dx: number, dy: number) => void;
12
+ }
13
+
14
+ /**
15
+ * Absolutely-positioned draggable edge handle.
16
+ * Tracks the pointer delta so both corners on this edge move by the same amount,
17
+ * preserving the edge angle.
18
+ */
19
+ export function EdgeHandle({ x, y, axis, onChange }: Props) {
20
+ let { zoom } = useZoom();
21
+ let lastPos = useRef<{ x: number; y: number } | null>(null);
22
+
23
+ function onPointerDown(e: React.PointerEvent<HTMLDivElement>) {
24
+ e.currentTarget.setPointerCapture(e.pointerId);
25
+ lastPos.current = { x: e.clientX, y: e.clientY };
26
+ }
27
+
28
+ function onPointerMove(e: React.PointerEvent<HTMLDivElement>) {
29
+ if (!(e.buttons & 1) || !lastPos.current) return;
30
+
31
+ // Convert screen-space delta to CSS-pixel delta by dividing by zoom.
32
+ let dx = (e.clientX - lastPos.current.x) / zoom;
33
+ let dy = (e.clientY - lastPos.current.y) / zoom;
34
+ lastPos.current = { x: e.clientX, y: e.clientY };
35
+ onChange(dx, dy);
36
+ }
37
+
38
+ function onPointerUp() {
39
+ lastPos.current = null;
40
+ }
41
+
42
+ return (
43
+ <div
44
+ className="edge-handle"
45
+ style={{
46
+ position: "absolute",
47
+ left: x,
48
+ top: y,
49
+ transform: "translate(-50%, -50%)",
50
+ width: axis === "x" ? 6 : 32,
51
+ height: axis === "x" ? 32 : 6,
52
+ borderRadius: 3,
53
+ background: "rgba(33, 150, 243, 0.85)",
54
+ border: "2px solid white",
55
+ boxShadow: "0 2px 6px rgba(0,0,0,0.35)",
56
+ cursor: axis === "x" ? "ew-resize" : "ns-resize",
57
+ touchAction: "none",
58
+ zIndex: 9,
59
+ }}
60
+ onPointerDown={onPointerDown}
61
+ onPointerMove={onPointerMove}
62
+ onPointerUp={onPointerUp}
63
+ />
64
+ );
65
+ }
@@ -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
+ }