@scaleflex/crop 2.0.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/LICENSE +21 -0
- package/README.md +452 -0
- package/dist/a11y/aria.d.ts +5 -0
- package/dist/a11y/keyboard.d.ts +13 -0
- package/dist/animation/lerp.d.ts +15 -0
- package/dist/animation/spring.d.ts +15 -0
- package/dist/canvas/bleed-layer.d.ts +6 -0
- package/dist/canvas/crop-frame.d.ts +32 -0
- package/dist/canvas/grid-layer.d.ts +7 -0
- package/dist/canvas/hit-test.d.ts +10 -0
- package/dist/canvas/image-layer.d.ts +28 -0
- package/dist/canvas/overlay-layer.d.ts +6 -0
- package/dist/canvas/renderer.d.ts +34 -0
- package/dist/chunks/sfx-crop-1LGASewd.cjs +353 -0
- package/dist/chunks/sfx-crop-CEe6OfTZ.js +2030 -0
- package/dist/core/config.d.ts +10 -0
- package/dist/core/crop-controller.d.ts +65 -0
- package/dist/core/types.d.ts +270 -0
- package/dist/define.cjs +1194 -0
- package/dist/define.d.ts +1 -0
- package/dist/define.js +1746 -0
- package/dist/elements/base.d.ts +17 -0
- package/dist/elements/icons.d.ts +21 -0
- package/dist/elements/parse-shapes.d.ts +13 -0
- package/dist/elements/popover-anchor.d.ts +20 -0
- package/dist/elements/sfx-crop-canvas.d.ts +24 -0
- package/dist/elements/sfx-crop-canvas.styles.d.ts +1 -0
- package/dist/elements/sfx-crop-rotate.d.ts +42 -0
- package/dist/elements/sfx-crop-rotate.styles.d.ts +5 -0
- package/dist/elements/sfx-crop-shapes.d.ts +67 -0
- package/dist/elements/sfx-crop-shapes.styles.d.ts +6 -0
- package/dist/elements/sfx-crop-toolbar.d.ts +64 -0
- package/dist/elements/sfx-crop-toolbar.styles.d.ts +7 -0
- package/dist/elements/sfx-crop-zoom.d.ts +66 -0
- package/dist/elements/sfx-crop-zoom.styles.d.ts +7 -0
- package/dist/elements/sfx-crop.d.ts +134 -0
- package/dist/elements/sfx-crop.styles.d.ts +9 -0
- package/dist/export/exporter.d.ts +19 -0
- package/dist/index.cjs +2 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +65 -0
- package/dist/interactions/drag-crop.d.ts +10 -0
- package/dist/interactions/pinch-zoom.d.ts +14 -0
- package/dist/interactions/pointer-tracker.d.ts +29 -0
- package/dist/interactions/resize-handles.d.ts +13 -0
- package/dist/interactions/wheel-zoom.d.ts +12 -0
- package/dist/react/define-CVJd5aYk.cjs +1545 -0
- package/dist/react/define-t4Z6KaLY.js +2590 -0
- package/dist/react/index-B-csHwK2.cjs +2 -0
- package/dist/react/index-CktjrogS.js +1468 -0
- package/dist/react/index.cjs +2 -0
- package/dist/react/index.d.ts +21 -0
- package/dist/react/index.js +10 -0
- package/dist/react/sfx-crop.d.ts +86 -0
- package/dist/react/use-sfx-crop-controller.d.ts +74 -0
- package/dist/react/use-sfx-crop.d.ts +31 -0
- package/dist/styles/shared.css.d.ts +20 -0
- package/dist/transforms/constrain.d.ts +68 -0
- package/dist/transforms/matrix.d.ts +23 -0
- package/dist/transforms/transform-state.d.ts +12 -0
- package/dist/utils/events.d.ts +16 -0
- package/dist/utils/math.d.ts +12 -0
- package/package.json +108 -0
|
@@ -0,0 +1,2590 @@
|
|
|
1
|
+
import { LitElement as mt, css as b, html as f } from "lit";
|
|
2
|
+
import { query as E, property as s, state as M } from "lit/decorators.js";
|
|
3
|
+
import { classMap as at } from "lit/directives/class-map.js";
|
|
4
|
+
import { p as ht, s as wt, m as yt, c as kt, g as q, a as R } from "./index-CktjrogS.js";
|
|
5
|
+
import { unsafeHTML as k } from "lit/directives/unsafe-html.js";
|
|
6
|
+
class N extends mt {
|
|
7
|
+
}
|
|
8
|
+
function A(i, t) {
|
|
9
|
+
typeof customElements > "u" || customElements.get(i) || customElements.define(i, t);
|
|
10
|
+
}
|
|
11
|
+
const St = b`
|
|
12
|
+
:host {
|
|
13
|
+
display: block;
|
|
14
|
+
width: 100%;
|
|
15
|
+
height: 100%;
|
|
16
|
+
min-width: 0;
|
|
17
|
+
min-height: 0;
|
|
18
|
+
/* Single source of clipping is .sfx-cr-container (overflow:hidden +
|
|
19
|
+
border-radius). Don't add a second mask here: this host is sized
|
|
20
|
+
by JS to the full outer rect (displayW×displayH), which is 4px
|
|
21
|
+
wider than the container's content-box, so its own border-radius
|
|
22
|
+
wouldn't align with the container's effective inner radius
|
|
23
|
+
(radius - border-width) and you'd see two stepped curves at the
|
|
24
|
+
corner. Transparent bg lets the container surface show through
|
|
25
|
+
in the rounded corner sliver. */
|
|
26
|
+
background: transparent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
canvas {
|
|
30
|
+
display: block;
|
|
31
|
+
width: 100%;
|
|
32
|
+
height: 100%;
|
|
33
|
+
outline: none;
|
|
34
|
+
touch-action: none;
|
|
35
|
+
}
|
|
36
|
+
`;
|
|
37
|
+
var Ct = Object.defineProperty, $t = (i, t, e, o) => {
|
|
38
|
+
for (var r = void 0, a = i.length - 1, n; a >= 0; a--)
|
|
39
|
+
(n = i[a]) && (r = n(t, e, r) || r);
|
|
40
|
+
return r && Ct(t, e, r), r;
|
|
41
|
+
};
|
|
42
|
+
const Q = class Q extends N {
|
|
43
|
+
render() {
|
|
44
|
+
return f`<canvas part="canvas"></canvas>`;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
Q.styles = [St];
|
|
48
|
+
let D = Q;
|
|
49
|
+
$t([
|
|
50
|
+
E("canvas")
|
|
51
|
+
], D.prototype, "canvasEl");
|
|
52
|
+
const J = [
|
|
53
|
+
"free",
|
|
54
|
+
"square",
|
|
55
|
+
// Landscape
|
|
56
|
+
"16:9",
|
|
57
|
+
"4:3",
|
|
58
|
+
"3:2",
|
|
59
|
+
"5:4",
|
|
60
|
+
"2:1",
|
|
61
|
+
// Portrait
|
|
62
|
+
"9:16",
|
|
63
|
+
"3:4",
|
|
64
|
+
"2:3",
|
|
65
|
+
"4:5",
|
|
66
|
+
"1:2"
|
|
67
|
+
], zt = [
|
|
68
|
+
"free",
|
|
69
|
+
"square",
|
|
70
|
+
"circle",
|
|
71
|
+
"rounded-rect"
|
|
72
|
+
];
|
|
73
|
+
function W(i) {
|
|
74
|
+
return typeof i != "string" ? !1 : zt.includes(i) ? !0 : ht(i) !== null;
|
|
75
|
+
}
|
|
76
|
+
function pt(i) {
|
|
77
|
+
if (i == null) return;
|
|
78
|
+
if (Array.isArray(i)) return i.filter(W);
|
|
79
|
+
if (typeof i != "string") return;
|
|
80
|
+
const t = i.trim();
|
|
81
|
+
if (t) {
|
|
82
|
+
if (t.startsWith("["))
|
|
83
|
+
try {
|
|
84
|
+
const e = JSON.parse(t);
|
|
85
|
+
if (Array.isArray(e)) return e.filter(W);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
return t.split(/[\s,]+/).filter(Boolean).filter(W);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const Rt = b`
|
|
92
|
+
:host {
|
|
93
|
+
/* Palette — light theme (matches ui-tw :root) */
|
|
94
|
+
--sfx-cr-primary: oklch(0.578 0.198 268.129);
|
|
95
|
+
--sfx-cr-primary-hover: oklch(0.5 0.198 268.129);
|
|
96
|
+
--sfx-cr-primary-mid: oklch(0.62 0.198 268.129);
|
|
97
|
+
--sfx-cr-primary-bg: oklch(0.578 0.198 268.129 / 0.07);
|
|
98
|
+
--sfx-cr-primary-glow: oklch(0.578 0.198 268.129 / 0.18);
|
|
99
|
+
|
|
100
|
+
--sfx-cr-success: oklch(0.637 0.17 151.295);
|
|
101
|
+
--sfx-cr-error: oklch(0.577 0.215 27.325);
|
|
102
|
+
|
|
103
|
+
--sfx-cr-text: oklch(0.37 0.022 248.413);
|
|
104
|
+
--sfx-cr-text-secondary: oklch(53.03% 0.039 249.89);
|
|
105
|
+
--sfx-cr-text-muted: oklch(0.685 0.033 249.82);
|
|
106
|
+
/* Fine-tilt ruler ink + halo. The ruler floats directly over the photo,
|
|
107
|
+
whose brightness is unknown, so its colour can't track the theme. We
|
|
108
|
+
render a bright (near-white) core wrapped in a dark halo: the white core
|
|
109
|
+
reads over dark images, the dark halo reads over bright ones — the same
|
|
110
|
+
trick subtitles use to stay legible over arbitrary footage. */
|
|
111
|
+
--sfx-cr-ruler-ink: oklch(1 0 0);
|
|
112
|
+
--sfx-cr-ruler-halo: oklch(0 0 0 / 0.85);
|
|
113
|
+
|
|
114
|
+
--sfx-cr-border: oklch(92.86% 0.009 247.92);
|
|
115
|
+
--sfx-cr-border-light: oklch(0.974 0.006 239.819);
|
|
116
|
+
|
|
117
|
+
--sfx-cr-bg: oklch(1 0 0);
|
|
118
|
+
--sfx-cr-surface: oklch(0.974 0.006 239.819);
|
|
119
|
+
--sfx-cr-canvas-bg: oklch(0.974 0.006 239.819);
|
|
120
|
+
/* Dimming overlay for pixels outside the crop rect. Light theme uses a
|
|
121
|
+
very soft, near-white tint so the whole surround stays bright; dark
|
|
122
|
+
theme keeps the classic black dim for contrast against the photo. */
|
|
123
|
+
--sfx-cr-overlay-color: oklch(1 0 0 / 0.52);
|
|
124
|
+
/* Crop frame + handle colors, theme-aware so the rectangle reads
|
|
125
|
+
against both a washed-out light background and a dimmed dark one. */
|
|
126
|
+
--sfx-cr-frame-color: oklch(0.37 0.022 248.413);
|
|
127
|
+
--sfx-cr-frame-shadow: oklch(1 0 0 / 0.7);
|
|
128
|
+
--sfx-cr-handle-fill: oklch(0.37 0.022 248.413);
|
|
129
|
+
--sfx-cr-handle-stroke: oklch(1 0 0 / 0.95);
|
|
130
|
+
|
|
131
|
+
--sfx-cr-ring: oklch(0.578 0.198 268.129 / 0.7);
|
|
132
|
+
--sfx-cr-shadow: oklch(26.18% 0.024 256.43 / 0.1);
|
|
133
|
+
|
|
134
|
+
/* Derived — kept for internal reuse */
|
|
135
|
+
--sfx-cr-toolbar-bg: oklch(1 0 0 / 0.85);
|
|
136
|
+
--sfx-cr-toolbar-color: var(--sfx-cr-text);
|
|
137
|
+
--sfx-cr-toolbar-border: oklch(92.86% 0.009 247.92 / 0.6);
|
|
138
|
+
/* shadow-sm + soft primary tint */
|
|
139
|
+
--sfx-cr-toolbar-shadow: 0 1px 3px 0 oklch(0 0 0 / 0.1), 0 1px 2px -1px oklch(0 0 0 / 0.1);
|
|
140
|
+
--sfx-cr-btn-size: 36px;
|
|
141
|
+
--sfx-cr-btn-radius: 6px;
|
|
142
|
+
--sfx-cr-btn-hover-bg: var(--sfx-cr-primary-bg);
|
|
143
|
+
--sfx-cr-btn-active-bg: oklch(0.578 0.198 268.129 / 0.14);
|
|
144
|
+
--sfx-cr-separator-color: var(--sfx-cr-border-light);
|
|
145
|
+
--sfx-cr-slider-track: var(--sfx-cr-border);
|
|
146
|
+
--sfx-cr-slider-fill: var(--sfx-cr-primary);
|
|
147
|
+
--sfx-cr-slider-thumb: var(--sfx-cr-primary);
|
|
148
|
+
/* Translucent so the dropdown picks up whatever sits behind it
|
|
149
|
+
(image, overlay) when paired with backdrop-filter. */
|
|
150
|
+
--sfx-cr-dropdown-bg: oklch(0.974 0.006 239.819 / 0.8);
|
|
151
|
+
--sfx-cr-dropdown-hover: var(--sfx-cr-primary-bg);
|
|
152
|
+
/* shadow-md + shadow-lg blend */
|
|
153
|
+
--sfx-cr-dropdown-shadow: 0 10px 15px -3px oklch(0 0 0 / 0.1), 0 4px 6px -4px oklch(0 0 0 / 0.1);
|
|
154
|
+
--sfx-cr-zoom-bar-bg: oklch(1 0 0 / 0.85);
|
|
155
|
+
--sfx-cr-transition: 0.15s ease;
|
|
156
|
+
--sfx-cr-font: "Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
157
|
+
|
|
158
|
+
/* Border-radius scale — mirrors ui-tw --radius-sm/md/lg/xl */
|
|
159
|
+
--sfx-cr-radius-sm: 4px;
|
|
160
|
+
--sfx-cr-radius-md: 6px;
|
|
161
|
+
--sfx-cr-radius-lg: 8px;
|
|
162
|
+
--sfx-cr-radius-xl: 12px;
|
|
163
|
+
/* Outer card (when <sfx-crop> fills the host) */
|
|
164
|
+
--sfx-cr-radius: var(--sfx-cr-radius-xl);
|
|
165
|
+
--sfx-cr-card-shadow: 0 28px 80px oklch(0 0 0 / 0.2), 0 4px 16px oklch(0 0 0 / 0.06);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/* Dark variant — mirrors ui-tw :root.dark. */
|
|
169
|
+
:host([theme="dark"]) {
|
|
170
|
+
--sfx-cr-primary: oklch(0.6 0.2 268.129);
|
|
171
|
+
--sfx-cr-primary-hover: oklch(0.55 0.2 268.129);
|
|
172
|
+
--sfx-cr-primary-mid: oklch(0.65 0.2 268.129);
|
|
173
|
+
--sfx-cr-primary-bg: oklch(0.6 0.2 268.129 / 0.07);
|
|
174
|
+
--sfx-cr-primary-glow: oklch(0.6 0.2 268.129 / 0.22);
|
|
175
|
+
|
|
176
|
+
--sfx-cr-success: oklch(0.6 0.2 154.83);
|
|
177
|
+
--sfx-cr-error: oklch(0.55 0.2 27.325);
|
|
178
|
+
|
|
179
|
+
--sfx-cr-text: oklch(0.95 0.01 264.55);
|
|
180
|
+
--sfx-cr-text-secondary: oklch(0.9 0.01 264.55);
|
|
181
|
+
--sfx-cr-text-muted: oklch(0.75 0.01 249.82);
|
|
182
|
+
/* Ruler keeps the white core + dark halo over the photo (see light theme);
|
|
183
|
+
--sfx-cr-ruler-halo is inherited from the base :host. */
|
|
184
|
+
--sfx-cr-ruler-ink: oklch(1 0 0);
|
|
185
|
+
|
|
186
|
+
--sfx-cr-border: oklch(0.3 0.01 247.92);
|
|
187
|
+
--sfx-cr-border-light: oklch(0.3 0.01 285);
|
|
188
|
+
|
|
189
|
+
--sfx-cr-bg: oklch(0.13 0.027 261.692);
|
|
190
|
+
--sfx-cr-surface: oklch(0.25 0.01 264.55);
|
|
191
|
+
--sfx-cr-canvas-bg: oklch(0.13 0.027 261.692);
|
|
192
|
+
--sfx-cr-overlay-color: oklch(0 0 0 / 0.35);
|
|
193
|
+
--sfx-cr-frame-color: oklch(0.95 0.01 264.55);
|
|
194
|
+
--sfx-cr-frame-shadow: oklch(0 0 0 / 0.6);
|
|
195
|
+
--sfx-cr-handle-fill: oklch(0.95 0.01 264.55);
|
|
196
|
+
--sfx-cr-handle-stroke: oklch(0 0 0 / 0.25);
|
|
197
|
+
|
|
198
|
+
--sfx-cr-ring: oklch(0.6 0.2 268.129 / 0.7);
|
|
199
|
+
--sfx-cr-shadow: oklch(0 0 0 / 0.2);
|
|
200
|
+
|
|
201
|
+
--sfx-cr-toolbar-bg: oklch(0.13 0.027 261.692 / 0.85);
|
|
202
|
+
--sfx-cr-toolbar-color: oklch(0.95 0.01 264.55);
|
|
203
|
+
--sfx-cr-toolbar-border: oklch(0.3 0.01 247.92 / 0.5);
|
|
204
|
+
--sfx-cr-toolbar-shadow: 0 4px 20px oklch(0 0 0 / 0.4);
|
|
205
|
+
|
|
206
|
+
--sfx-cr-btn-hover-bg: oklch(0.6 0.2 268.129 / 0.22);
|
|
207
|
+
--sfx-cr-btn-active-bg: oklch(0.6 0.2 268.129 / 0.32);
|
|
208
|
+
|
|
209
|
+
--sfx-cr-slider-track: oklch(0.3 0.01 247.92);
|
|
210
|
+
|
|
211
|
+
--sfx-cr-dropdown-bg: oklch(0.13 0.027 261.692 / 0.82);
|
|
212
|
+
--sfx-cr-dropdown-hover: oklch(0.6 0.2 268.129 / 0.22);
|
|
213
|
+
--sfx-cr-dropdown-shadow: 0 10px 15px -3px oklch(0 0 0 / 0.5), 0 4px 6px -4px oklch(0 0 0 / 0.3);
|
|
214
|
+
--sfx-cr-zoom-bar-bg: oklch(0.13 0.027 261.692 / 0.85);
|
|
215
|
+
|
|
216
|
+
--sfx-cr-card-shadow: 0 28px 80px oklch(0 0 0 / 0.55), 0 4px 16px oklch(0 0 0 / 0.2);
|
|
217
|
+
}
|
|
218
|
+
`, j = b`
|
|
219
|
+
.sfx-cr-sr-only {
|
|
220
|
+
position: absolute;
|
|
221
|
+
width: 1px;
|
|
222
|
+
height: 1px;
|
|
223
|
+
padding: 0;
|
|
224
|
+
margin: -1px;
|
|
225
|
+
overflow: hidden;
|
|
226
|
+
clip: rect(0, 0, 0, 0);
|
|
227
|
+
white-space: nowrap;
|
|
228
|
+
border: 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@media (prefers-reduced-motion: reduce) {
|
|
232
|
+
:host, :host *, :host *::before, :host *::after {
|
|
233
|
+
transition-duration: 0.01ms !important;
|
|
234
|
+
animation-duration: 0.01ms !important;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
`, Pt = b`
|
|
238
|
+
@keyframes sfx-cr-spin { to { transform: rotate(360deg); } }
|
|
239
|
+
`, It = b`
|
|
240
|
+
@keyframes sfx-cr-toolbar-enter {
|
|
241
|
+
from {
|
|
242
|
+
opacity: 0;
|
|
243
|
+
}
|
|
244
|
+
to {
|
|
245
|
+
opacity: 1;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
`;
|
|
249
|
+
b`
|
|
250
|
+
@keyframes sfx-cr-zoom-enter {
|
|
251
|
+
from {
|
|
252
|
+
opacity: 0;
|
|
253
|
+
transform: translateX(-50%) translateY(12px);
|
|
254
|
+
}
|
|
255
|
+
to {
|
|
256
|
+
opacity: 1;
|
|
257
|
+
transform: translateX(-50%) translateY(0);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
`;
|
|
261
|
+
b`
|
|
262
|
+
@keyframes sfx-cr-card-in {
|
|
263
|
+
from {
|
|
264
|
+
opacity: 0;
|
|
265
|
+
transform: translateY(10px) scale(0.98);
|
|
266
|
+
}
|
|
267
|
+
to {
|
|
268
|
+
opacity: 1;
|
|
269
|
+
transform: translateY(0) scale(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
`;
|
|
273
|
+
b`
|
|
274
|
+
input[type="range"]::-webkit-slider-thumb {
|
|
275
|
+
-webkit-appearance: none;
|
|
276
|
+
width: 16px;
|
|
277
|
+
height: 16px;
|
|
278
|
+
border-radius: 50%;
|
|
279
|
+
background: var(--sfx-cr-slider-thumb);
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
box-shadow: 0 1px 4px var(--sfx-cr-primary-glow);
|
|
282
|
+
transition: transform 150ms ease, box-shadow 150ms ease;
|
|
283
|
+
}
|
|
284
|
+
input[type="range"]::-webkit-slider-thumb:hover {
|
|
285
|
+
transform: scale(1.15);
|
|
286
|
+
box-shadow: 0 0 0 5px var(--sfx-cr-primary-glow);
|
|
287
|
+
}
|
|
288
|
+
input[type="range"]::-moz-range-thumb {
|
|
289
|
+
width: 16px;
|
|
290
|
+
height: 16px;
|
|
291
|
+
border-radius: 50%;
|
|
292
|
+
background: var(--sfx-cr-slider-thumb);
|
|
293
|
+
border: none;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
box-shadow: 0 1px 4px var(--sfx-cr-primary-glow);
|
|
296
|
+
}
|
|
297
|
+
`;
|
|
298
|
+
const Tt = b`
|
|
299
|
+
:host {
|
|
300
|
+
display: block;
|
|
301
|
+
position: relative;
|
|
302
|
+
width: 100%;
|
|
303
|
+
height: 100%;
|
|
304
|
+
font-family: var(--sfx-cr-font);
|
|
305
|
+
color: var(--sfx-cr-text);
|
|
306
|
+
/* Establish a named inline-size container so the toolbar / shapes /
|
|
307
|
+
zoom / rotate sub-elements can switch to their compact left-rail
|
|
308
|
+
layout based on the editor's own width — independent of the page
|
|
309
|
+
viewport. A narrow column on a wide desktop (sidebar preview,
|
|
310
|
+
split view) gets the same compact UI as a phone. */
|
|
311
|
+
container-type: inline-size;
|
|
312
|
+
container-name: sfxcrop;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
:host([hidden]) { display: none; }
|
|
316
|
+
|
|
317
|
+
:host(:focus-visible) {
|
|
318
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
319
|
+
outline-offset: 2px;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.sfx-cr-container {
|
|
323
|
+
position: relative;
|
|
324
|
+
overflow: hidden;
|
|
325
|
+
width: 100%;
|
|
326
|
+
height: 100%;
|
|
327
|
+
background: var(--sfx-cr-bg);
|
|
328
|
+
border-radius: var(--sfx-cr-radius);
|
|
329
|
+
/* Editor card frame: shadow-only (no border). A border draws two
|
|
330
|
+
visible curves at the corners (outer + inner padding edge),
|
|
331
|
+
reading as a "double ring"; a slightly stronger shadow gives the
|
|
332
|
+
same elevation feel with a single clean rounded silhouette. */
|
|
333
|
+
/* shadow-xs ring + shadow-md elevation, sourced from ui-tw scale */
|
|
334
|
+
box-shadow:
|
|
335
|
+
0 0 0 1px oklch(0 0 0 / 0.05),
|
|
336
|
+
0 4px 6px -1px oklch(0 0 0 / 0.1),
|
|
337
|
+
0 2px 4px -2px oklch(0 0 0 / 0.1);
|
|
338
|
+
font-family: var(--sfx-cr-font);
|
|
339
|
+
user-select: none;
|
|
340
|
+
-webkit-user-select: none;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* ====== Loading ====== */
|
|
344
|
+
.sfx-cr-loading {
|
|
345
|
+
position: absolute;
|
|
346
|
+
inset: 0;
|
|
347
|
+
display: flex;
|
|
348
|
+
flex-direction: column;
|
|
349
|
+
align-items: center;
|
|
350
|
+
justify-content: center;
|
|
351
|
+
gap: 16px;
|
|
352
|
+
background: var(--sfx-cr-bg);
|
|
353
|
+
z-index: 10;
|
|
354
|
+
transition: opacity 280ms ease;
|
|
355
|
+
}
|
|
356
|
+
.sfx-cr-loading--hidden {
|
|
357
|
+
opacity: 0;
|
|
358
|
+
pointer-events: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.sfx-cr-loading-spinner {
|
|
362
|
+
width: 36px;
|
|
363
|
+
height: 36px;
|
|
364
|
+
border: 3px solid var(--sfx-cr-border);
|
|
365
|
+
border-top-color: var(--sfx-cr-primary);
|
|
366
|
+
border-radius: 50%;
|
|
367
|
+
animation: sfx-cr-spin 0.8s linear infinite;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.sfx-cr-loading-text {
|
|
371
|
+
font-size: 14px;
|
|
372
|
+
font-weight: 500;
|
|
373
|
+
color: var(--sfx-cr-text-secondary);
|
|
374
|
+
letter-spacing: 0.2px;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ====== Error ====== */
|
|
378
|
+
.sfx-cr-error {
|
|
379
|
+
position: absolute;
|
|
380
|
+
inset: 0;
|
|
381
|
+
display: none;
|
|
382
|
+
flex-direction: column;
|
|
383
|
+
align-items: center;
|
|
384
|
+
justify-content: center;
|
|
385
|
+
gap: 12px;
|
|
386
|
+
padding: 32px;
|
|
387
|
+
background: var(--sfx-cr-bg);
|
|
388
|
+
z-index: 10;
|
|
389
|
+
color: var(--sfx-cr-error);
|
|
390
|
+
font-size: 14px;
|
|
391
|
+
font-weight: 500;
|
|
392
|
+
text-align: center;
|
|
393
|
+
}
|
|
394
|
+
.sfx-cr-error--visible { display: flex; }
|
|
395
|
+
`;
|
|
396
|
+
var Ot = Object.defineProperty, c = (i, t, e, o) => {
|
|
397
|
+
for (var r = void 0, a = i.length - 1, n; a >= 0; a--)
|
|
398
|
+
(n = i[a]) && (r = n(t, e, r) || r);
|
|
399
|
+
return r && Ot(t, e, r), r;
|
|
400
|
+
};
|
|
401
|
+
const Et = {
|
|
402
|
+
fromAttribute(i) {
|
|
403
|
+
return i === null ? "interaction" : i === "true" || i === "" ? !0 : i === "false" ? !1 : "interaction";
|
|
404
|
+
},
|
|
405
|
+
toAttribute(i) {
|
|
406
|
+
return i === !0 ? "true" : i === !1 ? "false" : "interaction";
|
|
407
|
+
}
|
|
408
|
+
}, tt = class tt extends N {
|
|
409
|
+
constructor() {
|
|
410
|
+
super(...arguments), this.src = "", this.cropShape = "16:9", this.theme = "light", this.initialRotation = 0, this.initialScale = 1, this.minScale = 0.5, this.maxScale = 5, this.minCropSize = 20, this.handleSize = 12, this.borderRadius = 20, this.outputQuality = 0.92, this.maxOutputWidth = 0, this.maxOutputHeight = 0, this.bleedMarginSize = 10, this.animationSpeed = 1, this.handleColor = "#ffffff", this.overlayColor = "rgba(0, 0, 0, 0.55)", this.bleedMarginColor = "rgba(255, 0, 0, 0.5)", this.outputType = "image/png", this.toolbarPosition = "top", this.variant = "classic", this.showGrid = "interaction", this.showToolbar = !0, this.showRotateSlider = !0, this.showZoomSlider = !0, this.showShapeSelector = !0, this.showRotateButton = !0, this.showFlipButton = !0, this.showBleedMargin = !1, this.enableAnimations = !0, this.keyboard = !0, this.pinchZoom = !0, this.wheelZoom = !0, this.availableShapes = [...J], this.initialCrop = null, this.icons = {}, this.loading = !1, this.errorMessage = null, this.controller = null, this.currentImage = null, this.parentResizeObserver = null, this.onToolbarCommand = (t) => {
|
|
411
|
+
if (!this.controller) return;
|
|
412
|
+
const e = t.detail;
|
|
413
|
+
switch (e.type) {
|
|
414
|
+
case "reset":
|
|
415
|
+
this.controller.reset();
|
|
416
|
+
break;
|
|
417
|
+
case "rotate-left":
|
|
418
|
+
this.controller.rotateLeft();
|
|
419
|
+
break;
|
|
420
|
+
case "flip-h":
|
|
421
|
+
this.controller.flipHorizontal();
|
|
422
|
+
break;
|
|
423
|
+
case "rotation":
|
|
424
|
+
this.controller.setRotation(e.value);
|
|
425
|
+
break;
|
|
426
|
+
case "scale":
|
|
427
|
+
this.controller.setScale(e.value);
|
|
428
|
+
break;
|
|
429
|
+
case "shape":
|
|
430
|
+
this.cropShape = e.value;
|
|
431
|
+
break;
|
|
432
|
+
case "save":
|
|
433
|
+
this.save();
|
|
434
|
+
break;
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
// === Lifecycle ===
|
|
439
|
+
async firstUpdated() {
|
|
440
|
+
if (wt(this), !(this.canvasHost instanceof D))
|
|
441
|
+
throw new Error(
|
|
442
|
+
"<sfx-crop>: custom elements not registered. Import '@scaleflex/crop/define' before using the tag."
|
|
443
|
+
);
|
|
444
|
+
if (await this.canvasHost.updateComplete, !this.isConnected) return;
|
|
445
|
+
const t = this.canvasHost.canvasEl, e = yt(this.buildConfig());
|
|
446
|
+
this.controller = kt({
|
|
447
|
+
canvas: t,
|
|
448
|
+
host: this,
|
|
449
|
+
// Pass the canvas host (which we size to the image's display rect)
|
|
450
|
+
// so the controller's ResizeObserver and renderer measure the tight
|
|
451
|
+
// photo box — not the outer column that also includes toolbars.
|
|
452
|
+
layoutContainer: this.canvasHost,
|
|
453
|
+
config: e,
|
|
454
|
+
callbacks: {
|
|
455
|
+
onReady: () => this.dispatch("sfx-crop-ready", { element: this }),
|
|
456
|
+
onImageLoad: (r) => {
|
|
457
|
+
this.currentImage = r, this.fitHostToImage(), this.dispatch("sfx-crop-image-load", { image: r });
|
|
458
|
+
},
|
|
459
|
+
onError: (r) => this.dispatch("sfx-crop-error", { error: r }),
|
|
460
|
+
onChange: (r) => {
|
|
461
|
+
this.dispatch("sfx-crop-change", r);
|
|
462
|
+
},
|
|
463
|
+
onCropChange: (r) => this.dispatch("sfx-crop-crop-change", r),
|
|
464
|
+
onRotationSync: (r) => {
|
|
465
|
+
var a;
|
|
466
|
+
return (a = this.toolbarHost) == null ? void 0 : a.setRotationValue(r);
|
|
467
|
+
},
|
|
468
|
+
onShapeSync: (r) => {
|
|
469
|
+
var a;
|
|
470
|
+
this.cropShape = r, (a = this.toolbarHost) == null || a.setShapeValue(r);
|
|
471
|
+
},
|
|
472
|
+
onScaleSync: () => {
|
|
473
|
+
},
|
|
474
|
+
onLoadingChange: (r, a) => {
|
|
475
|
+
this.loading = r, this.errorMessage = a;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
const o = this.parentElement;
|
|
480
|
+
o && (this.parentResizeObserver = new ResizeObserver(() => this.fitHostToImage()), this.parentResizeObserver.observe(o)), this.src && this.controller.loadImage(this.src);
|
|
481
|
+
}
|
|
482
|
+
/** @see LIVE_CONFIG_KEYS for the exact set forwarded to `controller.update()`. */
|
|
483
|
+
updated(t) {
|
|
484
|
+
if (!this.controller) return;
|
|
485
|
+
const e = {};
|
|
486
|
+
let o = !1;
|
|
487
|
+
for (const r of Mt)
|
|
488
|
+
t.has(r) && (e[r] = this[r], o = !0);
|
|
489
|
+
o && this.controller.update(e), (t.has("variant") || t.has("cropShape")) && this.fitHostToImage();
|
|
490
|
+
}
|
|
491
|
+
disconnectedCallback() {
|
|
492
|
+
var t, e;
|
|
493
|
+
super.disconnectedCallback(), (t = this.parentResizeObserver) == null || t.disconnect(), this.parentResizeObserver = null, (e = this.controller) == null || e.destroy(), this.controller = null;
|
|
494
|
+
}
|
|
495
|
+
render() {
|
|
496
|
+
return f`
|
|
497
|
+
<div class="sfx-cr-container" part="container">
|
|
498
|
+
<sfx-crop-canvas part="canvas-host"></sfx-crop-canvas>
|
|
499
|
+
${this.showToolbar ? f`
|
|
500
|
+
<sfx-crop-toolbar
|
|
501
|
+
part="toolbar"
|
|
502
|
+
.shape=${this.cropShape}
|
|
503
|
+
?show-rotate-button=${this.showRotateButton}
|
|
504
|
+
?show-flip-button=${this.showFlipButton}
|
|
505
|
+
?show-rotate-slider=${this.showRotateSlider}
|
|
506
|
+
?show-shape-selector=${this.showShapeSelector}
|
|
507
|
+
toolbar-position=${this.toolbarPosition}
|
|
508
|
+
variant=${this.variant}
|
|
509
|
+
.availableShapes=${this.availableShapes}
|
|
510
|
+
.icons=${this.icons}
|
|
511
|
+
@sfx-crop-toolbar-command=${this.onToolbarCommand}
|
|
512
|
+
></sfx-crop-toolbar>
|
|
513
|
+
` : null}
|
|
514
|
+
<div class=${at({ "sfx-cr-loading": !0, "sfx-cr-loading--hidden": !this.loading })} part="loading">
|
|
515
|
+
<div class="sfx-cr-loading-spinner"></div>
|
|
516
|
+
<div class="sfx-cr-loading-text">Loading…</div>
|
|
517
|
+
</div>
|
|
518
|
+
<div class=${at({ "sfx-cr-error": !0, "sfx-cr-error--visible": !!this.errorMessage })} part="error">
|
|
519
|
+
${this.errorMessage ?? "Failed to load image"}
|
|
520
|
+
</div>
|
|
521
|
+
</div>
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
// === Public imperative API ===
|
|
525
|
+
loadImage(t) {
|
|
526
|
+
return this.ensure().loadImage(t);
|
|
527
|
+
}
|
|
528
|
+
getTransformState() {
|
|
529
|
+
return this.ensure().getTransformState();
|
|
530
|
+
}
|
|
531
|
+
setCropShape(t) {
|
|
532
|
+
this.ensure(), this.cropShape = t;
|
|
533
|
+
}
|
|
534
|
+
setCropRect(t) {
|
|
535
|
+
this.ensure().setCropRect(t);
|
|
536
|
+
}
|
|
537
|
+
getCropRect() {
|
|
538
|
+
return this.ensure().getCropRect();
|
|
539
|
+
}
|
|
540
|
+
rotateLeft() {
|
|
541
|
+
this.ensure().rotateLeft();
|
|
542
|
+
}
|
|
543
|
+
flipHorizontal() {
|
|
544
|
+
this.ensure().flipHorizontal();
|
|
545
|
+
}
|
|
546
|
+
setRotation(t) {
|
|
547
|
+
this.ensure().setRotation(t);
|
|
548
|
+
}
|
|
549
|
+
setScale(t) {
|
|
550
|
+
this.ensure().setScale(t);
|
|
551
|
+
}
|
|
552
|
+
reset() {
|
|
553
|
+
this.ensure().reset();
|
|
554
|
+
}
|
|
555
|
+
toCanvas() {
|
|
556
|
+
return this.ensure().toCanvas();
|
|
557
|
+
}
|
|
558
|
+
toBlob(t, e) {
|
|
559
|
+
return this.ensure().toBlob(t, e);
|
|
560
|
+
}
|
|
561
|
+
toDataURL(t, e) {
|
|
562
|
+
return this.ensure().toDataURL(t, e);
|
|
563
|
+
}
|
|
564
|
+
toTransformParams() {
|
|
565
|
+
return this.ensure().toTransformParams();
|
|
566
|
+
}
|
|
567
|
+
async save(t, e) {
|
|
568
|
+
const o = await this.toBlob(t, e), r = this.toDataURL(t, e), a = this.toTransformParams();
|
|
569
|
+
this.dispatch("sfx-crop-save", { blob: o, dataURL: r, params: a });
|
|
570
|
+
}
|
|
571
|
+
cancel() {
|
|
572
|
+
this.dispatch("sfx-crop-cancel", void 0);
|
|
573
|
+
}
|
|
574
|
+
// === Internals ===
|
|
575
|
+
ensure() {
|
|
576
|
+
if (!this.controller)
|
|
577
|
+
throw new Error('<sfx-crop> not connected — wait for "sfx-crop-ready" or firstUpdated().');
|
|
578
|
+
return this.controller;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Size both the canvas host (to the image's display rect, no letterbox)
|
|
582
|
+
* and the outer `<sfx-crop>` host (photo rect + measured toolbar stack).
|
|
583
|
+
*
|
|
584
|
+
* Bound order for `max-width` / `max-height`:
|
|
585
|
+
* 1. Consumer-provided values on the host's own CSS / inline style.
|
|
586
|
+
* 2. Parent's client rect as a fallback.
|
|
587
|
+
* 3. Viewport size as a last resort.
|
|
588
|
+
*
|
|
589
|
+
* Called on image-load, on state changes (90° rotation swaps aspect),
|
|
590
|
+
* and whenever the parent resizes.
|
|
591
|
+
*/
|
|
592
|
+
fitHostToImage() {
|
|
593
|
+
if (!this.currentImage || !this.isConnected || !this.canvasHost) return;
|
|
594
|
+
const t = this.currentImage.naturalWidth, e = this.currentImage.naturalHeight, o = this.style.width, r = this.style.height;
|
|
595
|
+
this.style.width = "", this.style.height = "";
|
|
596
|
+
const a = (m) => {
|
|
597
|
+
if (!m || m === "none") return Number.POSITIVE_INFINITY;
|
|
598
|
+
const O = _(m);
|
|
599
|
+
return Number.isFinite(O) && O > 0 ? O : Number.POSITIVE_INFINITY;
|
|
600
|
+
}, n = getComputedStyle(this);
|
|
601
|
+
let d = a(n.maxWidth), p = a(n.maxHeight);
|
|
602
|
+
const h = this.parentElement, g = h ? getComputedStyle(h) : null;
|
|
603
|
+
if (h && g && (g.display === "none" || h.clientWidth === 0)) {
|
|
604
|
+
this.style.width = o, this.style.height = r;
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const C = (() => {
|
|
608
|
+
if (!h || !g) return 0;
|
|
609
|
+
const m = h.clientWidth;
|
|
610
|
+
return Math.max(0, m - (_(g.paddingLeft) || 0) - (_(g.paddingRight) || 0));
|
|
611
|
+
})();
|
|
612
|
+
C > 0 ? d = Math.min(d, C) : Number.isFinite(d) || (d = window.innerWidth), Number.isFinite(p) || (p = window.innerHeight), this.style.width = o, this.style.height = r;
|
|
613
|
+
const I = d, T = p;
|
|
614
|
+
if (I <= 0 || T <= 0) return;
|
|
615
|
+
let L, F;
|
|
616
|
+
if (this.variant === "fixed") {
|
|
617
|
+
const m = this.fixedFrameAspect(I / T);
|
|
618
|
+
let O = I, Y = I / m;
|
|
619
|
+
Y > T && (Y = T, O = T * m), L = Math.floor(O), F = Math.floor(Y);
|
|
620
|
+
} else {
|
|
621
|
+
const m = Math.min(I / t, T / e, 1);
|
|
622
|
+
L = Math.floor(t * m), F = Math.floor(e * m);
|
|
623
|
+
}
|
|
624
|
+
const st = _(o), vt = _(r);
|
|
625
|
+
(Number.isNaN(st) || Math.abs(st - L) >= 1 || Math.abs(vt - F) >= 1) && (this.style.width = `${L}px`, this.style.height = `${F}px`);
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Aspect ratio (W/H) of the fixed-variant editor box for the current shape.
|
|
629
|
+
* `free` fills the host (uses the available-area aspect); `square` / `circle`
|
|
630
|
+
* / `rounded-rect` are square; ratio shapes use their parsed ratio.
|
|
631
|
+
*/
|
|
632
|
+
fixedFrameAspect(t) {
|
|
633
|
+
const e = this.cropShape;
|
|
634
|
+
if (e === "free") return t;
|
|
635
|
+
if (e === "rounded-rect") return 1;
|
|
636
|
+
const o = q(e);
|
|
637
|
+
return o && o > 0 ? o : t;
|
|
638
|
+
}
|
|
639
|
+
dispatch(t, e) {
|
|
640
|
+
this.dispatchEvent(new CustomEvent(t, { detail: e, bubbles: !0, composed: !0 }));
|
|
641
|
+
}
|
|
642
|
+
parseInitialCrop() {
|
|
643
|
+
const t = this.initialCrop;
|
|
644
|
+
if (!t) return null;
|
|
645
|
+
const e = typeof t == "object" ? t : Nt(t);
|
|
646
|
+
if (!e || typeof e != "object") return null;
|
|
647
|
+
const o = e;
|
|
648
|
+
return typeof o.x != "number" || typeof o.y != "number" || typeof o.width != "number" || typeof o.height != "number" ? null : { x: o.x, y: o.y, width: o.width, height: o.height };
|
|
649
|
+
}
|
|
650
|
+
buildConfig() {
|
|
651
|
+
return {
|
|
652
|
+
src: this.src,
|
|
653
|
+
variant: this.variant,
|
|
654
|
+
cropShape: this.cropShape,
|
|
655
|
+
theme: this.theme,
|
|
656
|
+
initialRotation: this.initialRotation,
|
|
657
|
+
initialScale: this.initialScale,
|
|
658
|
+
initialCrop: this.parseInitialCrop(),
|
|
659
|
+
minScale: this.minScale,
|
|
660
|
+
maxScale: this.maxScale,
|
|
661
|
+
minCropSize: this.minCropSize,
|
|
662
|
+
availableShapes: pt(this.availableShapes) ?? [...J],
|
|
663
|
+
handleSize: this.handleSize,
|
|
664
|
+
handleColor: this.handleColor,
|
|
665
|
+
borderRadius: this.borderRadius,
|
|
666
|
+
outputType: this.outputType,
|
|
667
|
+
outputQuality: this.outputQuality,
|
|
668
|
+
maxOutputWidth: this.maxOutputWidth,
|
|
669
|
+
maxOutputHeight: this.maxOutputHeight,
|
|
670
|
+
overlayColor: this.overlayColor,
|
|
671
|
+
showGrid: this.showGrid,
|
|
672
|
+
showToolbar: this.showToolbar,
|
|
673
|
+
showRotateSlider: this.showRotateSlider,
|
|
674
|
+
showZoomSlider: this.showZoomSlider,
|
|
675
|
+
showShapeSelector: this.showShapeSelector,
|
|
676
|
+
showRotateButton: this.showRotateButton,
|
|
677
|
+
showFlipButton: this.showFlipButton,
|
|
678
|
+
toolbarPosition: this.toolbarPosition,
|
|
679
|
+
showBleedMargin: this.showBleedMargin,
|
|
680
|
+
bleedMarginSize: this.bleedMarginSize,
|
|
681
|
+
bleedMarginColor: this.bleedMarginColor,
|
|
682
|
+
enableAnimations: this.enableAnimations,
|
|
683
|
+
animationSpeed: this.animationSpeed,
|
|
684
|
+
keyboard: this.keyboard,
|
|
685
|
+
pinchZoom: this.pinchZoom,
|
|
686
|
+
wheelZoom: this.wheelZoom
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
};
|
|
690
|
+
tt.styles = [Rt, j, Pt, Tt];
|
|
691
|
+
let l = tt;
|
|
692
|
+
c([
|
|
693
|
+
s({ type: String, reflect: !0 })
|
|
694
|
+
], l.prototype, "src");
|
|
695
|
+
c([
|
|
696
|
+
s({ type: String, attribute: "crop-shape", reflect: !0 })
|
|
697
|
+
], l.prototype, "cropShape");
|
|
698
|
+
c([
|
|
699
|
+
s({ type: String, reflect: !0 })
|
|
700
|
+
], l.prototype, "theme");
|
|
701
|
+
c([
|
|
702
|
+
s({ type: Number, attribute: "initial-rotation" })
|
|
703
|
+
], l.prototype, "initialRotation");
|
|
704
|
+
c([
|
|
705
|
+
s({ type: Number, attribute: "initial-scale" })
|
|
706
|
+
], l.prototype, "initialScale");
|
|
707
|
+
c([
|
|
708
|
+
s({ type: Number, attribute: "min-scale" })
|
|
709
|
+
], l.prototype, "minScale");
|
|
710
|
+
c([
|
|
711
|
+
s({ type: Number, attribute: "max-scale" })
|
|
712
|
+
], l.prototype, "maxScale");
|
|
713
|
+
c([
|
|
714
|
+
s({ type: Number, attribute: "min-crop-size" })
|
|
715
|
+
], l.prototype, "minCropSize");
|
|
716
|
+
c([
|
|
717
|
+
s({ type: Number, attribute: "handle-size" })
|
|
718
|
+
], l.prototype, "handleSize");
|
|
719
|
+
c([
|
|
720
|
+
s({ type: Number, attribute: "border-radius" })
|
|
721
|
+
], l.prototype, "borderRadius");
|
|
722
|
+
c([
|
|
723
|
+
s({ type: Number, attribute: "output-quality" })
|
|
724
|
+
], l.prototype, "outputQuality");
|
|
725
|
+
c([
|
|
726
|
+
s({ type: Number, attribute: "max-output-width" })
|
|
727
|
+
], l.prototype, "maxOutputWidth");
|
|
728
|
+
c([
|
|
729
|
+
s({ type: Number, attribute: "max-output-height" })
|
|
730
|
+
], l.prototype, "maxOutputHeight");
|
|
731
|
+
c([
|
|
732
|
+
s({ type: Number, attribute: "bleed-margin-size" })
|
|
733
|
+
], l.prototype, "bleedMarginSize");
|
|
734
|
+
c([
|
|
735
|
+
s({ type: Number, attribute: "animation-speed" })
|
|
736
|
+
], l.prototype, "animationSpeed");
|
|
737
|
+
c([
|
|
738
|
+
s({ type: String, attribute: "handle-color" })
|
|
739
|
+
], l.prototype, "handleColor");
|
|
740
|
+
c([
|
|
741
|
+
s({ type: String, attribute: "overlay-color" })
|
|
742
|
+
], l.prototype, "overlayColor");
|
|
743
|
+
c([
|
|
744
|
+
s({ type: String, attribute: "bleed-margin-color" })
|
|
745
|
+
], l.prototype, "bleedMarginColor");
|
|
746
|
+
c([
|
|
747
|
+
s({ type: String, attribute: "output-type" })
|
|
748
|
+
], l.prototype, "outputType");
|
|
749
|
+
c([
|
|
750
|
+
s({ type: String, attribute: "toolbar-position", reflect: !0 })
|
|
751
|
+
], l.prototype, "toolbarPosition");
|
|
752
|
+
c([
|
|
753
|
+
s({ type: String, reflect: !0 })
|
|
754
|
+
], l.prototype, "variant");
|
|
755
|
+
c([
|
|
756
|
+
s({ attribute: "show-grid", converter: Et })
|
|
757
|
+
], l.prototype, "showGrid");
|
|
758
|
+
c([
|
|
759
|
+
s({ type: Boolean, attribute: "show-toolbar" })
|
|
760
|
+
], l.prototype, "showToolbar");
|
|
761
|
+
c([
|
|
762
|
+
s({ type: Boolean, attribute: "show-rotate-slider" })
|
|
763
|
+
], l.prototype, "showRotateSlider");
|
|
764
|
+
c([
|
|
765
|
+
s({ type: Boolean, attribute: "show-zoom-slider" })
|
|
766
|
+
], l.prototype, "showZoomSlider");
|
|
767
|
+
c([
|
|
768
|
+
s({ type: Boolean, attribute: "show-shape-selector" })
|
|
769
|
+
], l.prototype, "showShapeSelector");
|
|
770
|
+
c([
|
|
771
|
+
s({ type: Boolean, attribute: "show-rotate-button" })
|
|
772
|
+
], l.prototype, "showRotateButton");
|
|
773
|
+
c([
|
|
774
|
+
s({ type: Boolean, attribute: "show-flip-button" })
|
|
775
|
+
], l.prototype, "showFlipButton");
|
|
776
|
+
c([
|
|
777
|
+
s({ type: Boolean, attribute: "show-bleed-margin" })
|
|
778
|
+
], l.prototype, "showBleedMargin");
|
|
779
|
+
c([
|
|
780
|
+
s({ type: Boolean, attribute: "enable-animations" })
|
|
781
|
+
], l.prototype, "enableAnimations");
|
|
782
|
+
c([
|
|
783
|
+
s({ type: Boolean })
|
|
784
|
+
], l.prototype, "keyboard");
|
|
785
|
+
c([
|
|
786
|
+
s({ type: Boolean, attribute: "pinch-zoom" })
|
|
787
|
+
], l.prototype, "pinchZoom");
|
|
788
|
+
c([
|
|
789
|
+
s({ type: Boolean, attribute: "wheel-zoom" })
|
|
790
|
+
], l.prototype, "wheelZoom");
|
|
791
|
+
c([
|
|
792
|
+
s({ attribute: "available-shapes" })
|
|
793
|
+
], l.prototype, "availableShapes");
|
|
794
|
+
c([
|
|
795
|
+
s({ attribute: "initial-crop" })
|
|
796
|
+
], l.prototype, "initialCrop");
|
|
797
|
+
c([
|
|
798
|
+
s({ attribute: !1 })
|
|
799
|
+
], l.prototype, "icons");
|
|
800
|
+
c([
|
|
801
|
+
M()
|
|
802
|
+
], l.prototype, "loading");
|
|
803
|
+
c([
|
|
804
|
+
M()
|
|
805
|
+
], l.prototype, "errorMessage");
|
|
806
|
+
c([
|
|
807
|
+
E("sfx-crop-canvas")
|
|
808
|
+
], l.prototype, "canvasHost");
|
|
809
|
+
c([
|
|
810
|
+
E("sfx-crop-toolbar")
|
|
811
|
+
], l.prototype, "toolbarHost");
|
|
812
|
+
c([
|
|
813
|
+
E(".sfx-cr-container")
|
|
814
|
+
], l.prototype, "containerEl");
|
|
815
|
+
const Mt = [
|
|
816
|
+
"src",
|
|
817
|
+
"variant",
|
|
818
|
+
"cropShape",
|
|
819
|
+
"theme",
|
|
820
|
+
"minScale",
|
|
821
|
+
"maxScale",
|
|
822
|
+
"minCropSize",
|
|
823
|
+
"borderRadius",
|
|
824
|
+
"handleSize",
|
|
825
|
+
"handleColor",
|
|
826
|
+
"overlayColor",
|
|
827
|
+
"showGrid",
|
|
828
|
+
"showBleedMargin",
|
|
829
|
+
"bleedMarginSize",
|
|
830
|
+
"bleedMarginColor",
|
|
831
|
+
"enableAnimations",
|
|
832
|
+
"animationSpeed",
|
|
833
|
+
"keyboard",
|
|
834
|
+
"pinchZoom",
|
|
835
|
+
"wheelZoom"
|
|
836
|
+
];
|
|
837
|
+
function _(i) {
|
|
838
|
+
if (!i) return NaN;
|
|
839
|
+
const t = parseFloat(i);
|
|
840
|
+
return Number.isFinite(t) ? /^-?\d*\.?\d+(?:px)?$/.test(i.trim()) ? t : NaN : NaN;
|
|
841
|
+
}
|
|
842
|
+
function Nt(i) {
|
|
843
|
+
try {
|
|
844
|
+
return JSON.parse(i);
|
|
845
|
+
} catch {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const At = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 9V7a2 2 0 0 0-2-2h-6"/><path d="m15 8-3-3 3-3"/><path d="M4 14a2 2 0 0 0-2 2v3a2 2 0 0 0 2 2h13a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2Z"/></svg>', Bt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h3"/><path d="M16 3h3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-3"/><path d="M12 20v2"/><path d="M12 15v2"/><path d="M12 10v2"/><path d="M12 5v2"/></svg>', dt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 2v14a2 2 0 0 0 2 2h14"/><path d="M18 22V8a2 2 0 0 0-2-2H2"/></svg>', _t = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="20" height="14" x="2" y="3" rx="2"/><line x1="8" x2="16" y1="21" y2="21"/><line x1="12" x2="12" y1="17" y2="21"/></svg>', Dt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="14" height="20" x="5" y="2" rx="2" ry="2"/><path d="M12 18h.01"/></svg>', ft = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/></svg>', xt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="5"/></svg>', jt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="12" height="20" x="6" y="2" rx="2"/><rect width="20" height="12" x="2" y="6" rx="2"/></svg>', Ht = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>', Lt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 22h20"/><rect x="3" y="6" width="18" height="14" rx="2" transform="rotate(-10 12 13)"/></svg>', Ft = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/></svg>', Vt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="8" x2="14" y1="11" y2="11"/></svg>', Yt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" x2="16.65" y1="21" y2="16.65"/><line x1="11" x2="11" y1="8" y2="14"/><line x1="8" x2="14" y1="11" y2="11"/></svg>', Wt = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/></svg>', u = {
|
|
850
|
+
rotateLeft: "",
|
|
851
|
+
// populated below via a second block to avoid TDZ on const order
|
|
852
|
+
flipHorizontal: "",
|
|
853
|
+
tilt: "",
|
|
854
|
+
loupe: "",
|
|
855
|
+
zoomIn: "",
|
|
856
|
+
zoomOut: "",
|
|
857
|
+
cropAspect: "",
|
|
858
|
+
cropCustom: "",
|
|
859
|
+
cropCircle: "",
|
|
860
|
+
cropRoundedRect: "",
|
|
861
|
+
orientLandscape: "",
|
|
862
|
+
orientPortrait: "",
|
|
863
|
+
chevronDown: "",
|
|
864
|
+
reset: ""
|
|
865
|
+
};
|
|
866
|
+
u.rotateLeft = At;
|
|
867
|
+
u.flipHorizontal = Bt;
|
|
868
|
+
u.tilt = Lt;
|
|
869
|
+
u.loupe = Yt;
|
|
870
|
+
u.zoomIn = Ft;
|
|
871
|
+
u.zoomOut = Vt;
|
|
872
|
+
u.cropAspect = jt;
|
|
873
|
+
u.cropCustom = dt;
|
|
874
|
+
u.cropCircle = ft;
|
|
875
|
+
u.cropRoundedRect = xt;
|
|
876
|
+
u.orientLandscape = _t;
|
|
877
|
+
u.orientPortrait = Dt;
|
|
878
|
+
u.chevronDown = Ht;
|
|
879
|
+
u.reset = Wt;
|
|
880
|
+
function w(i, t) {
|
|
881
|
+
const e = t == null ? void 0 : t[i];
|
|
882
|
+
return typeof e == "string" && e.length > 0 ? e : u[i];
|
|
883
|
+
}
|
|
884
|
+
const Xt = b`
|
|
885
|
+
:host {
|
|
886
|
+
/* Inline-flex so the host doesn't take toolbar space — the actual ruler
|
|
887
|
+
below uses position:fixed and anchors to the canvas bottom-center via
|
|
888
|
+
createPopoverAnchor, exactly where the legacy popover lived. */
|
|
889
|
+
position: relative;
|
|
890
|
+
display: inline-flex;
|
|
891
|
+
align-items: center;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.sfx-cr-rotate-root {
|
|
895
|
+
position: relative;
|
|
896
|
+
display: flex;
|
|
897
|
+
align-items: center;
|
|
898
|
+
justify-content: center;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
.sfx-cr-rotate-popover {
|
|
902
|
+
position: fixed;
|
|
903
|
+
top: var(--sfx-cr-popover-top, 50%);
|
|
904
|
+
left: var(--sfx-cr-popover-left, 50%);
|
|
905
|
+
transform: translateX(-50%);
|
|
906
|
+
display: flex;
|
|
907
|
+
flex-direction: column;
|
|
908
|
+
align-items: center;
|
|
909
|
+
gap: 4px;
|
|
910
|
+
padding: 0;
|
|
911
|
+
background: transparent;
|
|
912
|
+
border: none;
|
|
913
|
+
box-shadow: none;
|
|
914
|
+
pointer-events: auto;
|
|
915
|
+
white-space: nowrap;
|
|
916
|
+
z-index: 10;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/* Ruler viewport — fixed width, overflow clipped. Ticks scroll inside. */
|
|
920
|
+
.sfx-cr-rotate-ruler {
|
|
921
|
+
position: relative;
|
|
922
|
+
width: var(--ruler-w, 260px);
|
|
923
|
+
height: 30px;
|
|
924
|
+
overflow: hidden;
|
|
925
|
+
cursor: grab;
|
|
926
|
+
touch-action: none;
|
|
927
|
+
user-select: none;
|
|
928
|
+
-webkit-user-select: none;
|
|
929
|
+
}
|
|
930
|
+
.sfx-cr-rotate-ruler.is-dragging { cursor: grabbing; }
|
|
931
|
+
.sfx-cr-rotate-ruler:focus-visible {
|
|
932
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
933
|
+
outline-offset: 2px;
|
|
934
|
+
border-radius: 4px;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/* Tick strip — absolutely positioned ticks, the whole strip is
|
|
938
|
+
translated along X in sync with the current value. */
|
|
939
|
+
.sfx-cr-rotate-ticks {
|
|
940
|
+
position: absolute;
|
|
941
|
+
top: 50%;
|
|
942
|
+
left: 0;
|
|
943
|
+
height: 100%;
|
|
944
|
+
will-change: transform;
|
|
945
|
+
/* Dark halo wrapping every tick so the white ink stays legible over a
|
|
946
|
+
bright photo. Applied to the strip so all ticks share one shadow pass. */
|
|
947
|
+
filter:
|
|
948
|
+
drop-shadow(0 0 1px var(--sfx-cr-ruler-halo, oklch(0 0 0 / 0.85)))
|
|
949
|
+
drop-shadow(0 0 2px var(--sfx-cr-ruler-halo, oklch(0 0 0 / 0.85)));
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
.sfx-cr-rotate-tick {
|
|
953
|
+
position: absolute;
|
|
954
|
+
top: 50%;
|
|
955
|
+
width: 1px;
|
|
956
|
+
height: 8px;
|
|
957
|
+
margin-left: -0.5px;
|
|
958
|
+
margin-top: -4px;
|
|
959
|
+
border-radius: 0.5px;
|
|
960
|
+
background: var(--sfx-cr-ruler-ink, var(--sfx-cr-text));
|
|
961
|
+
opacity: 0.9;
|
|
962
|
+
}
|
|
963
|
+
.sfx-cr-rotate-tick--major {
|
|
964
|
+
width: 1px;
|
|
965
|
+
height: 12px;
|
|
966
|
+
margin-left: -0.5px;
|
|
967
|
+
margin-top: -6px;
|
|
968
|
+
opacity: 1;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/* Fixed center indicator — single vertical line, slightly taller than ticks. */
|
|
972
|
+
.sfx-cr-rotate-indicator {
|
|
973
|
+
position: absolute;
|
|
974
|
+
top: calc(50% + 4px);
|
|
975
|
+
height: 16px;
|
|
976
|
+
left: 50%;
|
|
977
|
+
width: 4px;
|
|
978
|
+
margin-left: -2px;
|
|
979
|
+
background: var(--sfx-cr-ruler-ink, var(--sfx-cr-text));
|
|
980
|
+
border-radius: 2px;
|
|
981
|
+
pointer-events: none;
|
|
982
|
+
/* Same dark halo as the ticks so the centre marker stays visible. */
|
|
983
|
+
filter:
|
|
984
|
+
drop-shadow(0 0 1px var(--sfx-cr-ruler-halo, oklch(0 0 0 / 0.85)))
|
|
985
|
+
drop-shadow(0 0 2px var(--sfx-cr-ruler-halo, oklch(0 0 0 / 0.85)));
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.sfx-cr-rotate-value {
|
|
989
|
+
font-size: 14px;
|
|
990
|
+
font-weight: 400;
|
|
991
|
+
color: var(--sfx-cr-ruler-ink, var(--sfx-cr-text));
|
|
992
|
+
text-align: center;
|
|
993
|
+
font-variant-numeric: tabular-nums;
|
|
994
|
+
letter-spacing: 0.2px;
|
|
995
|
+
/* Dark halo so the degree readout reads over both bright and dark photos. */
|
|
996
|
+
text-shadow:
|
|
997
|
+
0 0 2px var(--sfx-cr-ruler-halo, oklch(0 0 0 / 0.85)),
|
|
998
|
+
0 1px 2px var(--sfx-cr-ruler-halo, oklch(0 0 0 / 0.85));
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
@media (max-width: 768px) {
|
|
1002
|
+
.sfx-cr-rotate-ruler { width: 220px; height: 22px; }
|
|
1003
|
+
.sfx-cr-rotate-tick { height: 6px; margin-top: -3px; }
|
|
1004
|
+
.sfx-cr-rotate-tick--major { height: 9px; margin-top: -4.5px; }
|
|
1005
|
+
.sfx-cr-rotate-indicator {
|
|
1006
|
+
top: calc(50% + 3px);
|
|
1007
|
+
height: 12px;
|
|
1008
|
+
width: 3px;
|
|
1009
|
+
margin-left: -1.5px;
|
|
1010
|
+
}
|
|
1011
|
+
.sfx-cr-rotate-value { font-size: 12px; }
|
|
1012
|
+
.sfx-cr-rotate-popover {
|
|
1013
|
+
transform: translateX(-50%) translateY(24px);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
@media (max-width: 480px) {
|
|
1017
|
+
.sfx-cr-rotate-ruler { width: 180px; height: 20px; }
|
|
1018
|
+
.sfx-cr-rotate-tick { height: 5px; margin-top: -2.5px; }
|
|
1019
|
+
.sfx-cr-rotate-tick--major { height: 8px; margin-top: -4px; }
|
|
1020
|
+
.sfx-cr-rotate-indicator {
|
|
1021
|
+
top: calc(50% + 2px);
|
|
1022
|
+
height: 10px;
|
|
1023
|
+
}
|
|
1024
|
+
.sfx-cr-rotate-value { font-size: 11px; }
|
|
1025
|
+
.sfx-cr-rotate-popover {
|
|
1026
|
+
transform: translateX(-50%) translateY(30px);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
/* Narrow editor — make the ruler more compact when the toolbar
|
|
1031
|
+
collapses into the vertical left rail. */
|
|
1032
|
+
@container sfxcrop (max-width: 760px) {
|
|
1033
|
+
.sfx-cr-rotate-ruler { width: 140px; height: 18px; }
|
|
1034
|
+
.sfx-cr-rotate-tick { height: 5px; margin-top: -2.5px; }
|
|
1035
|
+
.sfx-cr-rotate-tick--major { height: 8px; margin-top: -4px; }
|
|
1036
|
+
.sfx-cr-rotate-indicator {
|
|
1037
|
+
top: calc(50% + 2px);
|
|
1038
|
+
height: 10px;
|
|
1039
|
+
width: 3px;
|
|
1040
|
+
margin-left: -1.5px;
|
|
1041
|
+
}
|
|
1042
|
+
.sfx-cr-rotate-value { font-size: 11px; }
|
|
1043
|
+
}
|
|
1044
|
+
`, Ut = 72;
|
|
1045
|
+
function ut(i, t) {
|
|
1046
|
+
let e = null, o = !1;
|
|
1047
|
+
const r = () => {
|
|
1048
|
+
var g;
|
|
1049
|
+
let h = i;
|
|
1050
|
+
for (let z = 0; z < 6; z++) {
|
|
1051
|
+
const C = h.getRootNode();
|
|
1052
|
+
if (!(C instanceof ShadowRoot)) break;
|
|
1053
|
+
if (h = C.host, ((g = h.tagName) == null ? void 0 : g.toLowerCase()) === "sfx-crop") return h;
|
|
1054
|
+
}
|
|
1055
|
+
return null;
|
|
1056
|
+
}, a = () => {
|
|
1057
|
+
var C;
|
|
1058
|
+
const h = (C = i.shadowRoot) == null ? void 0 : C.querySelector(t), g = r();
|
|
1059
|
+
if (!h || !g) return;
|
|
1060
|
+
const z = g.getBoundingClientRect();
|
|
1061
|
+
h.style.setProperty("--sfx-cr-popover-left", `${z.left + z.width / 2}px`), h.style.setProperty("--sfx-cr-popover-top", `${z.bottom - Ut}px`);
|
|
1062
|
+
}, n = () => a();
|
|
1063
|
+
return { start: () => {
|
|
1064
|
+
if (o) return;
|
|
1065
|
+
o = !0, a(), window.addEventListener("resize", n), window.addEventListener("scroll", n, !0);
|
|
1066
|
+
const h = r();
|
|
1067
|
+
h && typeof ResizeObserver < "u" && (e = new ResizeObserver(() => a()), e.observe(h));
|
|
1068
|
+
}, stop: () => {
|
|
1069
|
+
o && (o = !1, window.removeEventListener("resize", n), window.removeEventListener("scroll", n, !0), e == null || e.disconnect(), e = null);
|
|
1070
|
+
}, update: a };
|
|
1071
|
+
}
|
|
1072
|
+
var Zt = Object.defineProperty, H = (i, t, e, o) => {
|
|
1073
|
+
for (var r = void 0, a = i.length - 1, n; a >= 0; a--)
|
|
1074
|
+
(n = i[a]) && (r = n(t, e, r) || r);
|
|
1075
|
+
return r && Zt(t, e, r), r;
|
|
1076
|
+
};
|
|
1077
|
+
const V = 6, X = 260, U = 1, Kt = 5, et = class et extends N {
|
|
1078
|
+
constructor() {
|
|
1079
|
+
super(...arguments), this.value = 0, this.min = -45, this.max = 45, this.icons = {}, this.dragging = !1, this.activePointer = null, this.pointerStartX = 0, this.pointerStartValue = 0, this.popoverAnchor = ut(this, ".sfx-cr-rotate-popover"), this.onKeyDown = (t) => {
|
|
1080
|
+
t.key === "ArrowLeft" ? (t.preventDefault(), this.emit(R(this.value - (t.shiftKey ? 5 : 1), this.min, this.max))) : t.key === "ArrowRight" && (t.preventDefault(), this.emit(R(this.value + (t.shiftKey ? 5 : 1), this.min, this.max)));
|
|
1081
|
+
}, this.onPointerDown = (t) => {
|
|
1082
|
+
if (this.activePointer === null) {
|
|
1083
|
+
this.activePointer = t.pointerId, this.pointerStartX = t.clientX, this.pointerStartValue = this.value, this.dragging = !0;
|
|
1084
|
+
try {
|
|
1085
|
+
t.currentTarget.setPointerCapture(t.pointerId);
|
|
1086
|
+
} catch {
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}, this.onPointerMove = (t) => {
|
|
1090
|
+
if (this.activePointer !== t.pointerId) return;
|
|
1091
|
+
const e = t.clientX - this.pointerStartX, o = this.pointerStartValue - e / V;
|
|
1092
|
+
this.emit(R(o, this.min, this.max));
|
|
1093
|
+
}, this.onPointerUp = (t) => {
|
|
1094
|
+
if (this.activePointer === t.pointerId) {
|
|
1095
|
+
this.activePointer = null, this.dragging = !1;
|
|
1096
|
+
try {
|
|
1097
|
+
t.currentTarget.releasePointerCapture(t.pointerId);
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
Math.abs(this.value) < 2 && this.emit(0);
|
|
1101
|
+
}
|
|
1102
|
+
}, this.onReset = () => {
|
|
1103
|
+
this.emit(0);
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
connectedCallback() {
|
|
1107
|
+
super.connectedCallback(), this.popoverAnchor.start();
|
|
1108
|
+
}
|
|
1109
|
+
disconnectedCallback() {
|
|
1110
|
+
super.disconnectedCallback(), this.popoverAnchor.stop();
|
|
1111
|
+
}
|
|
1112
|
+
updated(t) {
|
|
1113
|
+
(t.has("value") || t.has("dragging")) && this.popoverAnchor.update();
|
|
1114
|
+
}
|
|
1115
|
+
render() {
|
|
1116
|
+
const t = `${this.value > 0 ? "+" : ""}${this.value.toFixed(1)}°`, e = X / 2 - (this.value - this.min) * V, o = X / V, r = 4, a = Math.max(this.min, Math.floor(this.value - o / 2 - r)), n = Math.min(this.max, Math.ceil(this.value + o / 2 + r)), d = [];
|
|
1117
|
+
for (let p = Math.ceil(a / U) * U; p <= n; p += U)
|
|
1118
|
+
d.push({ deg: p, major: p % Kt === 0 });
|
|
1119
|
+
return f`
|
|
1120
|
+
<div class="sfx-cr-rotate-root" @keydown=${this.onKeyDown}>
|
|
1121
|
+
<div class="sfx-cr-rotate-popover" role="group" aria-label="Fine rotation">
|
|
1122
|
+
<div
|
|
1123
|
+
class=${`sfx-cr-rotate-ruler${this.dragging ? " is-dragging" : ""}`}
|
|
1124
|
+
role="slider"
|
|
1125
|
+
aria-valuemin=${String(this.min)}
|
|
1126
|
+
aria-valuemax=${String(this.max)}
|
|
1127
|
+
aria-valuenow=${String(this.value)}
|
|
1128
|
+
aria-valuetext=${t}
|
|
1129
|
+
tabindex="0"
|
|
1130
|
+
style=${`--ruler-w: ${X}px`}
|
|
1131
|
+
@pointerdown=${this.onPointerDown}
|
|
1132
|
+
@pointermove=${this.onPointerMove}
|
|
1133
|
+
@pointerup=${this.onPointerUp}
|
|
1134
|
+
@pointercancel=${this.onPointerUp}
|
|
1135
|
+
@dblclick=${this.onReset}
|
|
1136
|
+
>
|
|
1137
|
+
<div class="sfx-cr-rotate-ticks" style=${`transform: translateX(${e.toFixed(1)}px)`}>
|
|
1138
|
+
${d.map((p) => f`
|
|
1139
|
+
<span
|
|
1140
|
+
class=${`sfx-cr-rotate-tick${p.major ? " sfx-cr-rotate-tick--major" : ""}`}
|
|
1141
|
+
style=${`left: ${((p.deg - this.min) * V).toFixed(1)}px`}
|
|
1142
|
+
></span>
|
|
1143
|
+
`)}
|
|
1144
|
+
</div>
|
|
1145
|
+
<div class="sfx-cr-rotate-indicator" aria-hidden="true"></div>
|
|
1146
|
+
</div>
|
|
1147
|
+
<span class="sfx-cr-rotate-value">${t}</span>
|
|
1148
|
+
</div>
|
|
1149
|
+
</div>
|
|
1150
|
+
`;
|
|
1151
|
+
}
|
|
1152
|
+
/** Sync value without emitting — used by host to reflect controller state. */
|
|
1153
|
+
setValue(t) {
|
|
1154
|
+
this.value = R(t, this.min, this.max);
|
|
1155
|
+
}
|
|
1156
|
+
emit(t) {
|
|
1157
|
+
this.value !== t && (this.value = t, this.dispatchEvent(new CustomEvent("sfx-crop-rotate-change", {
|
|
1158
|
+
detail: { degrees: t },
|
|
1159
|
+
bubbles: !0,
|
|
1160
|
+
composed: !0
|
|
1161
|
+
})));
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
et.styles = [j, Xt];
|
|
1165
|
+
let $ = et;
|
|
1166
|
+
H([
|
|
1167
|
+
s({ type: Number })
|
|
1168
|
+
], $.prototype, "value");
|
|
1169
|
+
H([
|
|
1170
|
+
s({ type: Number })
|
|
1171
|
+
], $.prototype, "min");
|
|
1172
|
+
H([
|
|
1173
|
+
s({ type: Number })
|
|
1174
|
+
], $.prototype, "max");
|
|
1175
|
+
H([
|
|
1176
|
+
s({ attribute: !1 })
|
|
1177
|
+
], $.prototype, "icons");
|
|
1178
|
+
H([
|
|
1179
|
+
M()
|
|
1180
|
+
], $.prototype, "dragging");
|
|
1181
|
+
const Gt = b`
|
|
1182
|
+
:host {
|
|
1183
|
+
position: relative;
|
|
1184
|
+
display: inline-block;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
.sfx-cr-shape-trigger {
|
|
1188
|
+
display: flex;
|
|
1189
|
+
align-items: center;
|
|
1190
|
+
gap: 6px;
|
|
1191
|
+
padding: 8px 14px;
|
|
1192
|
+
min-width: 84px;
|
|
1193
|
+
height: 36px;
|
|
1194
|
+
background: transparent;
|
|
1195
|
+
color: var(--sfx-cr-text-secondary);
|
|
1196
|
+
border: 1.5px solid var(--sfx-cr-border);
|
|
1197
|
+
border-radius: 50px;
|
|
1198
|
+
cursor: pointer;
|
|
1199
|
+
font-family: var(--sfx-cr-font);
|
|
1200
|
+
font-size: 14px;
|
|
1201
|
+
font-weight: 500;
|
|
1202
|
+
transition:
|
|
1203
|
+
background var(--sfx-cr-transition),
|
|
1204
|
+
border-color var(--sfx-cr-transition),
|
|
1205
|
+
color var(--sfx-cr-transition),
|
|
1206
|
+
transform var(--sfx-cr-transition),
|
|
1207
|
+
box-shadow var(--sfx-cr-transition);
|
|
1208
|
+
white-space: nowrap;
|
|
1209
|
+
letter-spacing: 0.1px;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
/* Fixed variant: translucent pill so the trigger reads over the photo
|
|
1213
|
+
(no dimmed crop mask behind it). Plain background — no filter/transform. */
|
|
1214
|
+
:host([variant="fixed"]) .sfx-cr-shape-trigger {
|
|
1215
|
+
background: var(--sfx-cr-overlay-color);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
.sfx-cr-shape-trigger:hover {
|
|
1219
|
+
border-color: var(--sfx-cr-primary);
|
|
1220
|
+
transform: translateY(-1px);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
.sfx-cr-shape-trigger:focus-visible {
|
|
1224
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
1225
|
+
outline-offset: 2px;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
.sfx-cr-shape-trigger-icon {
|
|
1229
|
+
display: flex;
|
|
1230
|
+
width: 20px;
|
|
1231
|
+
height: 20px;
|
|
1232
|
+
}
|
|
1233
|
+
.sfx-cr-shape-trigger-icon svg { width: 100%; height: 100%; display: block; }
|
|
1234
|
+
|
|
1235
|
+
.sfx-cr-shape-trigger-label { line-height: 1; }
|
|
1236
|
+
|
|
1237
|
+
.sfx-cr-shape-chevron {
|
|
1238
|
+
display: flex;
|
|
1239
|
+
width: 14px;
|
|
1240
|
+
height: 14px;
|
|
1241
|
+
margin-left: auto;
|
|
1242
|
+
transition: transform var(--sfx-cr-transition);
|
|
1243
|
+
}
|
|
1244
|
+
.sfx-cr-shape-chevron svg { width: 100%; height: 100%; display: block; }
|
|
1245
|
+
|
|
1246
|
+
:host([open]) .sfx-cr-shape-chevron {
|
|
1247
|
+
transform: rotate(180deg);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
.sfx-cr-shape-dropdown {
|
|
1251
|
+
position: absolute;
|
|
1252
|
+
top: calc(100% + 8px);
|
|
1253
|
+
right: 0;
|
|
1254
|
+
/* Size to the widest option + orientation toggle. Clamp to avoid
|
|
1255
|
+
spilling off the viewport on very narrow screens. */
|
|
1256
|
+
width: max-content;
|
|
1257
|
+
min-width: 88px;
|
|
1258
|
+
max-width: min(92vw, 200px);
|
|
1259
|
+
max-height: min(55vh, 360px);
|
|
1260
|
+
padding: 4px;
|
|
1261
|
+
/* Barely-there grey tint with translucency so the panel picks up
|
|
1262
|
+
whatever sits behind it (image, dark overlay, etc.) without reading
|
|
1263
|
+
as a solid white box. backdrop-filter blur keeps text crisp on
|
|
1264
|
+
busy backgrounds. */
|
|
1265
|
+
background: var(--sfx-cr-dropdown-bg);
|
|
1266
|
+
border: 1px solid var(--sfx-cr-border);
|
|
1267
|
+
border-radius: var(--sfx-cr-radius-lg, 8px);
|
|
1268
|
+
box-shadow: var(--sfx-cr-dropdown-shadow);
|
|
1269
|
+
backdrop-filter: blur(14px) saturate(160%);
|
|
1270
|
+
-webkit-backdrop-filter: blur(14px) saturate(160%);
|
|
1271
|
+
display: flex;
|
|
1272
|
+
flex-direction: column;
|
|
1273
|
+
z-index: 100;
|
|
1274
|
+
opacity: 0;
|
|
1275
|
+
transform: translateY(6px) scale(0.96);
|
|
1276
|
+
pointer-events: none;
|
|
1277
|
+
transition: opacity 120ms ease-in, transform 120ms ease-in;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/* Orientation toggle — naked icon-only buttons centered at the top.
|
|
1281
|
+
No border, no background: only the rectangle SVG is visible. Active
|
|
1282
|
+
state is signalled by the icon's color (text) vs. inactive (muted). */
|
|
1283
|
+
.sfx-cr-shape-orient {
|
|
1284
|
+
display: flex;
|
|
1285
|
+
justify-content: center;
|
|
1286
|
+
gap: 8px;
|
|
1287
|
+
margin-bottom: 4px;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
.sfx-cr-shape-orient-btn {
|
|
1291
|
+
display: flex;
|
|
1292
|
+
align-items: center;
|
|
1293
|
+
justify-content: center;
|
|
1294
|
+
width: 36px;
|
|
1295
|
+
height: 32px;
|
|
1296
|
+
padding: 0;
|
|
1297
|
+
background: transparent;
|
|
1298
|
+
color: var(--sfx-cr-text-muted);
|
|
1299
|
+
border: none;
|
|
1300
|
+
border-radius: 4px;
|
|
1301
|
+
cursor: pointer;
|
|
1302
|
+
transition: color var(--sfx-cr-transition);
|
|
1303
|
+
}
|
|
1304
|
+
.sfx-cr-shape-orient-btn:hover {
|
|
1305
|
+
color: var(--sfx-cr-text-secondary);
|
|
1306
|
+
}
|
|
1307
|
+
.sfx-cr-shape-orient-btn.is-active {
|
|
1308
|
+
color: var(--sfx-cr-text-secondary);
|
|
1309
|
+
}
|
|
1310
|
+
.sfx-cr-shape-orient-btn svg {
|
|
1311
|
+
width: 24px;
|
|
1312
|
+
height: 24px;
|
|
1313
|
+
/* display:block kills SVG's default baseline drop (inline elements sit
|
|
1314
|
+
on the text baseline, leaving a few sub-pixels of descender space at
|
|
1315
|
+
the bottom — enough to push a tightly-fitted icon visibly off-center).
|
|
1316
|
+
The -1px translate is optical correction: both Lucide monitor (stand
|
|
1317
|
+
under the screen) and smartphone (home-indicator dot near the bottom)
|
|
1318
|
+
carry visual weight below their geometric centre. */
|
|
1319
|
+
display: block;
|
|
1320
|
+
transform: translateY(-1px);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
.sfx-cr-shape-list {
|
|
1324
|
+
flex: 1 1 auto;
|
|
1325
|
+
display: flex;
|
|
1326
|
+
flex-direction: column;
|
|
1327
|
+
gap: 1px;
|
|
1328
|
+
overflow-y: auto;
|
|
1329
|
+
scrollbar-width: thin;
|
|
1330
|
+
}
|
|
1331
|
+
.sfx-cr-shape-list::-webkit-scrollbar {
|
|
1332
|
+
width: 4px;
|
|
1333
|
+
}
|
|
1334
|
+
.sfx-cr-shape-list::-webkit-scrollbar-thumb {
|
|
1335
|
+
background: var(--sfx-cr-border);
|
|
1336
|
+
border-radius: 2px;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
:host([open]) .sfx-cr-shape-dropdown {
|
|
1340
|
+
opacity: 1;
|
|
1341
|
+
transform: translateY(0) scale(1);
|
|
1342
|
+
pointer-events: auto;
|
|
1343
|
+
transition: opacity 220ms cubic-bezier(0.34, 1.2, 0.64, 1),
|
|
1344
|
+
transform 220ms cubic-bezier(0.34, 1.2, 0.64, 1);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
.sfx-cr-shape-option {
|
|
1348
|
+
display: flex;
|
|
1349
|
+
align-items: center;
|
|
1350
|
+
gap: 8px;
|
|
1351
|
+
/* Stretch to the dropdown's resolved (content-sized) width so all
|
|
1352
|
+
rows share the same left edge + hover highlight. */
|
|
1353
|
+
width: 100%;
|
|
1354
|
+
padding: 5px 10px;
|
|
1355
|
+
height: 30px;
|
|
1356
|
+
background: transparent;
|
|
1357
|
+
color: var(--sfx-cr-text-secondary);
|
|
1358
|
+
border: none;
|
|
1359
|
+
border-radius: var(--sfx-cr-radius-sm, 4px);
|
|
1360
|
+
cursor: pointer;
|
|
1361
|
+
font-family: var(--sfx-cr-font);
|
|
1362
|
+
font-size: 14px;
|
|
1363
|
+
font-weight: 500;
|
|
1364
|
+
text-align: left;
|
|
1365
|
+
transition: background var(--sfx-cr-transition), color var(--sfx-cr-transition);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
.sfx-cr-shape-option:hover {
|
|
1369
|
+
background: var(--sfx-cr-dropdown-hover);
|
|
1370
|
+
color: var(--sfx-cr-primary);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
.sfx-cr-shape-option:focus-visible {
|
|
1374
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
1375
|
+
outline-offset: -2px;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.sfx-cr-shape-option--active {
|
|
1379
|
+
background: var(--sfx-cr-primary-bg);
|
|
1380
|
+
color: var(--sfx-cr-primary);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
.sfx-cr-shape-option-icon {
|
|
1384
|
+
display: flex;
|
|
1385
|
+
align-items: center;
|
|
1386
|
+
justify-content: center;
|
|
1387
|
+
width: 18px;
|
|
1388
|
+
height: 18px;
|
|
1389
|
+
color: var(--sfx-cr-text-secondary);
|
|
1390
|
+
flex-shrink: 0;
|
|
1391
|
+
}
|
|
1392
|
+
.sfx-cr-shape-option-icon svg { width: 100%; height: 100%; display: block; }
|
|
1393
|
+
.sfx-cr-shape-option--active .sfx-cr-shape-option-icon { color: var(--sfx-cr-primary); }
|
|
1394
|
+
|
|
1395
|
+
@media (max-width: 768px) {
|
|
1396
|
+
/* Drop label + chevron and shrink to a square icon-only pill so
|
|
1397
|
+
the shape icon sits dead-center. The base 84px min-width was
|
|
1398
|
+
sized for the desktop "[icon] Aspect 16:9 ▾" layout and leaves
|
|
1399
|
+
empty pill space on the right once those pieces are hidden. */
|
|
1400
|
+
.sfx-cr-shape-trigger-label { display: none; }
|
|
1401
|
+
.sfx-cr-shape-chevron { display: none; }
|
|
1402
|
+
.sfx-cr-shape-trigger {
|
|
1403
|
+
min-width: 0;
|
|
1404
|
+
width: 36px;
|
|
1405
|
+
padding: 0;
|
|
1406
|
+
gap: 0;
|
|
1407
|
+
justify-content: center;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/* Narrow editor: drop the textual label + chevron so the trigger
|
|
1412
|
+
reduces to an icon-only 30×30 capsule that matches the rest of
|
|
1413
|
+
the compact left-rail toolbar. Anchor the dropdown to the left
|
|
1414
|
+
edge since the trigger now lives in the left column. Container
|
|
1415
|
+
query keyed off the sfxcrop named container so a narrow editor on
|
|
1416
|
+
a wide desktop also collapses. */
|
|
1417
|
+
@container sfxcrop (max-width: 760px) {
|
|
1418
|
+
/* Label/chevron also need hiding when only the editor (not viewport)
|
|
1419
|
+
is narrow — the 768px @media above only triggers on small viewports. */
|
|
1420
|
+
.sfx-cr-shape-trigger-label { display: none; }
|
|
1421
|
+
.sfx-cr-shape-chevron { display: none; }
|
|
1422
|
+
.sfx-cr-shape-trigger {
|
|
1423
|
+
min-width: 0;
|
|
1424
|
+
width: 30px;
|
|
1425
|
+
height: 30px;
|
|
1426
|
+
padding: 0;
|
|
1427
|
+
gap: 0;
|
|
1428
|
+
justify-content: center;
|
|
1429
|
+
}
|
|
1430
|
+
/* Shrink the icon SLOT (not just the SVG) and center its contents,
|
|
1431
|
+
so the 16×16 SVG sits dead-center inside the 30×30 trigger. The
|
|
1432
|
+
base .sfx-cr-shape-trigger-icon is 20×20 with display:flex but
|
|
1433
|
+
no justify/align — leaving a 16×16 SVG inside top-left aligned. */
|
|
1434
|
+
.sfx-cr-shape-trigger-icon {
|
|
1435
|
+
width: 16px;
|
|
1436
|
+
height: 16px;
|
|
1437
|
+
align-items: center;
|
|
1438
|
+
justify-content: center;
|
|
1439
|
+
}
|
|
1440
|
+
.sfx-cr-shape-trigger svg {
|
|
1441
|
+
width: 16px;
|
|
1442
|
+
height: 16px;
|
|
1443
|
+
}
|
|
1444
|
+
/* Fixed variant: match the toolbar's uniform 40×40 round icon buttons. */
|
|
1445
|
+
:host([variant="fixed"]) .sfx-cr-shape-trigger {
|
|
1446
|
+
width: 40px;
|
|
1447
|
+
height: 40px;
|
|
1448
|
+
}
|
|
1449
|
+
/* Classic left-rail only: the trigger sits in the vertical rail, so the
|
|
1450
|
+
dropdown opens to the SIDE (right of the trigger). Excluded from the
|
|
1451
|
+
fixed variant — there the toolbar is a horizontal top bar, so the menu
|
|
1452
|
+
must keep the default downward opening (below the trigger) to avoid
|
|
1453
|
+
running off the screen edge. */
|
|
1454
|
+
:host(:not([variant="fixed"])) .sfx-cr-shape-dropdown {
|
|
1455
|
+
right: auto;
|
|
1456
|
+
left: calc(100% + 6px);
|
|
1457
|
+
top: auto;
|
|
1458
|
+
bottom: -30px;
|
|
1459
|
+
transform: translateY(6px) scale(0.96);
|
|
1460
|
+
}
|
|
1461
|
+
:host(:not([variant="fixed"])[open]) .sfx-cr-shape-dropdown {
|
|
1462
|
+
transform: translateY(0) scale(1);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
`;
|
|
1466
|
+
var qt = Object.defineProperty, P = (i, t, e, o) => {
|
|
1467
|
+
for (var r = void 0, a = i.length - 1, n; a >= 0; a--)
|
|
1468
|
+
(n = i[a]) && (r = n(t, e, r) || r);
|
|
1469
|
+
return r && qt(t, e, r), r;
|
|
1470
|
+
};
|
|
1471
|
+
function gt(i) {
|
|
1472
|
+
let r, a;
|
|
1473
|
+
i >= 1 ? (r = 18, a = 18 / i) : (a = 18, r = 18 * i);
|
|
1474
|
+
const n = Math.max(0, r - 1.5), d = Math.max(0, a - 1.5), p = (22 - n) / 2, h = (22 - d) / 2, g = Math.min(3.5, Math.min(n, d) / 4);
|
|
1475
|
+
return `<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" fill="none" stroke="currentColor" stroke-width="${1.5}"><rect x="${p.toFixed(2)}" y="${h.toFixed(2)}" width="${n.toFixed(2)}" height="${d.toFixed(2)}" rx="${g.toFixed(2)}"/></svg>`;
|
|
1476
|
+
}
|
|
1477
|
+
function Jt() {
|
|
1478
|
+
return {
|
|
1479
|
+
free: { value: "free", label: "Custom", icon: dt, show: "both" },
|
|
1480
|
+
circle: { value: "circle", label: "Circle", icon: ft, show: "both" },
|
|
1481
|
+
"rounded-rect": { value: "rounded-rect", label: "Rounded", icon: xt, show: "both" },
|
|
1482
|
+
square: { value: "square", label: "1:1", icon: gt(1), show: "both" }
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
const bt = Jt();
|
|
1486
|
+
function Z(i) {
|
|
1487
|
+
const t = bt[i];
|
|
1488
|
+
if (t) return t;
|
|
1489
|
+
const e = ht(i);
|
|
1490
|
+
return e === null ? null : {
|
|
1491
|
+
value: i,
|
|
1492
|
+
label: i,
|
|
1493
|
+
icon: gt(e),
|
|
1494
|
+
show: e > 1 ? "landscape" : e < 1 ? "portrait" : "both"
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
const rt = class rt extends N {
|
|
1498
|
+
constructor() {
|
|
1499
|
+
super(...arguments), this.value = "free", this.shapes = [
|
|
1500
|
+
"free",
|
|
1501
|
+
"square",
|
|
1502
|
+
"16:9",
|
|
1503
|
+
"4:3",
|
|
1504
|
+
"3:2",
|
|
1505
|
+
"5:4",
|
|
1506
|
+
"2:1",
|
|
1507
|
+
"9:16",
|
|
1508
|
+
"3:4",
|
|
1509
|
+
"2:3",
|
|
1510
|
+
"4:5",
|
|
1511
|
+
"1:2"
|
|
1512
|
+
], this.open = !1, this.variant = "classic", this.icons = {}, this.orientation = "landscape", this.focusedIndex = -1, this.focusRafId = null, this.docClickHandler = () => {
|
|
1513
|
+
this.open && this.close();
|
|
1514
|
+
}, this.onTriggerClick = (t) => {
|
|
1515
|
+
if (t.stopPropagation(), this.open)
|
|
1516
|
+
this.close();
|
|
1517
|
+
else {
|
|
1518
|
+
const e = this.getVisibleOptions();
|
|
1519
|
+
this.focusedIndex = Math.max(0, e.findIndex((o) => o.value === this.value)), this.open = !0, this.dispatchEvent(new CustomEvent("sfx-crop-popover-open", {
|
|
1520
|
+
detail: { source: "shapes" },
|
|
1521
|
+
bubbles: !0,
|
|
1522
|
+
composed: !0
|
|
1523
|
+
}));
|
|
1524
|
+
}
|
|
1525
|
+
}, this.onKeyDown = (t) => {
|
|
1526
|
+
if (!this.open) return;
|
|
1527
|
+
const e = this.getVisibleOptions();
|
|
1528
|
+
switch (t.key) {
|
|
1529
|
+
case "Escape":
|
|
1530
|
+
t.preventDefault(), t.stopPropagation(), this.close();
|
|
1531
|
+
break;
|
|
1532
|
+
case "ArrowDown":
|
|
1533
|
+
t.preventDefault(), t.stopPropagation(), this.focusOption(Math.min(this.focusedIndex + 1, e.length - 1));
|
|
1534
|
+
break;
|
|
1535
|
+
case "ArrowUp":
|
|
1536
|
+
t.preventDefault(), t.stopPropagation(), this.focusOption(Math.max(this.focusedIndex - 1, 0));
|
|
1537
|
+
break;
|
|
1538
|
+
case "ArrowLeft":
|
|
1539
|
+
t.preventDefault(), t.stopPropagation(), this.setOrientation("landscape");
|
|
1540
|
+
break;
|
|
1541
|
+
case "ArrowRight":
|
|
1542
|
+
t.preventDefault(), t.stopPropagation(), this.setOrientation("portrait");
|
|
1543
|
+
break;
|
|
1544
|
+
case "Enter":
|
|
1545
|
+
case " ":
|
|
1546
|
+
if (t.preventDefault(), t.stopPropagation(), this.focusedIndex >= 0 && this.focusedIndex < e.length) {
|
|
1547
|
+
const o = e[this.focusedIndex].value;
|
|
1548
|
+
this.value = o, this.close(), this.emit(o);
|
|
1549
|
+
}
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
connectedCallback() {
|
|
1555
|
+
super.connectedCallback(), document.addEventListener("click", this.docClickHandler);
|
|
1556
|
+
}
|
|
1557
|
+
disconnectedCallback() {
|
|
1558
|
+
super.disconnectedCallback(), document.removeEventListener("click", this.docClickHandler), this.focusRafId !== null && (cancelAnimationFrame(this.focusRafId), this.focusRafId = null);
|
|
1559
|
+
}
|
|
1560
|
+
updated(t) {
|
|
1561
|
+
if (t.has("open") && this.open) {
|
|
1562
|
+
const e = Z(this.value);
|
|
1563
|
+
e && e.show !== "both" && (this.orientation = e.show), this.focusRafId = requestAnimationFrame(() => {
|
|
1564
|
+
this.focusRafId = null;
|
|
1565
|
+
const r = this.getVisibleOptions().findIndex((a) => a.value === this.value);
|
|
1566
|
+
this.focusOption(r >= 0 ? r : 0);
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
render() {
|
|
1571
|
+
const t = this.getVisibleOptions(), e = Z(this.value) ?? bt.free;
|
|
1572
|
+
return f`
|
|
1573
|
+
<div
|
|
1574
|
+
@keydown=${this.onKeyDown}
|
|
1575
|
+
@click=${(o) => o.stopPropagation()}
|
|
1576
|
+
>
|
|
1577
|
+
<button
|
|
1578
|
+
type="button"
|
|
1579
|
+
class="sfx-cr-shape-trigger"
|
|
1580
|
+
aria-label="Select crop shape"
|
|
1581
|
+
aria-haspopup="listbox"
|
|
1582
|
+
aria-expanded=${this.open ? "true" : "false"}
|
|
1583
|
+
@click=${this.onTriggerClick}
|
|
1584
|
+
>
|
|
1585
|
+
<span class="sfx-cr-shape-trigger-icon">${k(w("cropAspect", this.icons))}</span>
|
|
1586
|
+
<span class="sfx-cr-shape-trigger-label">${e.label}</span>
|
|
1587
|
+
<span class="sfx-cr-shape-chevron">${k(w("chevronDown", this.icons))}</span>
|
|
1588
|
+
</button>
|
|
1589
|
+
<div class="sfx-cr-shape-dropdown" role="listbox">
|
|
1590
|
+
<div class="sfx-cr-shape-orient" role="tablist" aria-label="Orientation">
|
|
1591
|
+
<button
|
|
1592
|
+
type="button"
|
|
1593
|
+
class=${`sfx-cr-shape-orient-btn${this.orientation === "landscape" ? " is-active" : ""}`}
|
|
1594
|
+
role="tab"
|
|
1595
|
+
aria-selected=${this.orientation === "landscape" ? "true" : "false"}
|
|
1596
|
+
aria-label="Landscape orientations"
|
|
1597
|
+
@click=${() => this.setOrientation("landscape")}
|
|
1598
|
+
>${k(w("orientLandscape", this.icons))}</button>
|
|
1599
|
+
<button
|
|
1600
|
+
type="button"
|
|
1601
|
+
class=${`sfx-cr-shape-orient-btn${this.orientation === "portrait" ? " is-active" : ""}`}
|
|
1602
|
+
role="tab"
|
|
1603
|
+
aria-selected=${this.orientation === "portrait" ? "true" : "false"}
|
|
1604
|
+
aria-label="Portrait orientations"
|
|
1605
|
+
@click=${() => this.setOrientation("portrait")}
|
|
1606
|
+
>${k(w("orientPortrait", this.icons))}</button>
|
|
1607
|
+
</div>
|
|
1608
|
+
<div class="sfx-cr-shape-list" role="presentation">
|
|
1609
|
+
${t.map((o, r) => f`
|
|
1610
|
+
<button
|
|
1611
|
+
type="button"
|
|
1612
|
+
class=${`sfx-cr-shape-option${o.value === this.value ? " sfx-cr-shape-option--active" : ""}`}
|
|
1613
|
+
role="option"
|
|
1614
|
+
aria-selected=${o.value === this.value ? "true" : "false"}
|
|
1615
|
+
@click=${(a) => this.onOptionClick(a, o.value)}
|
|
1616
|
+
data-index=${String(r)}
|
|
1617
|
+
>
|
|
1618
|
+
<span class="sfx-cr-shape-option-icon">${k(this.optionIcon(o))}</span>
|
|
1619
|
+
<span class="sfx-cr-shape-option-label">${o.label}</span>
|
|
1620
|
+
</button>
|
|
1621
|
+
`)}
|
|
1622
|
+
</div>
|
|
1623
|
+
</div>
|
|
1624
|
+
</div>
|
|
1625
|
+
`;
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Pick the icon for a dropdown option, consulting consumer overrides
|
|
1629
|
+
* for the three non-ratio geometry shapes. Aspect-ratio options stay
|
|
1630
|
+
* on their proportional rectangle — not overridable, and that's
|
|
1631
|
+
* intentional since each one is generated from the ratio itself.
|
|
1632
|
+
*/
|
|
1633
|
+
optionIcon(t) {
|
|
1634
|
+
switch (t.value) {
|
|
1635
|
+
case "free":
|
|
1636
|
+
return w("cropCustom", this.icons);
|
|
1637
|
+
case "circle":
|
|
1638
|
+
return w("cropCircle", this.icons);
|
|
1639
|
+
case "rounded-rect":
|
|
1640
|
+
return w("cropRoundedRect", this.icons);
|
|
1641
|
+
default:
|
|
1642
|
+
return t.icon;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
/** Sync value without emitting — used by host. */
|
|
1646
|
+
setValue(t) {
|
|
1647
|
+
this.value = t;
|
|
1648
|
+
}
|
|
1649
|
+
setOrientation(t) {
|
|
1650
|
+
this.orientation !== t && (this.orientation = t, this.focusedIndex = -1);
|
|
1651
|
+
}
|
|
1652
|
+
/**
|
|
1653
|
+
* Options visible under the current orientation tab, respecting the
|
|
1654
|
+
* `shapes` allow-list. Each allowed name is resolved via `resolveShape`
|
|
1655
|
+
* so consumer-supplied `"W:H"` strings (not in SHAPE_MAP) get a dynamic
|
|
1656
|
+
* option with a proportional icon. Sorted: named shapes first in a
|
|
1657
|
+
* fixed order, then ratios by numeric value.
|
|
1658
|
+
*/
|
|
1659
|
+
getVisibleOptions() {
|
|
1660
|
+
const t = [];
|
|
1661
|
+
for (const e of this.shapes) {
|
|
1662
|
+
const o = Z(e);
|
|
1663
|
+
o && (o.show !== "both" && o.show !== this.orientation || t.push(o));
|
|
1664
|
+
}
|
|
1665
|
+
return t.sort((e, o) => {
|
|
1666
|
+
const r = { free: 0, circle: 1, "rounded-rect": 2, square: 3 }, a = r[e.value], n = r[o.value];
|
|
1667
|
+
if (a !== void 0 && n !== void 0) return a - n;
|
|
1668
|
+
if (a !== void 0) return -1;
|
|
1669
|
+
if (n !== void 0) return 1;
|
|
1670
|
+
const d = q(e.value) ?? 1, p = q(o.value) ?? 1;
|
|
1671
|
+
return this.orientation === "portrait" ? p - d : d - p;
|
|
1672
|
+
});
|
|
1673
|
+
}
|
|
1674
|
+
emit(t) {
|
|
1675
|
+
this.dispatchEvent(new CustomEvent("sfx-crop-shape-change", {
|
|
1676
|
+
detail: { shape: t },
|
|
1677
|
+
bubbles: !0,
|
|
1678
|
+
composed: !0
|
|
1679
|
+
}));
|
|
1680
|
+
}
|
|
1681
|
+
close() {
|
|
1682
|
+
this.open = !1;
|
|
1683
|
+
}
|
|
1684
|
+
onOptionClick(t, e) {
|
|
1685
|
+
t.stopPropagation(), this.value = e, this.close(), this.emit(e);
|
|
1686
|
+
}
|
|
1687
|
+
focusOption(t) {
|
|
1688
|
+
this.focusedIndex = t;
|
|
1689
|
+
const e = this.renderRoot.querySelector(
|
|
1690
|
+
`.sfx-cr-shape-option[data-index="${t}"]`
|
|
1691
|
+
);
|
|
1692
|
+
e == null || e.focus();
|
|
1693
|
+
}
|
|
1694
|
+
};
|
|
1695
|
+
rt.styles = [j, Gt];
|
|
1696
|
+
let y = rt;
|
|
1697
|
+
P([
|
|
1698
|
+
s({ type: String })
|
|
1699
|
+
], y.prototype, "value");
|
|
1700
|
+
P([
|
|
1701
|
+
s({ attribute: !1 })
|
|
1702
|
+
], y.prototype, "shapes");
|
|
1703
|
+
P([
|
|
1704
|
+
s({ type: Boolean, reflect: !0 })
|
|
1705
|
+
], y.prototype, "open");
|
|
1706
|
+
P([
|
|
1707
|
+
s({ type: String, reflect: !0 })
|
|
1708
|
+
], y.prototype, "variant");
|
|
1709
|
+
P([
|
|
1710
|
+
s({ attribute: !1 })
|
|
1711
|
+
], y.prototype, "icons");
|
|
1712
|
+
P([
|
|
1713
|
+
M()
|
|
1714
|
+
], y.prototype, "orientation");
|
|
1715
|
+
P([
|
|
1716
|
+
M()
|
|
1717
|
+
], y.prototype, "focusedIndex");
|
|
1718
|
+
const Qt = b`
|
|
1719
|
+
:host {
|
|
1720
|
+
position: absolute;
|
|
1721
|
+
top: 16px;
|
|
1722
|
+
left: 0;
|
|
1723
|
+
right: 0;
|
|
1724
|
+
display: flex;
|
|
1725
|
+
justify-content: center;
|
|
1726
|
+
z-index: 5;
|
|
1727
|
+
animation: sfx-cr-toolbar-enter 300ms ease forwards;
|
|
1728
|
+
pointer-events: none;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
:host([toolbar-position="bottom"]) {
|
|
1732
|
+
top: auto;
|
|
1733
|
+
bottom: 16px;
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
/* Fixed variant: the editor box IS the crop frame, so the floating bar sits
|
|
1737
|
+
directly over the photo. Classic gets its button contrast for free from
|
|
1738
|
+
the dimmed crop mask; fixed has none, so give each control its own
|
|
1739
|
+
translucent pill to stay legible over arbitrary image content.
|
|
1740
|
+
|
|
1741
|
+
IMPORTANT: backgrounds only — no backdrop-filter / transform / filter
|
|
1742
|
+
here or on any ancestor. Those create a containing block for
|
|
1743
|
+
position:fixed descendants, which would re-anchor the canvas-bottom ruler
|
|
1744
|
+
+ shape popover (both fixed-positioned) to the bar and knock the 45-deg
|
|
1745
|
+
tilt ruler off-screen. A plain background is safe. */
|
|
1746
|
+
:host([variant="fixed"]) .sfx-cr-toolbar,
|
|
1747
|
+
:host([variant="fixed"]) .sfx-cr-toolbar-group {
|
|
1748
|
+
/* Fixed variant runs a slightly tighter uniform gap than classic. */
|
|
1749
|
+
gap: 12px;
|
|
1750
|
+
}
|
|
1751
|
+
:host([variant="fixed"]) .sfx-cr-reset-btn,
|
|
1752
|
+
:host([variant="fixed"]) .sfx-cr-toolbar-btn {
|
|
1753
|
+
background: var(--sfx-cr-overlay-color);
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/* Primary "Done" action — pinned to the right edge of the toolbar strip,
|
|
1757
|
+
away from the centered control cluster. The host spans the full block
|
|
1758
|
+
width (left:0/right:0), so right:16px sits near the block's edge. */
|
|
1759
|
+
.sfx-cr-done-btn {
|
|
1760
|
+
position: absolute;
|
|
1761
|
+
right: 16px;
|
|
1762
|
+
top: 50%;
|
|
1763
|
+
transform: translateY(-50%);
|
|
1764
|
+
pointer-events: auto;
|
|
1765
|
+
display: inline-flex;
|
|
1766
|
+
align-items: center;
|
|
1767
|
+
justify-content: center;
|
|
1768
|
+
height: 36px;
|
|
1769
|
+
padding: 0 18px;
|
|
1770
|
+
background: var(--sfx-cr-primary);
|
|
1771
|
+
color: #fff;
|
|
1772
|
+
border: none;
|
|
1773
|
+
border-radius: 50px;
|
|
1774
|
+
cursor: pointer;
|
|
1775
|
+
font-family: var(--sfx-cr-font);
|
|
1776
|
+
font-size: 14px;
|
|
1777
|
+
font-weight: 600;
|
|
1778
|
+
letter-spacing: 0.1px;
|
|
1779
|
+
white-space: nowrap;
|
|
1780
|
+
box-shadow: var(--sfx-cr-toolbar-shadow);
|
|
1781
|
+
transition:
|
|
1782
|
+
background var(--sfx-cr-transition),
|
|
1783
|
+
transform var(--sfx-cr-transition);
|
|
1784
|
+
}
|
|
1785
|
+
.sfx-cr-done-btn:hover {
|
|
1786
|
+
background: var(--sfx-cr-primary-hover);
|
|
1787
|
+
transform: translateY(-50%) scale(1.02);
|
|
1788
|
+
}
|
|
1789
|
+
.sfx-cr-done-btn:active {
|
|
1790
|
+
transform: translateY(-50%) scale(0.97);
|
|
1791
|
+
}
|
|
1792
|
+
.sfx-cr-done-btn:focus-visible {
|
|
1793
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
1794
|
+
outline-offset: 2px;
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
/* Hole-punch pattern: the toolbar host is pointer-transparent and
|
|
1798
|
+
individual interactive children below opt back in. Gaps between
|
|
1799
|
+
buttons fall through to the canvas — important when a crop handle
|
|
1800
|
+
ends up under the floating toolbar and the user wants to grab it.
|
|
1801
|
+
The rotate / zoom / shapes sub-elements already follow the same
|
|
1802
|
+
pattern in their own stylesheets. */
|
|
1803
|
+
.sfx-cr-toolbar {
|
|
1804
|
+
display: flex;
|
|
1805
|
+
flex-direction: row;
|
|
1806
|
+
align-items: center;
|
|
1807
|
+
gap: 16px;
|
|
1808
|
+
padding: 4px 8px;
|
|
1809
|
+
background: transparent;
|
|
1810
|
+
color: var(--sfx-cr-toolbar-color);
|
|
1811
|
+
border: none;
|
|
1812
|
+
border-radius: 0;
|
|
1813
|
+
box-shadow: none;
|
|
1814
|
+
pointer-events: none;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
.sfx-cr-toolbar-group {
|
|
1818
|
+
display: flex;
|
|
1819
|
+
flex-direction: row;
|
|
1820
|
+
align-items: center;
|
|
1821
|
+
/* Match the toolbar's own gap so spacing is uniform across every button
|
|
1822
|
+
(Reset, rotate, flip, shapes), not tighter inside the group. */
|
|
1823
|
+
gap: 16px;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/* Sub-element hosts — same hole-punch opt-in as the plain buttons.
|
|
1827
|
+
Their hosts are tight inline-flex wrappers around a single trigger,
|
|
1828
|
+
so re-enabling them adds no extra dead zone. */
|
|
1829
|
+
sfx-crop-rotate,
|
|
1830
|
+
sfx-crop-shapes {
|
|
1831
|
+
pointer-events: auto;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
/* The fine-tilt ruler renders as a canvas-anchored fixed popover, but its
|
|
1835
|
+
inline host still occupies a zero-width slot in the toolbar row — which
|
|
1836
|
+
doubles the gap between the flip button and the shape selector. Pull the
|
|
1837
|
+
host out of flow so every visible button stays evenly spaced (both
|
|
1838
|
+
variants). The popover is position:fixed, so this doesn't move it. */
|
|
1839
|
+
sfx-crop-rotate {
|
|
1840
|
+
position: absolute;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
.sfx-cr-toolbar-btn {
|
|
1844
|
+
pointer-events: auto;
|
|
1845
|
+
width: 52px;
|
|
1846
|
+
height: 36px;
|
|
1847
|
+
display: flex;
|
|
1848
|
+
align-items: center;
|
|
1849
|
+
justify-content: center;
|
|
1850
|
+
background: transparent;
|
|
1851
|
+
color: var(--sfx-cr-text-secondary);
|
|
1852
|
+
/* Transparent default border → hover swaps in a colored ring without
|
|
1853
|
+
the layout shifting from 0 → 1 px. */
|
|
1854
|
+
border: 1px solid transparent;
|
|
1855
|
+
border-radius: 999px;
|
|
1856
|
+
cursor: pointer;
|
|
1857
|
+
padding: 0;
|
|
1858
|
+
transition:
|
|
1859
|
+
border-color var(--sfx-cr-transition),
|
|
1860
|
+
color var(--sfx-cr-transition),
|
|
1861
|
+
transform var(--sfx-cr-transition);
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
.sfx-cr-toolbar-btn:hover {
|
|
1865
|
+
border-color: var(--sfx-cr-primary);
|
|
1866
|
+
transform: translateY(-1px);
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
.sfx-cr-toolbar-btn:active {
|
|
1870
|
+
transform: translateY(0) scale(0.96);
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
.sfx-cr-toolbar-btn:disabled {
|
|
1874
|
+
opacity: 0.45;
|
|
1875
|
+
cursor: not-allowed;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
.sfx-cr-toolbar-btn:disabled:hover {
|
|
1879
|
+
border-color: transparent;
|
|
1880
|
+
color: var(--sfx-cr-text-muted);
|
|
1881
|
+
transform: none;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
.sfx-cr-toolbar-btn:focus-visible {
|
|
1885
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
1886
|
+
outline-offset: 2px;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
.sfx-cr-toolbar-btn svg {
|
|
1890
|
+
width: 20px;
|
|
1891
|
+
height: 20px;
|
|
1892
|
+
display: block;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
/* Reset pill — mirrors the shape-selector trigger visual language:
|
|
1896
|
+
capsule border, transparent fill, primary-blue tint on hover. Lives
|
|
1897
|
+
before every other control so the user can wipe back to the initial
|
|
1898
|
+
state in one click. */
|
|
1899
|
+
.sfx-cr-reset-btn {
|
|
1900
|
+
pointer-events: auto;
|
|
1901
|
+
display: flex;
|
|
1902
|
+
align-items: center;
|
|
1903
|
+
gap: 6px;
|
|
1904
|
+
padding: 8px 14px;
|
|
1905
|
+
min-width: 84px;
|
|
1906
|
+
height: 36px;
|
|
1907
|
+
background: transparent;
|
|
1908
|
+
color: var(--sfx-cr-text-secondary);
|
|
1909
|
+
border: 1.5px solid var(--sfx-cr-border);
|
|
1910
|
+
border-radius: 50px;
|
|
1911
|
+
cursor: pointer;
|
|
1912
|
+
font-family: var(--sfx-cr-font);
|
|
1913
|
+
font-size: 14px;
|
|
1914
|
+
font-weight: 500;
|
|
1915
|
+
letter-spacing: 0.1px;
|
|
1916
|
+
white-space: nowrap;
|
|
1917
|
+
transition:
|
|
1918
|
+
background var(--sfx-cr-transition),
|
|
1919
|
+
border-color var(--sfx-cr-transition),
|
|
1920
|
+
color var(--sfx-cr-transition),
|
|
1921
|
+
transform var(--sfx-cr-transition),
|
|
1922
|
+
box-shadow var(--sfx-cr-transition);
|
|
1923
|
+
}
|
|
1924
|
+
.sfx-cr-reset-btn:hover {
|
|
1925
|
+
border-color: var(--sfx-cr-primary);
|
|
1926
|
+
transform: translateY(-1px);
|
|
1927
|
+
}
|
|
1928
|
+
.sfx-cr-reset-btn:focus-visible {
|
|
1929
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
1930
|
+
outline-offset: 2px;
|
|
1931
|
+
}
|
|
1932
|
+
.sfx-cr-reset-btn svg {
|
|
1933
|
+
width: 18px;
|
|
1934
|
+
height: 18px;
|
|
1935
|
+
display: block;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
@media (max-width: 768px) {
|
|
1939
|
+
.sfx-cr-toolbar {
|
|
1940
|
+
flex-wrap: wrap;
|
|
1941
|
+
justify-content: center;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
@media (max-width: 480px) {
|
|
1946
|
+
.sfx-cr-toolbar {
|
|
1947
|
+
padding: 6px 8px;
|
|
1948
|
+
gap: 8px;
|
|
1949
|
+
}
|
|
1950
|
+
.sfx-cr-toolbar-group {
|
|
1951
|
+
gap: 8px;
|
|
1952
|
+
}
|
|
1953
|
+
.sfx-cr-toolbar-btn {
|
|
1954
|
+
width: 40px;
|
|
1955
|
+
height: 32px;
|
|
1956
|
+
}
|
|
1957
|
+
.sfx-cr-toolbar-btn svg {
|
|
1958
|
+
width: 18px;
|
|
1959
|
+
height: 18px;
|
|
1960
|
+
display: block;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
/* Narrow editor (component itself is small, regardless of viewport):
|
|
1965
|
+
stack the toolbar vertically along the LEFT edge, icons only.
|
|
1966
|
+
Tucked tight against the edge with a compact 30×30 footprint so
|
|
1967
|
+
the photo behind keeps maximum breathing room. Triggered by the
|
|
1968
|
+
sfxcrop container set on the sfx-crop host — matches editor
|
|
1969
|
+
width, not viewport width, so a narrow column on a wide desktop
|
|
1970
|
+
gets the same compact layout.
|
|
1971
|
+
|
|
1972
|
+
EXCLUDED from the fixed variant via :not([variant="fixed"]): a fixed
|
|
1973
|
+
frame is often portrait/phone-shaped (narrow but tall), where the left
|
|
1974
|
+
rail looks wrong — fixed keeps the horizontal top bar at any width. */
|
|
1975
|
+
@container sfxcrop (max-width: 760px) {
|
|
1976
|
+
:host(:not([variant="fixed"])) {
|
|
1977
|
+
top: 8px;
|
|
1978
|
+
bottom: 8px;
|
|
1979
|
+
/* Stay full-width (don't collapse to a left-hugging strip): the Done
|
|
1980
|
+
button is absolutely positioned against this host, so the host must
|
|
1981
|
+
keep spanning to the container's right edge for Done to stay in the
|
|
1982
|
+
top-right corner. The button column is tucked left with padding
|
|
1983
|
+
instead of by repositioning the whole host. */
|
|
1984
|
+
left: 0;
|
|
1985
|
+
right: 0;
|
|
1986
|
+
padding-left: 4px;
|
|
1987
|
+
justify-content: flex-start;
|
|
1988
|
+
/* Cross-axis centers the stacked button column vertically inside
|
|
1989
|
+
the full-height host strip — toolbar floats in the middle of
|
|
1990
|
+
the photo regardless of its content height. */
|
|
1991
|
+
align-items: center;
|
|
1992
|
+
}
|
|
1993
|
+
:host(:not([variant="fixed"])[toolbar-position="bottom"]) {
|
|
1994
|
+
top: 8px;
|
|
1995
|
+
bottom: 8px;
|
|
1996
|
+
}
|
|
1997
|
+
/* Keep Done pinned to the editor's top-right corner — exactly where the
|
|
1998
|
+
desktop bar puts it — even though the host now spans the full photo
|
|
1999
|
+
height (top:50% would otherwise drop it to the vertical middle). A bit
|
|
2000
|
+
smaller and tucked tighter into the corner to free up the photo. */
|
|
2001
|
+
:host(:not([variant="fixed"])) .sfx-cr-done-btn {
|
|
2002
|
+
top: 6px;
|
|
2003
|
+
right: 8px;
|
|
2004
|
+
height: 28px;
|
|
2005
|
+
padding: 0 12px;
|
|
2006
|
+
font-size: 12px;
|
|
2007
|
+
transform: none;
|
|
2008
|
+
}
|
|
2009
|
+
:host(:not([variant="fixed"])) .sfx-cr-done-btn:hover { transform: scale(1.02); }
|
|
2010
|
+
:host(:not([variant="fixed"])) .sfx-cr-done-btn:active { transform: scale(0.97); }
|
|
2011
|
+
:host(:not([variant="fixed"])) .sfx-cr-toolbar {
|
|
2012
|
+
flex-direction: column;
|
|
2013
|
+
flex-wrap: nowrap;
|
|
2014
|
+
align-items: center;
|
|
2015
|
+
justify-content: flex-start;
|
|
2016
|
+
gap: 4px;
|
|
2017
|
+
padding: 3px;
|
|
2018
|
+
}
|
|
2019
|
+
:host(:not([variant="fixed"])) .sfx-cr-toolbar-group {
|
|
2020
|
+
flex-direction: column;
|
|
2021
|
+
gap: 4px;
|
|
2022
|
+
}
|
|
2023
|
+
:host(:not([variant="fixed"])) .sfx-cr-toolbar-btn {
|
|
2024
|
+
width: 30px;
|
|
2025
|
+
height: 30px;
|
|
2026
|
+
}
|
|
2027
|
+
:host(:not([variant="fixed"])) .sfx-cr-toolbar-btn svg {
|
|
2028
|
+
width: 16px;
|
|
2029
|
+
height: 16px;
|
|
2030
|
+
}
|
|
2031
|
+
:host(:not([variant="fixed"])) .sfx-cr-reset-btn {
|
|
2032
|
+
width: 30px;
|
|
2033
|
+
height: 30px;
|
|
2034
|
+
min-width: 0;
|
|
2035
|
+
padding: 0;
|
|
2036
|
+
gap: 0;
|
|
2037
|
+
justify-content: center;
|
|
2038
|
+
}
|
|
2039
|
+
:host(:not([variant="fixed"])) .sfx-cr-reset-btn svg {
|
|
2040
|
+
width: 16px;
|
|
2041
|
+
height: 16px;
|
|
2042
|
+
}
|
|
2043
|
+
:host(:not([variant="fixed"])) .sfx-cr-reset-btn span {
|
|
2044
|
+
display: none;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
/* Fixed + narrow (portrait/phone frame): keep the horizontal top bar, but
|
|
2049
|
+
render every control as a uniform 40×40 round icon button, centered in
|
|
2050
|
+
the frame. (Reset / shapes labels are dropped — icon only.) */
|
|
2051
|
+
@container sfxcrop (max-width: 760px) {
|
|
2052
|
+
:host([variant="fixed"]) .sfx-cr-toolbar {
|
|
2053
|
+
flex-wrap: wrap;
|
|
2054
|
+
justify-content: center;
|
|
2055
|
+
gap: 10px;
|
|
2056
|
+
}
|
|
2057
|
+
:host([variant="fixed"]) .sfx-cr-toolbar-group {
|
|
2058
|
+
gap: 10px;
|
|
2059
|
+
}
|
|
2060
|
+
:host([variant="fixed"]) .sfx-cr-reset-btn,
|
|
2061
|
+
:host([variant="fixed"]) .sfx-cr-toolbar-btn {
|
|
2062
|
+
width: 40px;
|
|
2063
|
+
height: 40px;
|
|
2064
|
+
min-width: 0;
|
|
2065
|
+
padding: 0;
|
|
2066
|
+
gap: 0;
|
|
2067
|
+
justify-content: center;
|
|
2068
|
+
}
|
|
2069
|
+
:host([variant="fixed"]) .sfx-cr-reset-btn span {
|
|
2070
|
+
display: none;
|
|
2071
|
+
}
|
|
2072
|
+
:host([variant="fixed"]) .sfx-cr-done-btn {
|
|
2073
|
+
/* Stay in the top-right corner when the bar wraps to multiple rows
|
|
2074
|
+
(top:50% would otherwise centre Done over the taller wrapped bar),
|
|
2075
|
+
a bit smaller and tucked tighter into the corner. */
|
|
2076
|
+
top: 6px;
|
|
2077
|
+
right: 8px;
|
|
2078
|
+
height: 28px;
|
|
2079
|
+
padding: 0 12px;
|
|
2080
|
+
font-size: 12px;
|
|
2081
|
+
transform: none;
|
|
2082
|
+
}
|
|
2083
|
+
:host([variant="fixed"]) .sfx-cr-done-btn:hover { transform: scale(1.02); }
|
|
2084
|
+
:host([variant="fixed"]) .sfx-cr-done-btn:active { transform: scale(0.97); }
|
|
2085
|
+
}
|
|
2086
|
+
`;
|
|
2087
|
+
var te = Object.defineProperty, v = (i, t, e, o) => {
|
|
2088
|
+
for (var r = void 0, a = i.length - 1, n; a >= 0; a--)
|
|
2089
|
+
(n = i[a]) && (r = n(t, e, r) || r);
|
|
2090
|
+
return r && te(t, e, r), r;
|
|
2091
|
+
};
|
|
2092
|
+
const ot = class ot extends N {
|
|
2093
|
+
constructor() {
|
|
2094
|
+
super(...arguments), this.shape = "free", this.rotation = 0, this.showRotateButton = !0, this.showFlipButton = !0, this.showRotateSlider = !0, this.showShapeSelector = !0, this.toolbarPosition = "top", this.variant = "classic", this.availableShapes = null, this.icons = {}, this.onPopoverOpen = (t) => {
|
|
2095
|
+
var o;
|
|
2096
|
+
((o = t.detail) == null ? void 0 : o.source) !== "shapes" && this.shapesEl && (this.shapesEl.open = !1);
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
render() {
|
|
2100
|
+
const t = this.showRotateButton || this.showFlipButton, e = pt(this.availableShapes) ?? [...J];
|
|
2101
|
+
return f`
|
|
2102
|
+
<div class="sfx-cr-toolbar" @sfx-crop-popover-open=${this.onPopoverOpen}>
|
|
2103
|
+
<button
|
|
2104
|
+
type="button"
|
|
2105
|
+
class="sfx-cr-reset-btn"
|
|
2106
|
+
aria-label="Reset all changes"
|
|
2107
|
+
@click=${() => this.emit({ type: "reset" })}
|
|
2108
|
+
>
|
|
2109
|
+
${k(w("reset", this.icons))}
|
|
2110
|
+
<span>Reset</span>
|
|
2111
|
+
</button>
|
|
2112
|
+
|
|
2113
|
+
${t ? f`
|
|
2114
|
+
<div class="sfx-cr-toolbar-group">
|
|
2115
|
+
${this.showRotateButton ? f`
|
|
2116
|
+
<button
|
|
2117
|
+
type="button"
|
|
2118
|
+
class="sfx-cr-toolbar-btn"
|
|
2119
|
+
aria-label="Rotate left 90°"
|
|
2120
|
+
@click=${() => this.emit({ type: "rotate-left" })}
|
|
2121
|
+
>${k(w("rotateLeft", this.icons))}</button>
|
|
2122
|
+
` : null}
|
|
2123
|
+
${this.showFlipButton ? f`
|
|
2124
|
+
<button
|
|
2125
|
+
type="button"
|
|
2126
|
+
class="sfx-cr-toolbar-btn"
|
|
2127
|
+
aria-label="Flip horizontal"
|
|
2128
|
+
@click=${() => this.emit({ type: "flip-h" })}
|
|
2129
|
+
>${k(w("flipHorizontal", this.icons))}</button>
|
|
2130
|
+
` : null}
|
|
2131
|
+
</div>
|
|
2132
|
+
` : null}
|
|
2133
|
+
|
|
2134
|
+
${this.showRotateSlider ? f`
|
|
2135
|
+
<sfx-crop-rotate
|
|
2136
|
+
.value=${this.rotation}
|
|
2137
|
+
.icons=${this.icons}
|
|
2138
|
+
@sfx-crop-rotate-change=${(o) => this.emit({ type: "rotation", value: o.detail.degrees })}
|
|
2139
|
+
></sfx-crop-rotate>
|
|
2140
|
+
` : null}
|
|
2141
|
+
|
|
2142
|
+
${this.showShapeSelector ? f`
|
|
2143
|
+
<sfx-crop-shapes
|
|
2144
|
+
.value=${this.shape}
|
|
2145
|
+
.shapes=${e}
|
|
2146
|
+
variant=${this.variant}
|
|
2147
|
+
.icons=${this.icons}
|
|
2148
|
+
@sfx-crop-shape-change=${(o) => this.emit({ type: "shape", value: o.detail.shape })}
|
|
2149
|
+
></sfx-crop-shapes>
|
|
2150
|
+
` : null}
|
|
2151
|
+
</div>
|
|
2152
|
+
|
|
2153
|
+
<button
|
|
2154
|
+
type="button"
|
|
2155
|
+
class="sfx-cr-done-btn"
|
|
2156
|
+
part="done"
|
|
2157
|
+
aria-label="Done"
|
|
2158
|
+
@click=${() => this.emit({ type: "save" })}
|
|
2159
|
+
>Done</button>
|
|
2160
|
+
`;
|
|
2161
|
+
}
|
|
2162
|
+
/** Sync the rotation slider without firing an event. */
|
|
2163
|
+
setRotationValue(t) {
|
|
2164
|
+
var e;
|
|
2165
|
+
this.rotation = t, (e = this.rotateEl) == null || e.setValue(t);
|
|
2166
|
+
}
|
|
2167
|
+
/** Sync the shape selector without firing an event. */
|
|
2168
|
+
setShapeValue(t) {
|
|
2169
|
+
var e;
|
|
2170
|
+
this.shape = t, (e = this.shapesEl) == null || e.setValue(t);
|
|
2171
|
+
}
|
|
2172
|
+
emit(t) {
|
|
2173
|
+
this.dispatchEvent(new CustomEvent("sfx-crop-toolbar-command", {
|
|
2174
|
+
detail: t,
|
|
2175
|
+
bubbles: !0,
|
|
2176
|
+
composed: !0
|
|
2177
|
+
}));
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
ot.styles = [j, It, Qt];
|
|
2181
|
+
let x = ot;
|
|
2182
|
+
v([
|
|
2183
|
+
s({ type: String })
|
|
2184
|
+
], x.prototype, "shape");
|
|
2185
|
+
v([
|
|
2186
|
+
s({ type: Number })
|
|
2187
|
+
], x.prototype, "rotation");
|
|
2188
|
+
v([
|
|
2189
|
+
s({ type: Boolean, attribute: "show-rotate-button" })
|
|
2190
|
+
], x.prototype, "showRotateButton");
|
|
2191
|
+
v([
|
|
2192
|
+
s({ type: Boolean, attribute: "show-flip-button" })
|
|
2193
|
+
], x.prototype, "showFlipButton");
|
|
2194
|
+
v([
|
|
2195
|
+
s({ type: Boolean, attribute: "show-rotate-slider" })
|
|
2196
|
+
], x.prototype, "showRotateSlider");
|
|
2197
|
+
v([
|
|
2198
|
+
s({ type: Boolean, attribute: "show-shape-selector" })
|
|
2199
|
+
], x.prototype, "showShapeSelector");
|
|
2200
|
+
v([
|
|
2201
|
+
s({ type: String, attribute: "toolbar-position", reflect: !0 })
|
|
2202
|
+
], x.prototype, "toolbarPosition");
|
|
2203
|
+
v([
|
|
2204
|
+
s({ type: String, reflect: !0 })
|
|
2205
|
+
], x.prototype, "variant");
|
|
2206
|
+
v([
|
|
2207
|
+
s({ attribute: "available-shapes" })
|
|
2208
|
+
], x.prototype, "availableShapes");
|
|
2209
|
+
v([
|
|
2210
|
+
s({ attribute: !1 })
|
|
2211
|
+
], x.prototype, "icons");
|
|
2212
|
+
v([
|
|
2213
|
+
E("sfx-crop-rotate")
|
|
2214
|
+
], x.prototype, "rotateEl");
|
|
2215
|
+
v([
|
|
2216
|
+
E("sfx-crop-shapes")
|
|
2217
|
+
], x.prototype, "shapesEl");
|
|
2218
|
+
const ee = b`
|
|
2219
|
+
:host {
|
|
2220
|
+
position: relative;
|
|
2221
|
+
display: inline-flex;
|
|
2222
|
+
align-items: center;
|
|
2223
|
+
}
|
|
2224
|
+
|
|
2225
|
+
.sfx-cr-zoom-root {
|
|
2226
|
+
position: relative;
|
|
2227
|
+
display: flex;
|
|
2228
|
+
align-items: center;
|
|
2229
|
+
justify-content: center;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
/* Trigger — visually matches the other toolbar icon buttons. */
|
|
2233
|
+
.sfx-cr-zoom-trigger {
|
|
2234
|
+
width: 44px;
|
|
2235
|
+
height: 36px;
|
|
2236
|
+
display: flex;
|
|
2237
|
+
align-items: center;
|
|
2238
|
+
justify-content: center;
|
|
2239
|
+
padding: 0;
|
|
2240
|
+
background: transparent;
|
|
2241
|
+
color: var(--sfx-cr-text-secondary);
|
|
2242
|
+
border: 1px solid transparent;
|
|
2243
|
+
border-radius: 999px;
|
|
2244
|
+
cursor: pointer;
|
|
2245
|
+
transition:
|
|
2246
|
+
border-color var(--sfx-cr-transition),
|
|
2247
|
+
color var(--sfx-cr-transition),
|
|
2248
|
+
transform var(--sfx-cr-transition);
|
|
2249
|
+
}
|
|
2250
|
+
.sfx-cr-zoom-trigger:hover {
|
|
2251
|
+
border-color: var(--sfx-cr-primary);
|
|
2252
|
+
color: var(--sfx-cr-primary);
|
|
2253
|
+
transform: translateY(-1px);
|
|
2254
|
+
}
|
|
2255
|
+
.sfx-cr-zoom-trigger:active {
|
|
2256
|
+
transform: translateY(0) scale(0.96);
|
|
2257
|
+
}
|
|
2258
|
+
.sfx-cr-zoom-trigger:focus-visible {
|
|
2259
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
2260
|
+
outline-offset: 2px;
|
|
2261
|
+
}
|
|
2262
|
+
.sfx-cr-zoom-trigger svg {
|
|
2263
|
+
width: 20px;
|
|
2264
|
+
height: 20px;
|
|
2265
|
+
display: block;
|
|
2266
|
+
}
|
|
2267
|
+
|
|
2268
|
+
:host([open]) .sfx-cr-zoom-trigger {
|
|
2269
|
+
color: var(--sfx-cr-primary);
|
|
2270
|
+
}
|
|
2271
|
+
|
|
2272
|
+
/* Popover — transparent, floats above the trigger. */
|
|
2273
|
+
.sfx-cr-zoom-popover {
|
|
2274
|
+
position: fixed;
|
|
2275
|
+
top: var(--sfx-cr-popover-top, 50%);
|
|
2276
|
+
left: var(--sfx-cr-popover-left, 50%);
|
|
2277
|
+
transform: translateX(-50%) translateY(6px) scale(0.98);
|
|
2278
|
+
display: flex;
|
|
2279
|
+
flex-direction: column;
|
|
2280
|
+
align-items: center;
|
|
2281
|
+
gap: 4px;
|
|
2282
|
+
padding: 0;
|
|
2283
|
+
background: transparent;
|
|
2284
|
+
border: none;
|
|
2285
|
+
box-shadow: none;
|
|
2286
|
+
opacity: 0;
|
|
2287
|
+
pointer-events: none;
|
|
2288
|
+
transition: opacity 120ms ease-in, transform 120ms ease-in;
|
|
2289
|
+
white-space: nowrap;
|
|
2290
|
+
z-index: 10;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
:host([open]) .sfx-cr-zoom-popover {
|
|
2294
|
+
opacity: 1;
|
|
2295
|
+
transform: translateX(-50%) translateY(0) scale(1);
|
|
2296
|
+
pointer-events: auto;
|
|
2297
|
+
transition: opacity 220ms cubic-bezier(0.34, 1.2, 0.64, 1),
|
|
2298
|
+
transform 220ms cubic-bezier(0.34, 1.2, 0.64, 1);
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
.sfx-cr-zoom-ruler {
|
|
2302
|
+
position: relative;
|
|
2303
|
+
width: var(--ruler-w, 260px);
|
|
2304
|
+
height: 30px;
|
|
2305
|
+
overflow: hidden;
|
|
2306
|
+
cursor: grab;
|
|
2307
|
+
touch-action: none;
|
|
2308
|
+
user-select: none;
|
|
2309
|
+
-webkit-user-select: none;
|
|
2310
|
+
}
|
|
2311
|
+
.sfx-cr-zoom-ruler.is-dragging { cursor: grabbing; }
|
|
2312
|
+
.sfx-cr-zoom-ruler:focus-visible {
|
|
2313
|
+
outline: 2px solid var(--sfx-cr-ring);
|
|
2314
|
+
outline-offset: 2px;
|
|
2315
|
+
border-radius: 4px;
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
.sfx-cr-zoom-ticks {
|
|
2319
|
+
position: absolute;
|
|
2320
|
+
top: 50%;
|
|
2321
|
+
left: 0;
|
|
2322
|
+
height: 100%;
|
|
2323
|
+
will-change: transform;
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
.sfx-cr-zoom-tick {
|
|
2327
|
+
position: absolute;
|
|
2328
|
+
top: 50%;
|
|
2329
|
+
width: 1px;
|
|
2330
|
+
height: 8px;
|
|
2331
|
+
margin-left: -0.5px;
|
|
2332
|
+
margin-top: -4px;
|
|
2333
|
+
border-radius: 0.5px;
|
|
2334
|
+
background: var(--sfx-cr-text);
|
|
2335
|
+
opacity: 0.55;
|
|
2336
|
+
}
|
|
2337
|
+
.sfx-cr-zoom-tick--major {
|
|
2338
|
+
width: 1px;
|
|
2339
|
+
height: 12px;
|
|
2340
|
+
margin-left: -0.5px;
|
|
2341
|
+
margin-top: -6px;
|
|
2342
|
+
opacity: 0.9;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
.sfx-cr-zoom-indicator {
|
|
2346
|
+
position: absolute;
|
|
2347
|
+
top: calc(50% + 4px);
|
|
2348
|
+
height: 16px;
|
|
2349
|
+
left: 50%;
|
|
2350
|
+
width: 4px;
|
|
2351
|
+
margin-left: -2px;
|
|
2352
|
+
background: var(--sfx-cr-text);
|
|
2353
|
+
border-radius: 2px;
|
|
2354
|
+
pointer-events: none;
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
.sfx-cr-zoom-value {
|
|
2358
|
+
font-size: 14px;
|
|
2359
|
+
font-weight: 400;
|
|
2360
|
+
color: var(--sfx-cr-text);
|
|
2361
|
+
text-align: center;
|
|
2362
|
+
font-variant-numeric: tabular-nums;
|
|
2363
|
+
letter-spacing: 0.2px;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
@media (max-width: 768px) {
|
|
2367
|
+
/* Slimmer ruler + smaller readout on phones, mirroring the rotate
|
|
2368
|
+
popover. Push the popover down toward the canvas bottom edge so
|
|
2369
|
+
it doesn't sit in the middle of the photo. */
|
|
2370
|
+
.sfx-cr-zoom-ruler { width: 220px; height: 22px; }
|
|
2371
|
+
.sfx-cr-zoom-tick { height: 6px; margin-top: -3px; }
|
|
2372
|
+
.sfx-cr-zoom-tick--major { height: 9px; margin-top: -4.5px; }
|
|
2373
|
+
.sfx-cr-zoom-indicator {
|
|
2374
|
+
top: calc(50% + 3px);
|
|
2375
|
+
height: 12px;
|
|
2376
|
+
width: 3px;
|
|
2377
|
+
margin-left: -1.5px;
|
|
2378
|
+
}
|
|
2379
|
+
.sfx-cr-zoom-value { font-size: 12px; }
|
|
2380
|
+
:host([open]) .sfx-cr-zoom-popover {
|
|
2381
|
+
transform: translateX(-50%) translateY(24px) scale(1);
|
|
2382
|
+
}
|
|
2383
|
+
}
|
|
2384
|
+
@media (max-width: 480px) {
|
|
2385
|
+
.sfx-cr-zoom-ruler { width: 180px; height: 20px; }
|
|
2386
|
+
.sfx-cr-zoom-tick { height: 5px; margin-top: -2.5px; }
|
|
2387
|
+
.sfx-cr-zoom-tick--major { height: 8px; margin-top: -4px; }
|
|
2388
|
+
.sfx-cr-zoom-indicator {
|
|
2389
|
+
top: calc(50% + 2px);
|
|
2390
|
+
height: 10px;
|
|
2391
|
+
}
|
|
2392
|
+
.sfx-cr-zoom-value { font-size: 11px; }
|
|
2393
|
+
:host([open]) .sfx-cr-zoom-popover {
|
|
2394
|
+
transform: translateX(-50%) translateY(30px) scale(1);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
/* Narrow editor — match the compact 30×30 trigger sizing of the
|
|
2399
|
+
rest of the vertical left-rail toolbar. Container query so a
|
|
2400
|
+
narrow editor on a wide desktop also collapses. */
|
|
2401
|
+
@container sfxcrop (max-width: 600px) {
|
|
2402
|
+
.sfx-cr-zoom-trigger {
|
|
2403
|
+
width: 30px;
|
|
2404
|
+
height: 30px;
|
|
2405
|
+
}
|
|
2406
|
+
.sfx-cr-zoom-trigger svg {
|
|
2407
|
+
width: 16px;
|
|
2408
|
+
height: 16px;
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
`;
|
|
2412
|
+
var re = Object.defineProperty, B = (i, t, e, o) => {
|
|
2413
|
+
for (var r = void 0, a = i.length - 1, n; a >= 0; a--)
|
|
2414
|
+
(n = i[a]) && (r = n(t, e, r) || r);
|
|
2415
|
+
return r && re(t, e, r), r;
|
|
2416
|
+
};
|
|
2417
|
+
function K(i, t, e) {
|
|
2418
|
+
return Math.log(i / t) / Math.log(e / t);
|
|
2419
|
+
}
|
|
2420
|
+
function nt(i, t, e) {
|
|
2421
|
+
return t * Math.pow(e / t, i);
|
|
2422
|
+
}
|
|
2423
|
+
const G = 260, lt = 260, ct = 40, oe = 5, it = class it extends N {
|
|
2424
|
+
constructor() {
|
|
2425
|
+
super(...arguments), this.min = 0.5, this.max = 5, this.value = 1, this.open = !1, this.icons = {}, this.dragging = !1, this.activePointer = null, this.pointerStartX = 0, this.pointerStartSlider = 0, this.autoCloseTimer = null, this.docClickHandler = () => {
|
|
2426
|
+
this.open && (this.clearAutoClose(), this.open = !1);
|
|
2427
|
+
}, this.popoverAnchor = ut(this, ".sfx-cr-zoom-popover"), this.onTriggerClick = (t) => {
|
|
2428
|
+
t.stopPropagation(), this.clearAutoClose(), this.open = !this.open, this.open && this.dispatchEvent(new CustomEvent("sfx-crop-popover-open", {
|
|
2429
|
+
detail: { source: "zoom" },
|
|
2430
|
+
bubbles: !0,
|
|
2431
|
+
composed: !0
|
|
2432
|
+
}));
|
|
2433
|
+
}, this.onKeyDown = (t) => {
|
|
2434
|
+
if (this.open) {
|
|
2435
|
+
if (t.key === "Escape") {
|
|
2436
|
+
t.preventDefault(), t.stopPropagation(), this.open = !1;
|
|
2437
|
+
return;
|
|
2438
|
+
}
|
|
2439
|
+
if (t.key === "ArrowLeft" || t.key === "ArrowRight") {
|
|
2440
|
+
t.preventDefault();
|
|
2441
|
+
const e = t.key === "ArrowRight" ? 1 : -1, o = t.shiftKey ? 0.05 : 0.01, r = K(this.value, this.min, this.max), a = R(r + e * o, 0, 1);
|
|
2442
|
+
this.emit(nt(a, this.min, this.max));
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
}, this.onPointerDown = (t) => {
|
|
2446
|
+
if (this.activePointer === null) {
|
|
2447
|
+
this.clearAutoClose(), this.activePointer = t.pointerId, this.pointerStartX = t.clientX, this.pointerStartSlider = K(this.value, this.min, this.max), this.dragging = !0;
|
|
2448
|
+
try {
|
|
2449
|
+
t.currentTarget.setPointerCapture(t.pointerId);
|
|
2450
|
+
} catch {
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
}, this.onPointerMove = (t) => {
|
|
2454
|
+
if (this.activePointer !== t.pointerId) return;
|
|
2455
|
+
const e = t.clientX - this.pointerStartX, o = R(this.pointerStartSlider - e / G, 0, 1);
|
|
2456
|
+
this.emit(nt(o, this.min, this.max));
|
|
2457
|
+
}, this.onPointerUp = (t) => {
|
|
2458
|
+
if (this.activePointer === t.pointerId) {
|
|
2459
|
+
this.activePointer = null, this.dragging = !1;
|
|
2460
|
+
try {
|
|
2461
|
+
t.currentTarget.releasePointerCapture(t.pointerId);
|
|
2462
|
+
} catch {
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
}, this.onReset = () => {
|
|
2466
|
+
this.emit(1);
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
connectedCallback() {
|
|
2470
|
+
super.connectedCallback(), document.addEventListener("click", this.docClickHandler);
|
|
2471
|
+
}
|
|
2472
|
+
disconnectedCallback() {
|
|
2473
|
+
super.disconnectedCallback(), document.removeEventListener("click", this.docClickHandler), this.popoverAnchor.stop(), this.clearAutoClose();
|
|
2474
|
+
}
|
|
2475
|
+
updated(t) {
|
|
2476
|
+
t.has("open") && (this.open ? this.popoverAnchor.start() : this.popoverAnchor.stop()), this.open && (t.has("value") || t.has("dragging")) && this.popoverAnchor.update();
|
|
2477
|
+
}
|
|
2478
|
+
render() {
|
|
2479
|
+
const t = K(this.value, this.min, this.max), e = Math.round(this.value * 100), o = lt / 2 - t * G, r = [], a = 1 / ct;
|
|
2480
|
+
for (let n = 0; n <= ct; n++)
|
|
2481
|
+
r.push({ pos: n * a, major: n % oe === 0 });
|
|
2482
|
+
return f`
|
|
2483
|
+
<div
|
|
2484
|
+
class="sfx-cr-zoom-root"
|
|
2485
|
+
@click=${(n) => n.stopPropagation()}
|
|
2486
|
+
@keydown=${this.onKeyDown}
|
|
2487
|
+
>
|
|
2488
|
+
<button
|
|
2489
|
+
type="button"
|
|
2490
|
+
class="sfx-cr-zoom-trigger"
|
|
2491
|
+
aria-label=${`Zoom — ${e}%`}
|
|
2492
|
+
aria-haspopup="dialog"
|
|
2493
|
+
aria-expanded=${this.open ? "true" : "false"}
|
|
2494
|
+
@click=${this.onTriggerClick}
|
|
2495
|
+
>${k(w("loupe", this.icons))}</button>
|
|
2496
|
+
|
|
2497
|
+
<div class="sfx-cr-zoom-popover" role="group" aria-label="Zoom controls">
|
|
2498
|
+
<div
|
|
2499
|
+
class=${`sfx-cr-zoom-ruler${this.dragging ? " is-dragging" : ""}`}
|
|
2500
|
+
role="slider"
|
|
2501
|
+
aria-valuemin=${String(this.min)}
|
|
2502
|
+
aria-valuemax=${String(this.max)}
|
|
2503
|
+
aria-valuenow=${this.value.toFixed(2)}
|
|
2504
|
+
aria-valuetext=${`${e}%`}
|
|
2505
|
+
tabindex="0"
|
|
2506
|
+
style=${`--ruler-w: ${lt}px`}
|
|
2507
|
+
@pointerdown=${this.onPointerDown}
|
|
2508
|
+
@pointermove=${this.onPointerMove}
|
|
2509
|
+
@pointerup=${this.onPointerUp}
|
|
2510
|
+
@pointercancel=${this.onPointerUp}
|
|
2511
|
+
@dblclick=${this.onReset}
|
|
2512
|
+
>
|
|
2513
|
+
<div class="sfx-cr-zoom-ticks" style=${`transform: translateX(${o.toFixed(1)}px)`}>
|
|
2514
|
+
${r.map((n) => f`
|
|
2515
|
+
<span
|
|
2516
|
+
class=${`sfx-cr-zoom-tick${n.major ? " sfx-cr-zoom-tick--major" : ""}`}
|
|
2517
|
+
style=${`left: ${(n.pos * G).toFixed(1)}px`}
|
|
2518
|
+
></span>
|
|
2519
|
+
`)}
|
|
2520
|
+
</div>
|
|
2521
|
+
<div class="sfx-cr-zoom-indicator" aria-hidden="true"></div>
|
|
2522
|
+
</div>
|
|
2523
|
+
<span class="sfx-cr-zoom-value">${e}%</span>
|
|
2524
|
+
</div>
|
|
2525
|
+
</div>
|
|
2526
|
+
`;
|
|
2527
|
+
}
|
|
2528
|
+
/** Sync value without emitting — used by host to reflect controller state. */
|
|
2529
|
+
setValue(t) {
|
|
2530
|
+
this.value = R(t, this.min, this.max);
|
|
2531
|
+
}
|
|
2532
|
+
/**
|
|
2533
|
+
* Open the popover and auto-close after `duration` ms of inactivity.
|
|
2534
|
+
* Subsequent calls debounce the close (so a stream of wheel events keeps
|
|
2535
|
+
* the slider visible). Manual interaction (trigger click, ruler drag)
|
|
2536
|
+
* cancels the pending close.
|
|
2537
|
+
*
|
|
2538
|
+
* If the popover is already open via manual click (no timer armed) this
|
|
2539
|
+
* is a no-op: a wheel burst must never hijack a user-opened popover into
|
|
2540
|
+
* auto-closing.
|
|
2541
|
+
*/
|
|
2542
|
+
showTemporarily(t = 1500) {
|
|
2543
|
+
if (this.open && this.autoCloseTimer === null) return;
|
|
2544
|
+
const e = this.open;
|
|
2545
|
+
this.open = !0, e || this.dispatchEvent(new CustomEvent("sfx-crop-popover-open", {
|
|
2546
|
+
detail: { source: "zoom" },
|
|
2547
|
+
bubbles: !0,
|
|
2548
|
+
composed: !0
|
|
2549
|
+
})), this.clearAutoClose(), this.autoCloseTimer = setTimeout(() => {
|
|
2550
|
+
this.autoCloseTimer = null, this.open = !1;
|
|
2551
|
+
}, t);
|
|
2552
|
+
}
|
|
2553
|
+
clearAutoClose() {
|
|
2554
|
+
this.autoCloseTimer !== null && (clearTimeout(this.autoCloseTimer), this.autoCloseTimer = null);
|
|
2555
|
+
}
|
|
2556
|
+
emit(t) {
|
|
2557
|
+
this.value !== t && (this.value = t, this.dispatchEvent(new CustomEvent("sfx-crop-zoom-change", {
|
|
2558
|
+
detail: { scale: t },
|
|
2559
|
+
bubbles: !0,
|
|
2560
|
+
composed: !0
|
|
2561
|
+
})));
|
|
2562
|
+
}
|
|
2563
|
+
};
|
|
2564
|
+
it.styles = [j, ee];
|
|
2565
|
+
let S = it;
|
|
2566
|
+
B([
|
|
2567
|
+
s({ type: Number })
|
|
2568
|
+
], S.prototype, "min");
|
|
2569
|
+
B([
|
|
2570
|
+
s({ type: Number })
|
|
2571
|
+
], S.prototype, "max");
|
|
2572
|
+
B([
|
|
2573
|
+
s({ type: Number })
|
|
2574
|
+
], S.prototype, "value");
|
|
2575
|
+
B([
|
|
2576
|
+
s({ type: Boolean, reflect: !0 })
|
|
2577
|
+
], S.prototype, "open");
|
|
2578
|
+
B([
|
|
2579
|
+
s({ attribute: !1 })
|
|
2580
|
+
], S.prototype, "icons");
|
|
2581
|
+
B([
|
|
2582
|
+
M()
|
|
2583
|
+
], S.prototype, "dragging");
|
|
2584
|
+
A("sfx-crop-zoom", S);
|
|
2585
|
+
A("sfx-crop-rotate", $);
|
|
2586
|
+
A("sfx-crop-shapes", y);
|
|
2587
|
+
A("sfx-crop-canvas", D);
|
|
2588
|
+
A("sfx-crop-toolbar", x);
|
|
2589
|
+
A("sfx-crop", l);
|
|
2590
|
+
//# sourceMappingURL=define-t4Z6KaLY.js.map
|