@ai-matrx/capture 0.0.0 → 0.1.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +40 -1
- package/dist/index.cjs +19 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +119 -0
- package/dist/index.d.ts +119 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/react.cjs +1003 -0
- package/dist/react.cjs.map +1 -0
- package/dist/react.d.cts +350 -0
- package/dist/react.d.ts +350 -0
- package/dist/react.js +996 -0
- package/dist/react.js.map +1 -0
- package/package.json +95 -7
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/react.ts
|
|
22
|
+
var react_exports = {};
|
|
23
|
+
__export(react_exports, {
|
|
24
|
+
CameraCapture: () => CameraCapture,
|
|
25
|
+
CaptureSheet: () => CaptureSheet,
|
|
26
|
+
CountdownOverlay: () => CountdownOverlay,
|
|
27
|
+
GridOverlay: () => GridOverlay,
|
|
28
|
+
ImageEditSheet: () => ImageEditSheet,
|
|
29
|
+
ModeSelector: () => ModeSelector,
|
|
30
|
+
OptionsGridPanel: () => OptionsGridPanel,
|
|
31
|
+
ShutterButton: () => ShutterButton,
|
|
32
|
+
ZoomRow: () => ZoomRow,
|
|
33
|
+
useTrackControls: () => useTrackControls
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(react_exports);
|
|
36
|
+
|
|
37
|
+
// src/components/CameraCapture.tsx
|
|
38
|
+
var import_react2 = require("react");
|
|
39
|
+
var import_lucide_react2 = require("lucide-react");
|
|
40
|
+
|
|
41
|
+
// src/cn.ts
|
|
42
|
+
var import_tailwind_merge = require("tailwind-merge");
|
|
43
|
+
function cn(...inputs) {
|
|
44
|
+
return (0, import_tailwind_merge.twMerge)(inputs.filter(Boolean).join(" "));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/hooks/useTrackControls.ts
|
|
48
|
+
var import_react = require("react");
|
|
49
|
+
var ZOOM_LADDER = [0.5, 1, 2, 4, 8];
|
|
50
|
+
function useTrackControls(stream) {
|
|
51
|
+
const [torchOn, setTorchOn] = (0, import_react.useState)(false);
|
|
52
|
+
const [zoom, setZoomState] = (0, import_react.useState)(1);
|
|
53
|
+
const [exposure, setExposureState] = (0, import_react.useState)(0);
|
|
54
|
+
const [caps, setCaps] = (0, import_react.useState)(null);
|
|
55
|
+
const track = stream?.getVideoTracks()[0] ?? null;
|
|
56
|
+
(0, import_react.useEffect)(() => {
|
|
57
|
+
setTorchOn(false);
|
|
58
|
+
if (!track || typeof track.getCapabilities !== "function") {
|
|
59
|
+
setCaps(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const read = () => {
|
|
63
|
+
try {
|
|
64
|
+
setCaps(track.getCapabilities());
|
|
65
|
+
const settings = track.getSettings();
|
|
66
|
+
if (typeof settings.zoom === "number") setZoomState(settings.zoom);
|
|
67
|
+
if (typeof settings.exposureCompensation === "number")
|
|
68
|
+
setExposureState(settings.exposureCompensation);
|
|
69
|
+
} catch {
|
|
70
|
+
setCaps(null);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
read();
|
|
74
|
+
const timer = window.setTimeout(read, 750);
|
|
75
|
+
return () => window.clearTimeout(timer);
|
|
76
|
+
}, [track]);
|
|
77
|
+
const torchSupported = caps?.torch === true;
|
|
78
|
+
const toggleTorch = (0, import_react.useCallback)(() => {
|
|
79
|
+
if (!track || !torchSupported) return;
|
|
80
|
+
const next = !torchOn;
|
|
81
|
+
track.applyConstraints({ advanced: [{ torch: next }] }).then(() => setTorchOn(next)).catch((err) => {
|
|
82
|
+
console.error("[capture-camera] torch toggle failed", err);
|
|
83
|
+
});
|
|
84
|
+
}, [track, torchSupported, torchOn]);
|
|
85
|
+
const zoomOptions = (0, import_react.useMemo)(() => {
|
|
86
|
+
const range = caps?.zoom;
|
|
87
|
+
if (!range || !(range.max > range.min)) return [];
|
|
88
|
+
const options = ZOOM_LADDER.filter(
|
|
89
|
+
(f) => f >= range.min && f <= range.max
|
|
90
|
+
);
|
|
91
|
+
if (!options.includes(1) && 1 >= range.min && 1 <= range.max) {
|
|
92
|
+
options.push(1);
|
|
93
|
+
options.sort((a, b) => a - b);
|
|
94
|
+
}
|
|
95
|
+
return options.length >= 2 ? options : [];
|
|
96
|
+
}, [caps]);
|
|
97
|
+
const setZoom = (0, import_react.useCallback)(
|
|
98
|
+
(factor) => {
|
|
99
|
+
if (!track || !caps?.zoom) return;
|
|
100
|
+
const clamped = Math.min(caps.zoom.max, Math.max(caps.zoom.min, factor));
|
|
101
|
+
track.applyConstraints({
|
|
102
|
+
advanced: [{ zoom: clamped }]
|
|
103
|
+
}).then(() => setZoomState(clamped)).catch((err) => {
|
|
104
|
+
console.error("[capture-camera] zoom failed", err);
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
[track, caps]
|
|
108
|
+
);
|
|
109
|
+
const exposureCaps = caps?.exposureCompensation;
|
|
110
|
+
const exposureRange = exposureCaps && exposureCaps.max > exposureCaps.min ? {
|
|
111
|
+
min: exposureCaps.min,
|
|
112
|
+
max: exposureCaps.max,
|
|
113
|
+
step: exposureCaps.step && exposureCaps.step > 0 ? exposureCaps.step : 0.5
|
|
114
|
+
} : null;
|
|
115
|
+
const setExposure = (0, import_react.useCallback)(
|
|
116
|
+
(value) => {
|
|
117
|
+
if (!track || !exposureRange) return;
|
|
118
|
+
const clamped = Math.min(
|
|
119
|
+
exposureRange.max,
|
|
120
|
+
Math.max(exposureRange.min, value)
|
|
121
|
+
);
|
|
122
|
+
track.applyConstraints({
|
|
123
|
+
advanced: [
|
|
124
|
+
{ exposureCompensation: clamped }
|
|
125
|
+
]
|
|
126
|
+
}).then(() => setExposureState(clamped)).catch((err) => {
|
|
127
|
+
console.error("[capture-camera] exposure failed", err);
|
|
128
|
+
});
|
|
129
|
+
},
|
|
130
|
+
[track, exposureRange]
|
|
131
|
+
);
|
|
132
|
+
return {
|
|
133
|
+
torchSupported,
|
|
134
|
+
torchOn,
|
|
135
|
+
toggleTorch,
|
|
136
|
+
zoomOptions,
|
|
137
|
+
zoom,
|
|
138
|
+
setZoom,
|
|
139
|
+
exposureSupported: exposureRange !== null,
|
|
140
|
+
exposure,
|
|
141
|
+
exposureRange,
|
|
142
|
+
setExposure
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/components/ShutterButton.tsx
|
|
147
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
148
|
+
function ShutterButton({
|
|
149
|
+
mode,
|
|
150
|
+
recording,
|
|
151
|
+
disabled = false,
|
|
152
|
+
onPress
|
|
153
|
+
}) {
|
|
154
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
155
|
+
"button",
|
|
156
|
+
{
|
|
157
|
+
type: "button",
|
|
158
|
+
onClick: onPress,
|
|
159
|
+
disabled,
|
|
160
|
+
"aria-label": mode === "photo" ? "Take photo" : recording ? "Stop recording" : "Start recording",
|
|
161
|
+
className: cn(
|
|
162
|
+
"group flex h-[74px] w-[74px] shrink-0 items-center justify-center rounded-full",
|
|
163
|
+
"border-[3.5px] border-white transition-opacity",
|
|
164
|
+
disabled && "opacity-30"
|
|
165
|
+
),
|
|
166
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
167
|
+
"span",
|
|
168
|
+
{
|
|
169
|
+
className: cn(
|
|
170
|
+
"block transition-all duration-200 ease-out group-active:scale-90",
|
|
171
|
+
mode === "photo" ? "h-[62px] w-[62px] rounded-full bg-white" : recording ? "h-8 w-8 rounded-md bg-[#FF3B30]" : "h-[62px] w-[62px] rounded-full bg-[#FF3B30]"
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/components/ModeSelector.tsx
|
|
180
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
181
|
+
var SPRING_EASING = "linear(0, 0.0047 0.71%, 0.0189 1.44%, 0.0755 2.93%, 0.1692 4.49%, 0.3921 7.55%, 0.8121 12.94%, 0.9804 15.49%, 1.0946 18.14%, 1.1423 20.16%, 1.1568 21.62%, 1.1541 23.03%, 1.1113 26.4%, 1.0322 31.83%, 0.9902 36.25%, 0.9769 40.24%, 0.9844 45.87%, 1.0028 55.35%, 1.0075 63.42%, 1.0006 85.48%, 1)";
|
|
182
|
+
var LABEL_BASE = "relative z-10 flex h-8 min-w-0 touch-manipulation items-center justify-center rounded-full px-3 text-[11px] font-semibold uppercase tracking-[0.12em] transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/70 disabled:opacity-40";
|
|
183
|
+
function ModeSelector({
|
|
184
|
+
mode,
|
|
185
|
+
onModeChange,
|
|
186
|
+
onUpload,
|
|
187
|
+
modeDisabled = false,
|
|
188
|
+
uploadDisabled = false,
|
|
189
|
+
extraModes = []
|
|
190
|
+
}) {
|
|
191
|
+
const segments = 3 + extraModes.length;
|
|
192
|
+
const activeIndex = mode === "video" ? 0 : 1;
|
|
193
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
194
|
+
"div",
|
|
195
|
+
{
|
|
196
|
+
role: "tablist",
|
|
197
|
+
"aria-label": "Capture mode",
|
|
198
|
+
className: "relative inline-grid rounded-full bg-white/10 p-1",
|
|
199
|
+
style: { gridTemplateColumns: `repeat(${segments}, minmax(0, 1fr))` },
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
202
|
+
"div",
|
|
203
|
+
{
|
|
204
|
+
"aria-hidden": true,
|
|
205
|
+
className: "pointer-events-none absolute bottom-1 top-1 left-1 rounded-full bg-white/20 transition-transform duration-500 ease-[cubic-bezier(0.34,1.56,0.64,1)] will-change-transform motion-reduce:transition-none",
|
|
206
|
+
style: {
|
|
207
|
+
width: `calc((100% - 0.5rem) / ${segments})`,
|
|
208
|
+
transform: `translateX(${activeIndex * 100}%)`,
|
|
209
|
+
transitionTimingFunction: SPRING_EASING
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
),
|
|
213
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
214
|
+
"button",
|
|
215
|
+
{
|
|
216
|
+
type: "button",
|
|
217
|
+
role: "tab",
|
|
218
|
+
"aria-selected": mode === "video",
|
|
219
|
+
disabled: modeDisabled,
|
|
220
|
+
onClick: () => onModeChange("video"),
|
|
221
|
+
className: cn(
|
|
222
|
+
LABEL_BASE,
|
|
223
|
+
mode === "video" ? "text-[#FFCC00]" : "text-white"
|
|
224
|
+
),
|
|
225
|
+
children: "Video"
|
|
226
|
+
}
|
|
227
|
+
),
|
|
228
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
229
|
+
"button",
|
|
230
|
+
{
|
|
231
|
+
type: "button",
|
|
232
|
+
role: "tab",
|
|
233
|
+
"aria-selected": mode === "photo",
|
|
234
|
+
disabled: modeDisabled,
|
|
235
|
+
onClick: () => onModeChange("photo"),
|
|
236
|
+
className: cn(
|
|
237
|
+
LABEL_BASE,
|
|
238
|
+
mode === "photo" ? "text-[#FFCC00]" : "text-white"
|
|
239
|
+
),
|
|
240
|
+
children: "Photo"
|
|
241
|
+
}
|
|
242
|
+
),
|
|
243
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
244
|
+
"button",
|
|
245
|
+
{
|
|
246
|
+
type: "button",
|
|
247
|
+
"aria-label": "Upload photos or videos from this device",
|
|
248
|
+
disabled: modeDisabled || uploadDisabled,
|
|
249
|
+
onClick: onUpload,
|
|
250
|
+
className: cn(LABEL_BASE, "text-white active:text-[#FFCC00]"),
|
|
251
|
+
children: "Upload"
|
|
252
|
+
}
|
|
253
|
+
),
|
|
254
|
+
extraModes.map((extra) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
255
|
+
"button",
|
|
256
|
+
{
|
|
257
|
+
type: "button",
|
|
258
|
+
"aria-label": extra.label,
|
|
259
|
+
disabled: modeDisabled,
|
|
260
|
+
onClick: extra.onSelect,
|
|
261
|
+
className: cn(LABEL_BASE, "text-white active:text-[#FFCC00]"),
|
|
262
|
+
children: extra.label
|
|
263
|
+
},
|
|
264
|
+
extra.id
|
|
265
|
+
))
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// src/components/ZoomRow.tsx
|
|
272
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
273
|
+
function formatFactor(factor) {
|
|
274
|
+
return factor < 1 ? `.${String(Math.round(factor * 10))}` : String(Math.round(factor * 10) / 10);
|
|
275
|
+
}
|
|
276
|
+
function ZoomRow({ options, value, onSelect }) {
|
|
277
|
+
if (options.length < 2) return null;
|
|
278
|
+
const active = options.reduce(
|
|
279
|
+
(best, opt) => Math.abs(opt - value) < Math.abs(best - value) ? opt : best
|
|
280
|
+
);
|
|
281
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex items-center justify-center gap-3", children: options.map((opt) => {
|
|
282
|
+
const isActive = opt === active;
|
|
283
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
284
|
+
"button",
|
|
285
|
+
{
|
|
286
|
+
type: "button",
|
|
287
|
+
onClick: () => onSelect(opt),
|
|
288
|
+
"aria-label": `Zoom ${formatFactor(opt)}x`,
|
|
289
|
+
"aria-pressed": isActive,
|
|
290
|
+
className: cn(
|
|
291
|
+
"flex touch-manipulation items-center justify-center rounded-full font-semibold transition-all duration-200",
|
|
292
|
+
isActive ? "h-10 w-10 bg-black/45 text-[13px] text-[#FFCC00]" : "h-8 w-8 bg-black/35 text-[12px] text-white"
|
|
293
|
+
),
|
|
294
|
+
children: [
|
|
295
|
+
formatFactor(opt),
|
|
296
|
+
isActive && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-[10px]", children: "\xD7" })
|
|
297
|
+
]
|
|
298
|
+
},
|
|
299
|
+
opt
|
|
300
|
+
);
|
|
301
|
+
}) });
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/components/OptionsGridPanel.tsx
|
|
305
|
+
var import_jsx_runtime4 = require("react/jsx-runtime");
|
|
306
|
+
function OptionsGridPanel({
|
|
307
|
+
open,
|
|
308
|
+
onClose,
|
|
309
|
+
tiles
|
|
310
|
+
}) {
|
|
311
|
+
if (!open) return null;
|
|
312
|
+
return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "absolute inset-0 z-40", children: [
|
|
313
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
314
|
+
"button",
|
|
315
|
+
{
|
|
316
|
+
type: "button",
|
|
317
|
+
"aria-label": "Close camera options",
|
|
318
|
+
onClick: onClose,
|
|
319
|
+
className: "absolute inset-0 bg-black/70"
|
|
320
|
+
}
|
|
321
|
+
),
|
|
322
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-x-3 bottom-3 mb-safe rounded-[2.5rem] bg-[#3a3a3c]/95 px-6 py-8 shadow-2xl", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "grid grid-cols-3 gap-x-4 gap-y-7", children: tiles.map((tile) => /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: "flex flex-col items-center gap-2.5", children: [
|
|
323
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
|
|
324
|
+
"button",
|
|
325
|
+
{
|
|
326
|
+
type: "button",
|
|
327
|
+
onClick: tile.onPress,
|
|
328
|
+
disabled: tile.disabled,
|
|
329
|
+
"aria-label": tile.label,
|
|
330
|
+
"aria-pressed": tile.active === true,
|
|
331
|
+
className: cn(
|
|
332
|
+
"flex h-16 w-16 touch-manipulation items-center justify-center rounded-full bg-[#2c2c2e] transition-colors",
|
|
333
|
+
tile.active ? "text-[#FFCC00]" : "text-white",
|
|
334
|
+
tile.disabled && "opacity-40"
|
|
335
|
+
),
|
|
336
|
+
children: tile.icon
|
|
337
|
+
}
|
|
338
|
+
),
|
|
339
|
+
/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-[13px] font-semibold uppercase tracking-[0.14em] text-white", children: tile.valueLabel ? `${tile.label} ${tile.valueLabel}` : tile.label })
|
|
340
|
+
] }, tile.id)) }) })
|
|
341
|
+
] });
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/components/CaptureSheet.tsx
|
|
345
|
+
var import_lucide_react = require("lucide-react");
|
|
346
|
+
var import_jsx_runtime5 = require("react/jsx-runtime");
|
|
347
|
+
function CaptureSheet({
|
|
348
|
+
open,
|
|
349
|
+
onClose,
|
|
350
|
+
variant = "content",
|
|
351
|
+
icon,
|
|
352
|
+
title,
|
|
353
|
+
body,
|
|
354
|
+
actions = [],
|
|
355
|
+
busyLabel = "Working\u2026"
|
|
356
|
+
}) {
|
|
357
|
+
if (!open) return null;
|
|
358
|
+
return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "absolute inset-0 z-50 flex flex-col justify-end", children: [
|
|
359
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
360
|
+
"button",
|
|
361
|
+
{
|
|
362
|
+
type: "button",
|
|
363
|
+
"aria-label": "Dismiss",
|
|
364
|
+
onClick: onClose,
|
|
365
|
+
className: "absolute inset-0 bg-black/30"
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "relative mx-2 mb-2 mb-safe rounded-[2rem] bg-[#f2f2f7] px-6 pb-6 pt-5 text-black shadow-2xl", children: [
|
|
369
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
370
|
+
"button",
|
|
371
|
+
{
|
|
372
|
+
type: "button",
|
|
373
|
+
onClick: onClose,
|
|
374
|
+
"aria-label": "Close",
|
|
375
|
+
className: "absolute right-4 top-4 flex h-9 w-9 items-center justify-center rounded-full bg-black/5 text-black/70 transition-colors hover:bg-black/10",
|
|
376
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react.X, { className: "h-5 w-5", strokeWidth: 2.5 })
|
|
377
|
+
}
|
|
378
|
+
),
|
|
379
|
+
variant === "busy" ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex min-h-[220px] items-center justify-center gap-2.5", children: [
|
|
380
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react.Loader2, { className: "h-5 w-5 animate-spin text-black/50" }),
|
|
381
|
+
/* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-[17px] font-medium text-black/80", children: busyLabel })
|
|
382
|
+
] }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "pt-4", children: [
|
|
383
|
+
icon && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mb-5 text-[#0a84ff]", children: icon }),
|
|
384
|
+
title && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h2", { className: "mb-2 text-[26px] font-bold leading-tight", children: title }),
|
|
385
|
+
body && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "text-[17px] leading-snug text-black/85", children: body }),
|
|
386
|
+
actions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "mt-7 flex flex-col gap-3", children: actions.map((action) => /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
|
|
387
|
+
"button",
|
|
388
|
+
{
|
|
389
|
+
type: "button",
|
|
390
|
+
onClick: action.onPress,
|
|
391
|
+
className: cn(
|
|
392
|
+
"h-[50px] w-full touch-manipulation rounded-full text-[17px] font-semibold transition-transform active:scale-[0.98]",
|
|
393
|
+
(action.kind ?? "primary") === "primary" ? "bg-[#0a84ff] text-white" : "bg-black/[0.06] text-[#0a84ff]"
|
|
394
|
+
),
|
|
395
|
+
children: action.label
|
|
396
|
+
},
|
|
397
|
+
action.label
|
|
398
|
+
)) })
|
|
399
|
+
] })
|
|
400
|
+
] })
|
|
401
|
+
] });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/components/GridOverlay.tsx
|
|
405
|
+
var import_jsx_runtime6 = require("react/jsx-runtime");
|
|
406
|
+
function GridOverlay({ visible }) {
|
|
407
|
+
if (!visible) return null;
|
|
408
|
+
return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { "aria-hidden": true, className: "pointer-events-none absolute inset-0 z-10", children: [
|
|
409
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute inset-y-0 left-1/3 w-px bg-white/40" }),
|
|
410
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute inset-y-0 left-2/3 w-px bg-white/40" }),
|
|
411
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute inset-x-0 top-1/3 h-px bg-white/40" }),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "absolute inset-x-0 top-2/3 h-px bg-white/40" })
|
|
413
|
+
] });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/components/CountdownOverlay.tsx
|
|
417
|
+
var import_jsx_runtime7 = require("react/jsx-runtime");
|
|
418
|
+
function CountdownOverlay({ seconds }) {
|
|
419
|
+
if (seconds === null || seconds <= 0) return null;
|
|
420
|
+
return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("div", { className: "pointer-events-none absolute inset-0 z-30 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
|
|
421
|
+
"span",
|
|
422
|
+
{
|
|
423
|
+
className: "text-[120px] font-light text-white drop-shadow-lg [@starting-style]:scale-125 [@starting-style]:opacity-0 transition-all duration-300",
|
|
424
|
+
children: seconds
|
|
425
|
+
},
|
|
426
|
+
seconds
|
|
427
|
+
) });
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/components/CameraCapture.tsx
|
|
431
|
+
var import_jsx_runtime8 = require("react/jsx-runtime");
|
|
432
|
+
function formatElapsed(totalSeconds) {
|
|
433
|
+
const m = Math.floor(totalSeconds / 60);
|
|
434
|
+
const s = String(totalSeconds % 60).padStart(2, "0");
|
|
435
|
+
return `${m}:${s}`;
|
|
436
|
+
}
|
|
437
|
+
var ASPECT_CYCLE = ["full", "4:3", "1:1", "16:9"];
|
|
438
|
+
function CameraCapture({
|
|
439
|
+
engine,
|
|
440
|
+
cloud,
|
|
441
|
+
mode,
|
|
442
|
+
onModeChange,
|
|
443
|
+
preview,
|
|
444
|
+
onClose,
|
|
445
|
+
blockedSheet,
|
|
446
|
+
controlsHidden = false,
|
|
447
|
+
shutterDisabled = false,
|
|
448
|
+
slots = {}
|
|
449
|
+
}) {
|
|
450
|
+
const [optionsOpen, setOptionsOpen] = (0, import_react2.useState)(false);
|
|
451
|
+
const [gridOn, setGridOn] = (0, import_react2.useState)(false);
|
|
452
|
+
const [timerSetting, setTimerSetting] = (0, import_react2.useState)(0);
|
|
453
|
+
const [aspect, setAspect] = (0, import_react2.useState)("full");
|
|
454
|
+
const [exposureOpen, setExposureOpen] = (0, import_react2.useState)(false);
|
|
455
|
+
const [countdown, setCountdown] = (0, import_react2.useState)(null);
|
|
456
|
+
const [blockedDismissed, setBlockedDismissed] = (0, import_react2.useState)(false);
|
|
457
|
+
const countdownRef = (0, import_react2.useRef)(null);
|
|
458
|
+
const controls = useTrackControls(engine.stream);
|
|
459
|
+
const clearCountdown = (0, import_react2.useCallback)(() => {
|
|
460
|
+
if (countdownRef.current) clearInterval(countdownRef.current);
|
|
461
|
+
countdownRef.current = null;
|
|
462
|
+
setCountdown(null);
|
|
463
|
+
}, []);
|
|
464
|
+
(0, import_react2.useEffect)(() => clearCountdown, [clearCountdown]);
|
|
465
|
+
const onShutter = (0, import_react2.useCallback)(() => {
|
|
466
|
+
if (mode === "video") {
|
|
467
|
+
if (engine.recording) engine.onStopRecording();
|
|
468
|
+
else engine.onStartRecording();
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (countdown !== null) {
|
|
472
|
+
clearCountdown();
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
if (timerSetting === 0) {
|
|
476
|
+
engine.onCapturePhoto({ aspect });
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
let remaining = timerSetting;
|
|
480
|
+
setCountdown(remaining);
|
|
481
|
+
countdownRef.current = setInterval(() => {
|
|
482
|
+
remaining -= 1;
|
|
483
|
+
if (remaining <= 0) {
|
|
484
|
+
clearCountdown();
|
|
485
|
+
engine.onCapturePhoto({ aspect });
|
|
486
|
+
} else {
|
|
487
|
+
setCountdown(remaining);
|
|
488
|
+
}
|
|
489
|
+
}, 1e3);
|
|
490
|
+
}, [mode, engine, countdown, timerSetting, aspect, clearCountdown]);
|
|
491
|
+
const cycleTimer = (0, import_react2.useCallback)(() => {
|
|
492
|
+
setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0);
|
|
493
|
+
}, []);
|
|
494
|
+
const coreTiles = [
|
|
495
|
+
...controls.torchSupported ? [
|
|
496
|
+
{
|
|
497
|
+
id: "flash",
|
|
498
|
+
label: "Flash",
|
|
499
|
+
icon: controls.torchOn ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Zap, { className: "h-6 w-6", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.ZapOff, { className: "h-6 w-6" }),
|
|
500
|
+
active: controls.torchOn,
|
|
501
|
+
onPress: controls.toggleTorch
|
|
502
|
+
}
|
|
503
|
+
] : [],
|
|
504
|
+
{
|
|
505
|
+
id: "timer",
|
|
506
|
+
label: "Timer",
|
|
507
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Timer, { className: "h-6 w-6" }),
|
|
508
|
+
active: timerSetting !== 0,
|
|
509
|
+
valueLabel: timerSetting === 0 ? void 0 : `${timerSetting}s`,
|
|
510
|
+
onPress: cycleTimer
|
|
511
|
+
},
|
|
512
|
+
{
|
|
513
|
+
id: "grid",
|
|
514
|
+
label: "Grid",
|
|
515
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Grid3x3, { className: "h-6 w-6" }),
|
|
516
|
+
active: gridOn,
|
|
517
|
+
onPress: () => setGridOn((g) => !g)
|
|
518
|
+
},
|
|
519
|
+
// Aspect applies to PHOTO output (center-cropped from the full sensor).
|
|
520
|
+
{
|
|
521
|
+
id: "aspect",
|
|
522
|
+
label: "Aspect",
|
|
523
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Proportions, { className: "h-6 w-6" }),
|
|
524
|
+
active: aspect !== "full",
|
|
525
|
+
valueLabel: aspect === "full" ? void 0 : aspect,
|
|
526
|
+
onPress: () => setAspect(
|
|
527
|
+
(a) => ASPECT_CYCLE[(ASPECT_CYCLE.indexOf(a) + 1) % ASPECT_CYCLE.length] ?? "full"
|
|
528
|
+
)
|
|
529
|
+
},
|
|
530
|
+
...controls.exposureSupported ? [
|
|
531
|
+
{
|
|
532
|
+
id: "exposure",
|
|
533
|
+
label: "Exposure",
|
|
534
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.SunMedium, { className: "h-6 w-6" }),
|
|
535
|
+
active: controls.exposure !== 0 || exposureOpen,
|
|
536
|
+
valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
|
|
537
|
+
onPress: () => setExposureOpen((o) => !o)
|
|
538
|
+
}
|
|
539
|
+
] : []
|
|
540
|
+
];
|
|
541
|
+
const tiles = [...coreTiles, ...slots.optionTiles ?? []];
|
|
542
|
+
const blocked = engine.blocked !== null;
|
|
543
|
+
return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
|
|
544
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute inset-0", children: preview }),
|
|
545
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(GridOverlay, { visible: gridOn && !blocked }),
|
|
546
|
+
mode === "photo" && aspect !== "full" && !blocked && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
547
|
+
"div",
|
|
548
|
+
{
|
|
549
|
+
"aria-hidden": true,
|
|
550
|
+
className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
|
|
551
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
552
|
+
"div",
|
|
553
|
+
{
|
|
554
|
+
className: "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]",
|
|
555
|
+
style: {
|
|
556
|
+
aspectRatio: aspect === "1:1" ? "1 / 1" : aspect === "4:3" ? "3 / 4" : "9 / 16",
|
|
557
|
+
width: aspect === "16:9" ? "100%" : void 0,
|
|
558
|
+
height: aspect === "16:9" ? void 0 : "70%",
|
|
559
|
+
maxWidth: "100%",
|
|
560
|
+
maxHeight: "100%"
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
)
|
|
564
|
+
}
|
|
565
|
+
),
|
|
566
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(CountdownOverlay, { seconds: countdown }),
|
|
567
|
+
!controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute inset-x-0 top-0 z-20 bg-black/65 pt-safe backdrop-blur-[2px]", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex h-11 items-center gap-0.5 px-1.5", children: [
|
|
568
|
+
onClose ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
569
|
+
"button",
|
|
570
|
+
{
|
|
571
|
+
type: "button",
|
|
572
|
+
onClick: onClose,
|
|
573
|
+
"aria-label": "Close camera",
|
|
574
|
+
className: "flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full text-white transition-colors hover:bg-white/10",
|
|
575
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.X, { className: "h-5 w-5" })
|
|
576
|
+
}
|
|
577
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "w-10 shrink-0" }),
|
|
578
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "min-w-0 flex-1", children: slots.topBarCenter }),
|
|
579
|
+
controls.torchSupported && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
580
|
+
"button",
|
|
581
|
+
{
|
|
582
|
+
type: "button",
|
|
583
|
+
onClick: controls.toggleTorch,
|
|
584
|
+
"aria-label": controls.torchOn ? "Turn flash off" : "Turn flash on",
|
|
585
|
+
"aria-pressed": controls.torchOn,
|
|
586
|
+
className: cn(
|
|
587
|
+
"flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full transition-colors",
|
|
588
|
+
controls.torchOn ? "text-[#FFCC00]" : "text-white hover:bg-white/10"
|
|
589
|
+
),
|
|
590
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
591
|
+
import_lucide_react2.Zap,
|
|
592
|
+
{
|
|
593
|
+
className: "h-5 w-5",
|
|
594
|
+
fill: controls.torchOn ? "currentColor" : "none"
|
|
595
|
+
}
|
|
596
|
+
)
|
|
597
|
+
}
|
|
598
|
+
),
|
|
599
|
+
slots.topBarTrailing,
|
|
600
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
601
|
+
"button",
|
|
602
|
+
{
|
|
603
|
+
type: "button",
|
|
604
|
+
onClick: () => setOptionsOpen((o) => !o),
|
|
605
|
+
"aria-label": "More camera options",
|
|
606
|
+
"aria-expanded": optionsOpen,
|
|
607
|
+
className: cn(
|
|
608
|
+
"flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full transition-colors",
|
|
609
|
+
optionsOpen ? "bg-white/20 text-white" : "text-white hover:bg-white/10"
|
|
610
|
+
),
|
|
611
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.Grip, { className: "h-5 w-5" })
|
|
612
|
+
}
|
|
613
|
+
)
|
|
614
|
+
] }) }),
|
|
615
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "pointer-events-none absolute inset-x-0 top-[50px] z-20 mt-safe flex flex-col items-center gap-1.5", children: [
|
|
616
|
+
engine.recording && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "flex items-center gap-2 rounded-full bg-black/60 px-3 py-1.5 text-sm font-medium text-white", children: [
|
|
617
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "h-2.5 w-2.5 animate-pulse rounded-full bg-[#FF3B30]" }),
|
|
618
|
+
formatElapsed(engine.recordElapsedSeconds)
|
|
619
|
+
] }),
|
|
620
|
+
slots.statusChips
|
|
621
|
+
] }),
|
|
622
|
+
!controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "absolute inset-x-0 bottom-0 z-20", children: [
|
|
623
|
+
!blocked && controls.zoomOptions.length >= 2 && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mb-2", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
624
|
+
ZoomRow,
|
|
625
|
+
{
|
|
626
|
+
options: controls.zoomOptions,
|
|
627
|
+
value: controls.zoom,
|
|
628
|
+
onSelect: controls.setZoom
|
|
629
|
+
}
|
|
630
|
+
) }),
|
|
631
|
+
slots.aboveBar && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "mb-1.5 px-2", children: slots.aboveBar }),
|
|
632
|
+
exposureOpen && controls.exposureSupported && controls.exposureRange && /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "mx-auto mb-3 flex w-64 items-center gap-3 rounded-full bg-black/55 px-4 py-2", children: [
|
|
633
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.SunMedium, { className: "h-4 w-4 shrink-0 text-[#FFCC00]" }),
|
|
634
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
635
|
+
"input",
|
|
636
|
+
{
|
|
637
|
+
type: "range",
|
|
638
|
+
min: controls.exposureRange.min,
|
|
639
|
+
max: controls.exposureRange.max,
|
|
640
|
+
step: controls.exposureRange.step,
|
|
641
|
+
value: controls.exposure,
|
|
642
|
+
onChange: (e) => controls.setExposure(Number(e.target.value)),
|
|
643
|
+
"aria-label": "Exposure compensation",
|
|
644
|
+
className: "w-full accent-[#FFCC00]"
|
|
645
|
+
}
|
|
646
|
+
),
|
|
647
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("span", { className: "w-8 shrink-0 text-right text-xs tabular-nums text-white", children: [
|
|
648
|
+
controls.exposure > 0 ? "+" : "",
|
|
649
|
+
controls.exposure
|
|
650
|
+
] })
|
|
651
|
+
] }),
|
|
652
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "bg-black/65 px-3 pb-safe backdrop-blur-[2px]", children: [
|
|
653
|
+
slots.aboveModeSelector,
|
|
654
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "relative flex items-center justify-center py-1.5", children: [
|
|
655
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
656
|
+
ModeSelector,
|
|
657
|
+
{
|
|
658
|
+
mode,
|
|
659
|
+
onModeChange,
|
|
660
|
+
onUpload: engine.onUpload,
|
|
661
|
+
modeDisabled: engine.recording,
|
|
662
|
+
uploadDisabled: shutterDisabled && !blocked,
|
|
663
|
+
extraModes: slots.extraModes
|
|
664
|
+
}
|
|
665
|
+
),
|
|
666
|
+
slots.modeRowTrailing && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "absolute right-0", children: slots.modeRowTrailing })
|
|
667
|
+
] }),
|
|
668
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "flex items-center justify-between px-2 pb-1.5 pt-0.5", children: [
|
|
669
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex w-14 justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
670
|
+
"button",
|
|
671
|
+
{
|
|
672
|
+
type: "button",
|
|
673
|
+
onClick: cloud.onOpenLibrary,
|
|
674
|
+
"aria-label": "Open your media library",
|
|
675
|
+
className: "h-11 w-11 touch-manipulation overflow-hidden rounded-xl bg-white/10 ring-1 ring-white/25 transition-transform active:scale-95",
|
|
676
|
+
children: cloud.recentsThumb ?? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "block h-full w-full bg-white/5" })
|
|
677
|
+
}
|
|
678
|
+
) }),
|
|
679
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
680
|
+
ShutterButton,
|
|
681
|
+
{
|
|
682
|
+
mode,
|
|
683
|
+
recording: engine.recording,
|
|
684
|
+
disabled: shutterDisabled || blocked,
|
|
685
|
+
onPress: onShutter
|
|
686
|
+
}
|
|
687
|
+
),
|
|
688
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)("div", { className: "flex w-14 justify-end", children: engine.onFlipCamera ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
689
|
+
"button",
|
|
690
|
+
{
|
|
691
|
+
type: "button",
|
|
692
|
+
onClick: engine.onFlipCamera,
|
|
693
|
+
"aria-label": "Switch camera",
|
|
694
|
+
className: "flex h-11 w-11 touch-manipulation items-center justify-center rounded-full bg-white/15 text-white transition-transform active:rotate-180 active:scale-95 duration-300",
|
|
695
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react2.RefreshCw, { className: "h-5 w-5" })
|
|
696
|
+
}
|
|
697
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "h-11 w-11" }) })
|
|
698
|
+
] })
|
|
699
|
+
] })
|
|
700
|
+
] }),
|
|
701
|
+
/* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
702
|
+
OptionsGridPanel,
|
|
703
|
+
{
|
|
704
|
+
open: optionsOpen && !controlsHidden,
|
|
705
|
+
onClose: () => setOptionsOpen(false),
|
|
706
|
+
tiles
|
|
707
|
+
}
|
|
708
|
+
),
|
|
709
|
+
blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(
|
|
710
|
+
CaptureSheet,
|
|
711
|
+
{
|
|
712
|
+
open: true,
|
|
713
|
+
onClose: () => setBlockedDismissed(true),
|
|
714
|
+
body: blockedSheet.body,
|
|
715
|
+
title: "Camera unavailable",
|
|
716
|
+
actions: blockedSheet.actions
|
|
717
|
+
}
|
|
718
|
+
),
|
|
719
|
+
slots.overlays
|
|
720
|
+
] });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// src/components/ImageEditSheet.tsx
|
|
724
|
+
var import_react3 = require("react");
|
|
725
|
+
var import_lucide_react3 = require("lucide-react");
|
|
726
|
+
var import_jsx_runtime9 = require("react/jsx-runtime");
|
|
727
|
+
var ASPECTS = [
|
|
728
|
+
{ id: "free", label: "Free", ratio: null },
|
|
729
|
+
{ id: "1:1", label: "Square", ratio: 1 },
|
|
730
|
+
{ id: "4:3", label: "4:3", ratio: 4 / 3 },
|
|
731
|
+
{ id: "16:9", label: "16:9", ratio: 16 / 9 }
|
|
732
|
+
];
|
|
733
|
+
var FULL_CROP = { x: 0, y: 0, w: 1, h: 1 };
|
|
734
|
+
var MIN_CROP = 0.08;
|
|
735
|
+
function ImageEditSheet({
|
|
736
|
+
open,
|
|
737
|
+
src,
|
|
738
|
+
onClose,
|
|
739
|
+
onSave
|
|
740
|
+
}) {
|
|
741
|
+
const [rotation, setRotation] = (0, import_react3.useState)(0);
|
|
742
|
+
const [flipped, setFlipped] = (0, import_react3.useState)(false);
|
|
743
|
+
const [aspect, setAspect] = (0, import_react3.useState)("free");
|
|
744
|
+
const [crop, setCrop] = (0, import_react3.useState)(FULL_CROP);
|
|
745
|
+
const [saving, setSaving] = (0, import_react3.useState)(false);
|
|
746
|
+
const stageRef = (0, import_react3.useRef)(null);
|
|
747
|
+
const imgRef = (0, import_react3.useRef)(null);
|
|
748
|
+
const dragRef = (0, import_react3.useRef)(null);
|
|
749
|
+
(0, import_react3.useEffect)(() => {
|
|
750
|
+
if (open) {
|
|
751
|
+
setRotation(0);
|
|
752
|
+
setFlipped(false);
|
|
753
|
+
setAspect("free");
|
|
754
|
+
setCrop(FULL_CROP);
|
|
755
|
+
setSaving(false);
|
|
756
|
+
}
|
|
757
|
+
}, [open, src]);
|
|
758
|
+
const applyAspect = (0, import_react3.useCallback)((preset) => {
|
|
759
|
+
setAspect(preset);
|
|
760
|
+
const ratio = ASPECTS.find((a) => a.id === preset)?.ratio ?? null;
|
|
761
|
+
if (ratio === null) return;
|
|
762
|
+
const img = imgRef.current;
|
|
763
|
+
if (!img || img.naturalWidth === 0) return;
|
|
764
|
+
const rotated = rotationSwapsAxes();
|
|
765
|
+
const iw = rotated ? img.naturalHeight : img.naturalWidth;
|
|
766
|
+
const ih = rotated ? img.naturalWidth : img.naturalHeight;
|
|
767
|
+
const imageRatio = iw / ih;
|
|
768
|
+
let w = 1;
|
|
769
|
+
let h = 1;
|
|
770
|
+
if (ratio > imageRatio) h = imageRatio / ratio;
|
|
771
|
+
else w = ratio / imageRatio;
|
|
772
|
+
setCrop({ x: (1 - w) / 2, y: (1 - h) / 2, w, h });
|
|
773
|
+
function rotationSwapsAxes() {
|
|
774
|
+
return rotation % 180 !== 0;
|
|
775
|
+
}
|
|
776
|
+
}, [rotation]);
|
|
777
|
+
const onPointerDown = (0, import_react3.useCallback)(
|
|
778
|
+
(kind) => (e) => {
|
|
779
|
+
e.preventDefault();
|
|
780
|
+
e.stopPropagation();
|
|
781
|
+
e.target.setPointerCapture(e.pointerId);
|
|
782
|
+
dragRef.current = {
|
|
783
|
+
kind,
|
|
784
|
+
startX: e.clientX,
|
|
785
|
+
startY: e.clientY,
|
|
786
|
+
startCrop: crop
|
|
787
|
+
};
|
|
788
|
+
},
|
|
789
|
+
[crop]
|
|
790
|
+
);
|
|
791
|
+
const onPointerMove = (0, import_react3.useCallback)(
|
|
792
|
+
(e) => {
|
|
793
|
+
const drag = dragRef.current;
|
|
794
|
+
const img = imgRef.current;
|
|
795
|
+
if (!drag || !img) return;
|
|
796
|
+
const rect = img.getBoundingClientRect();
|
|
797
|
+
if (rect.width === 0 || rect.height === 0) return;
|
|
798
|
+
const dx = (e.clientX - drag.startX) / rect.width;
|
|
799
|
+
const dy = (e.clientY - drag.startY) / rect.height;
|
|
800
|
+
const c = { ...drag.startCrop };
|
|
801
|
+
const ratio = ASPECTS.find((a) => a.id === aspect)?.ratio ?? null;
|
|
802
|
+
const frameRatio = rect.width / rect.height;
|
|
803
|
+
if (drag.kind === "move") {
|
|
804
|
+
c.x = Math.min(1 - c.w, Math.max(0, c.x + dx));
|
|
805
|
+
c.y = Math.min(1 - c.h, Math.max(0, c.y + dy));
|
|
806
|
+
} else {
|
|
807
|
+
const left = drag.kind === "nw" || drag.kind === "sw";
|
|
808
|
+
const top = drag.kind === "nw" || drag.kind === "ne";
|
|
809
|
+
let x2 = c.x + c.w;
|
|
810
|
+
let y2 = c.y + c.h;
|
|
811
|
+
if (left) c.x = Math.min(x2 - MIN_CROP, Math.max(0, c.x + dx));
|
|
812
|
+
else x2 = Math.max(c.x + MIN_CROP, Math.min(1, x2 + dx));
|
|
813
|
+
if (top) c.y = Math.min(y2 - MIN_CROP, Math.max(0, c.y + dy));
|
|
814
|
+
else y2 = Math.max(c.y + MIN_CROP, Math.min(1, y2 + dy));
|
|
815
|
+
c.w = x2 - c.x;
|
|
816
|
+
c.h = y2 - c.y;
|
|
817
|
+
if (ratio !== null) {
|
|
818
|
+
const targetH = c.w * frameRatio / ratio;
|
|
819
|
+
if (top) c.y = y2 - Math.min(targetH, y2);
|
|
820
|
+
c.h = Math.min(targetH, top ? y2 - c.y : 1 - c.y);
|
|
821
|
+
c.w = c.h * ratio / frameRatio;
|
|
822
|
+
if (left) c.x = x2 - c.w;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
setCrop(c);
|
|
826
|
+
},
|
|
827
|
+
[aspect]
|
|
828
|
+
);
|
|
829
|
+
const onPointerUp = (0, import_react3.useCallback)(() => {
|
|
830
|
+
dragRef.current = null;
|
|
831
|
+
}, []);
|
|
832
|
+
const save = (0, import_react3.useCallback)(() => {
|
|
833
|
+
const img = imgRef.current;
|
|
834
|
+
if (!img || img.naturalWidth === 0) return;
|
|
835
|
+
setSaving(true);
|
|
836
|
+
try {
|
|
837
|
+
const swap = rotation % 180 !== 0;
|
|
838
|
+
const outW = Math.round((swap ? img.naturalHeight : img.naturalWidth) * crop.w);
|
|
839
|
+
const outH = Math.round((swap ? img.naturalWidth : img.naturalHeight) * crop.h);
|
|
840
|
+
const canvas = document.createElement("canvas");
|
|
841
|
+
canvas.width = Math.max(1, outW);
|
|
842
|
+
canvas.height = Math.max(1, outH);
|
|
843
|
+
const ctx = canvas.getContext("2d");
|
|
844
|
+
if (!ctx) throw new Error("no 2d context");
|
|
845
|
+
const rw = swap ? img.naturalHeight : img.naturalWidth;
|
|
846
|
+
const rh = swap ? img.naturalWidth : img.naturalHeight;
|
|
847
|
+
ctx.translate(-crop.x * rw, -crop.y * rh);
|
|
848
|
+
ctx.translate(rw / 2, rh / 2);
|
|
849
|
+
ctx.rotate(rotation * Math.PI / 180);
|
|
850
|
+
if (flipped) ctx.scale(-1, 1);
|
|
851
|
+
ctx.drawImage(img, -img.naturalWidth / 2, -img.naturalHeight / 2);
|
|
852
|
+
canvas.toBlob(
|
|
853
|
+
(blob) => {
|
|
854
|
+
setSaving(false);
|
|
855
|
+
if (blob) {
|
|
856
|
+
onSave(blob);
|
|
857
|
+
onClose();
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
"image/jpeg",
|
|
861
|
+
0.92
|
|
862
|
+
);
|
|
863
|
+
} catch (err) {
|
|
864
|
+
console.error("[capture-camera] edit save failed", err);
|
|
865
|
+
setSaving(false);
|
|
866
|
+
}
|
|
867
|
+
}, [rotation, flipped, crop, onSave, onClose]);
|
|
868
|
+
if (!open || !src) return null;
|
|
869
|
+
return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
|
|
870
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex shrink-0 items-center justify-between px-4 pt-safe", children: [
|
|
871
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
872
|
+
"button",
|
|
873
|
+
{
|
|
874
|
+
type: "button",
|
|
875
|
+
onClick: onClose,
|
|
876
|
+
"aria-label": "Cancel editing",
|
|
877
|
+
className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-medium text-white",
|
|
878
|
+
children: [
|
|
879
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.X, { className: "h-5 w-5" }),
|
|
880
|
+
"Cancel"
|
|
881
|
+
]
|
|
882
|
+
}
|
|
883
|
+
),
|
|
884
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "text-[15px] font-semibold text-white/90", children: "Edit" }),
|
|
885
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)(
|
|
886
|
+
"button",
|
|
887
|
+
{
|
|
888
|
+
type: "button",
|
|
889
|
+
onClick: save,
|
|
890
|
+
disabled: saving,
|
|
891
|
+
"aria-label": "Save edited image",
|
|
892
|
+
className: "flex h-11 items-center gap-1.5 rounded-full px-3 text-[15px] font-semibold text-[#FFCC00] disabled:opacity-50",
|
|
893
|
+
children: [
|
|
894
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.Check, { className: "h-5 w-5" }),
|
|
895
|
+
"Save"
|
|
896
|
+
]
|
|
897
|
+
}
|
|
898
|
+
)
|
|
899
|
+
] }),
|
|
900
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
901
|
+
"div",
|
|
902
|
+
{
|
|
903
|
+
ref: stageRef,
|
|
904
|
+
className: "relative flex min-h-0 flex-1 items-center justify-center overflow-hidden p-4",
|
|
905
|
+
onPointerMove,
|
|
906
|
+
onPointerUp,
|
|
907
|
+
onPointerCancel: onPointerUp,
|
|
908
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "relative max-h-full max-w-full", children: [
|
|
909
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
910
|
+
"img",
|
|
911
|
+
{
|
|
912
|
+
ref: imgRef,
|
|
913
|
+
src,
|
|
914
|
+
alt: "Image being edited",
|
|
915
|
+
draggable: false,
|
|
916
|
+
className: "max-h-[62dvh] max-w-full select-none object-contain",
|
|
917
|
+
style: {
|
|
918
|
+
transform: `rotate(${rotation}deg) ${flipped ? "scaleX(-1)" : ""}`
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
),
|
|
922
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
923
|
+
"div",
|
|
924
|
+
{
|
|
925
|
+
role: "presentation",
|
|
926
|
+
onPointerDown: onPointerDown("move"),
|
|
927
|
+
className: "absolute cursor-move touch-none border-2 border-white shadow-[0_0_0_9999px_rgba(0,0,0,0.55)]",
|
|
928
|
+
style: {
|
|
929
|
+
left: `${crop.x * 100}%`,
|
|
930
|
+
top: `${crop.y * 100}%`,
|
|
931
|
+
width: `${crop.w * 100}%`,
|
|
932
|
+
height: `${crop.h * 100}%`
|
|
933
|
+
},
|
|
934
|
+
children: ["nw", "ne", "sw", "se"].map((corner) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
935
|
+
"span",
|
|
936
|
+
{
|
|
937
|
+
role: "presentation",
|
|
938
|
+
onPointerDown: onPointerDown(corner),
|
|
939
|
+
className: cn(
|
|
940
|
+
"absolute h-6 w-6 touch-none",
|
|
941
|
+
corner === "nw" && "-left-1.5 -top-1.5 border-l-4 border-t-4 cursor-nwse-resize",
|
|
942
|
+
corner === "ne" && "-right-1.5 -top-1.5 border-r-4 border-t-4 cursor-nesw-resize",
|
|
943
|
+
corner === "sw" && "-bottom-1.5 -left-1.5 border-b-4 border-l-4 cursor-nesw-resize",
|
|
944
|
+
corner === "se" && "-bottom-1.5 -right-1.5 border-b-4 border-r-4 cursor-nwse-resize",
|
|
945
|
+
"border-white"
|
|
946
|
+
)
|
|
947
|
+
},
|
|
948
|
+
corner
|
|
949
|
+
))
|
|
950
|
+
}
|
|
951
|
+
)
|
|
952
|
+
] })
|
|
953
|
+
}
|
|
954
|
+
),
|
|
955
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "shrink-0 pb-safe", children: [
|
|
956
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex items-center justify-center gap-2 pb-2", children: ASPECTS.map((a) => /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
957
|
+
"button",
|
|
958
|
+
{
|
|
959
|
+
type: "button",
|
|
960
|
+
onClick: () => applyAspect(a.id),
|
|
961
|
+
"aria-pressed": aspect === a.id,
|
|
962
|
+
className: cn(
|
|
963
|
+
"touch-manipulation rounded-full px-3.5 py-1.5 text-[12px] font-semibold uppercase tracking-wide transition-colors",
|
|
964
|
+
aspect === a.id ? "bg-white/20 text-[#FFCC00]" : "text-white/80"
|
|
965
|
+
),
|
|
966
|
+
children: a.label
|
|
967
|
+
},
|
|
968
|
+
a.id
|
|
969
|
+
)) }),
|
|
970
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "flex items-center justify-center gap-6 pb-4", children: [
|
|
971
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
972
|
+
"button",
|
|
973
|
+
{
|
|
974
|
+
type: "button",
|
|
975
|
+
onClick: () => {
|
|
976
|
+
setRotation((r) => (r + 270) % 360);
|
|
977
|
+
setCrop(FULL_CROP);
|
|
978
|
+
setAspect("free");
|
|
979
|
+
},
|
|
980
|
+
"aria-label": "Rotate left",
|
|
981
|
+
className: "flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10 text-white",
|
|
982
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.RotateCcw, { className: "h-5 w-5" })
|
|
983
|
+
}
|
|
984
|
+
),
|
|
985
|
+
/* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
|
|
986
|
+
"button",
|
|
987
|
+
{
|
|
988
|
+
type: "button",
|
|
989
|
+
onClick: () => setFlipped((f) => !f),
|
|
990
|
+
"aria-label": "Flip horizontally",
|
|
991
|
+
"aria-pressed": flipped,
|
|
992
|
+
className: cn(
|
|
993
|
+
"flex h-12 w-12 touch-manipulation items-center justify-center rounded-full bg-white/10",
|
|
994
|
+
flipped ? "text-[#FFCC00]" : "text-white"
|
|
995
|
+
),
|
|
996
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react3.FlipHorizontal2, { className: "h-5 w-5" })
|
|
997
|
+
}
|
|
998
|
+
)
|
|
999
|
+
] })
|
|
1000
|
+
] })
|
|
1001
|
+
] });
|
|
1002
|
+
}
|
|
1003
|
+
//# sourceMappingURL=react.cjs.map
|