@ai-matrx/capture 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.cjs CHANGED
@@ -22,11 +22,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var react_exports = {};
23
23
  __export(react_exports, {
24
24
  CameraCapture: () => CameraCapture,
25
+ CameraCaptureV3: () => CameraCaptureV3,
25
26
  CameraFeed: () => CameraFeed,
27
+ CaptureExpandingField: () => CaptureExpandingField,
26
28
  CaptureFilmstrip: () => CaptureFilmstrip,
29
+ CaptureRail: () => CaptureRail,
27
30
  CaptureSheet: () => CaptureSheet,
28
31
  CountdownOverlay: () => CountdownOverlay,
29
32
  GridOverlay: () => GridOverlay,
33
+ HoldShutter: () => HoldShutter,
30
34
  ImageEditSheet: () => ImageEditSheet,
31
35
  MediaViewer: () => MediaViewer,
32
36
  ModeSelector: () => ModeSelector,
@@ -1643,15 +1647,665 @@ function CameraCapture({
1643
1647
  ] });
1644
1648
  }
1645
1649
 
1646
- // src/components/CameraFeed.tsx
1650
+ // src/components/CameraCaptureV3.tsx
1651
+ var import_react8 = require("react");
1652
+ var import_lucide_react5 = require("lucide-react");
1653
+
1654
+ // src/components/CaptureRail.tsx
1647
1655
  var import_react6 = require("react");
1648
1656
  var import_jsx_runtime12 = require("react/jsx-runtime");
1657
+ function CaptureRail({
1658
+ actions,
1659
+ defaultExpanded = false,
1660
+ className
1661
+ }) {
1662
+ const [expanded, setExpanded] = (0, import_react6.useState)(defaultExpanded);
1663
+ const primary = actions.filter((a) => a.primary);
1664
+ const extra = actions.filter((a) => !a.primary);
1665
+ const shown = primary.length === 0 ? actions : expanded ? actions : primary;
1666
+ const collapsible = primary.length > 0 && extra.length > 0;
1667
+ if (actions.length === 0) return null;
1668
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1669
+ "div",
1670
+ {
1671
+ className: cn(
1672
+ "pointer-events-auto flex flex-col items-center gap-1",
1673
+ className
1674
+ ),
1675
+ children: [
1676
+ shown.map((action) => /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(RailButton, { action }, action.id)),
1677
+ collapsible ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1678
+ "button",
1679
+ {
1680
+ type: "button",
1681
+ onClick: () => setExpanded((open) => !open),
1682
+ "aria-expanded": expanded,
1683
+ "aria-label": expanded ? "Fewer options" : "More options",
1684
+ className: cn(
1685
+ "mt-0.5 flex h-9 w-9 touch-manipulation items-center justify-center",
1686
+ "rounded-full text-white/90 transition-colors hover:bg-white/10"
1687
+ ),
1688
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1689
+ Chevron,
1690
+ {
1691
+ className: cn(
1692
+ "h-5 w-5 transition-transform duration-200",
1693
+ expanded && "rotate-180"
1694
+ )
1695
+ }
1696
+ )
1697
+ }
1698
+ ) : null
1699
+ ]
1700
+ }
1701
+ );
1702
+ }
1703
+ function RailButton({ action }) {
1704
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsxs)(
1705
+ "button",
1706
+ {
1707
+ type: "button",
1708
+ onClick: action.onPress,
1709
+ disabled: action.disabled,
1710
+ "aria-label": action.label,
1711
+ "aria-pressed": action.active ?? void 0,
1712
+ className: cn(
1713
+ "flex w-12 shrink-0 touch-manipulation flex-col items-center justify-center gap-0.5",
1714
+ "rounded-2xl py-1.5 transition-colors",
1715
+ action.active ? "text-[#FFCC00]" : "text-white",
1716
+ action.disabled ? "opacity-40" : "hover:bg-white/10 active:bg-white/15"
1717
+ ),
1718
+ children: [
1719
+ /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)]", children: action.icon }),
1720
+ action.valueLabel ? /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("span", { className: "text-[10px] font-semibold leading-none tabular-nums drop-shadow-[0_1px_2px_rgba(0,0,0,0.7)]", children: action.valueLabel }) : null
1721
+ ]
1722
+ }
1723
+ );
1724
+ }
1725
+ function Chevron({ className }) {
1726
+ return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
1727
+ "svg",
1728
+ {
1729
+ viewBox: "0 0 24 24",
1730
+ fill: "none",
1731
+ stroke: "currentColor",
1732
+ strokeWidth: "2.5",
1733
+ strokeLinecap: "round",
1734
+ strokeLinejoin: "round",
1735
+ className,
1736
+ "aria-hidden": "true",
1737
+ children: /* @__PURE__ */ (0, import_jsx_runtime12.jsx)("path", { d: "m6 9 6 6 6-6" })
1738
+ }
1739
+ );
1740
+ }
1741
+
1742
+ // src/components/HoldShutter.tsx
1743
+ var import_react7 = require("react");
1744
+ var import_jsx_runtime13 = require("react/jsx-runtime");
1745
+ var HOLD_MS = 320;
1746
+ var LOCK_AFTER_MS = 1200;
1747
+ function HoldShutter({
1748
+ recording,
1749
+ elapsedSeconds,
1750
+ maxSeconds,
1751
+ disabled = false,
1752
+ onPressStart,
1753
+ onPhoto,
1754
+ onStartRecording,
1755
+ onStopRecording
1756
+ }) {
1757
+ const holdTimer = (0, import_react7.useRef)(null);
1758
+ const startedRecording = (0, import_react7.useRef)(false);
1759
+ const [pressed, setPressed] = (0, import_react7.useState)(false);
1760
+ const [locked, setLocked] = (0, import_react7.useState)(false);
1761
+ const clearHold = (0, import_react7.useCallback)(() => {
1762
+ if (holdTimer.current !== null) {
1763
+ clearTimeout(holdTimer.current);
1764
+ holdTimer.current = null;
1765
+ }
1766
+ }, []);
1767
+ (0, import_react7.useEffect)(() => {
1768
+ if (!recording) {
1769
+ setLocked(false);
1770
+ return;
1771
+ }
1772
+ if (elapsedSeconds * 1e3 >= LOCK_AFTER_MS) setLocked(true);
1773
+ }, [recording, elapsedSeconds]);
1774
+ (0, import_react7.useEffect)(() => clearHold, [clearHold]);
1775
+ const onPointerDown = (0, import_react7.useCallback)(
1776
+ (event) => {
1777
+ if (disabled) return;
1778
+ event.currentTarget.setPointerCapture?.(event.pointerId);
1779
+ setPressed(true);
1780
+ onPressStart?.();
1781
+ if (recording && locked) return;
1782
+ startedRecording.current = false;
1783
+ holdTimer.current = setTimeout(() => {
1784
+ holdTimer.current = null;
1785
+ startedRecording.current = true;
1786
+ onStartRecording();
1787
+ }, HOLD_MS);
1788
+ },
1789
+ [disabled, locked, onPressStart, onStartRecording, recording]
1790
+ );
1791
+ const endPress = (0, import_react7.useCallback)(() => {
1792
+ if (!pressed) return;
1793
+ setPressed(false);
1794
+ const wasHold = startedRecording.current;
1795
+ clearHold();
1796
+ startedRecording.current = false;
1797
+ if (recording && locked && !wasHold) {
1798
+ onStopRecording();
1799
+ return;
1800
+ }
1801
+ if (wasHold) {
1802
+ if (!locked) onStopRecording();
1803
+ return;
1804
+ }
1805
+ if (!recording) onPhoto();
1806
+ }, [clearHold, locked, onPhoto, onStopRecording, pressed, recording]);
1807
+ const progress = maxSeconds && maxSeconds > 0 ? Math.min(1, elapsedSeconds / maxSeconds) : 0;
1808
+ const R = 39;
1809
+ const C = 2 * Math.PI * R;
1810
+ return /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)("div", { className: "relative flex h-[86px] w-[86px] shrink-0 items-center justify-center", children: [
1811
+ recording && maxSeconds ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1812
+ "svg",
1813
+ {
1814
+ className: "pointer-events-none absolute inset-0 -rotate-90",
1815
+ viewBox: "0 0 86 86",
1816
+ "aria-hidden": "true",
1817
+ children: [
1818
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1819
+ "circle",
1820
+ {
1821
+ cx: "43",
1822
+ cy: "43",
1823
+ r: R,
1824
+ fill: "none",
1825
+ stroke: "rgba(255,255,255,0.25)",
1826
+ strokeWidth: "4"
1827
+ }
1828
+ ),
1829
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1830
+ "circle",
1831
+ {
1832
+ cx: "43",
1833
+ cy: "43",
1834
+ r: R,
1835
+ fill: "none",
1836
+ stroke: "#FF3B30",
1837
+ strokeWidth: "4",
1838
+ strokeLinecap: "round",
1839
+ strokeDasharray: C,
1840
+ strokeDashoffset: C * (1 - progress)
1841
+ }
1842
+ )
1843
+ ]
1844
+ }
1845
+ ) : null,
1846
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1847
+ "button",
1848
+ {
1849
+ type: "button",
1850
+ disabled,
1851
+ onPointerDown,
1852
+ onPointerUp: endPress,
1853
+ onPointerCancel: endPress,
1854
+ onLostPointerCapture: endPress,
1855
+ onContextMenu: (e) => e.preventDefault(),
1856
+ "aria-label": recording ? locked ? "Stop recording" : "Recording \u2014 release to stop" : "Tap for a photo, hold to record",
1857
+ className: cn(
1858
+ "group flex h-[74px] w-[74px] shrink-0 touch-none select-none items-center justify-center",
1859
+ "rounded-full border-[3.5px] border-white transition-opacity",
1860
+ disabled && "opacity-30"
1861
+ ),
1862
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1863
+ "span",
1864
+ {
1865
+ className: cn(
1866
+ "block transition-all duration-200 ease-out",
1867
+ recording ? "h-7 w-7 rounded-[6px] bg-[#FF3B30]" : cn(
1868
+ "h-[62px] w-[62px] rounded-full bg-white",
1869
+ pressed && "scale-90"
1870
+ )
1871
+ )
1872
+ }
1873
+ )
1874
+ }
1875
+ ),
1876
+ recording ? /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1877
+ "span",
1878
+ {
1879
+ className: cn(
1880
+ "pointer-events-none absolute -bottom-1 left-1/2 -translate-x-1/2 translate-y-full",
1881
+ "rounded-full bg-black/60 px-2 py-0.5 text-[11px] font-medium tabular-nums text-white"
1882
+ ),
1883
+ children: [
1884
+ formatElapsed2(elapsedSeconds),
1885
+ locked ? " \xB7 tap to stop" : ""
1886
+ ]
1887
+ }
1888
+ ) : null
1889
+ ] });
1890
+ }
1891
+ function formatElapsed2(totalSeconds) {
1892
+ const m = Math.floor(totalSeconds / 60);
1893
+ const s = String(Math.floor(totalSeconds % 60)).padStart(2, "0");
1894
+ return `${m}:${s}`;
1895
+ }
1896
+
1897
+ // src/components/CameraCaptureV3.tsx
1898
+ var import_jsx_runtime14 = require("react/jsx-runtime");
1899
+ var ASPECT_CYCLE2 = ["full", "4:3", "1:1", "16:9"];
1900
+ function CameraCaptureV3({
1901
+ engine,
1902
+ cloud,
1903
+ preview,
1904
+ media,
1905
+ onRecordIntent,
1906
+ maxRecordSeconds,
1907
+ onReviewOpenChange,
1908
+ onClose,
1909
+ blockedSheet,
1910
+ controlsHidden = false,
1911
+ shutterDisabled = false,
1912
+ slots = {}
1913
+ }) {
1914
+ const [viewerKey, setViewerKey] = (0, import_react8.useState)(null);
1915
+ const [editTarget, setEditTarget] = (0, import_react8.useState)(
1916
+ null
1917
+ );
1918
+ const [gridOn, setGridOn] = (0, import_react8.useState)(false);
1919
+ const [timerSetting, setTimerSetting] = (0, import_react8.useState)(0);
1920
+ const [aspect, setAspect] = (0, import_react8.useState)("full");
1921
+ const [countdown, setCountdown] = (0, import_react8.useState)(null);
1922
+ const [blockedDismissed, setBlockedDismissed] = (0, import_react8.useState)(false);
1923
+ const countdownRef = (0, import_react8.useRef)(null);
1924
+ const controls = useTrackControls(engine.stream);
1925
+ const reviewOpen = viewerKey !== null || editTarget !== null;
1926
+ (0, import_react8.useEffect)(() => {
1927
+ onReviewOpenChange?.(reviewOpen);
1928
+ }, [reviewOpen, onReviewOpenChange]);
1929
+ const clearCountdown = (0, import_react8.useCallback)(() => {
1930
+ if (countdownRef.current) clearInterval(countdownRef.current);
1931
+ countdownRef.current = null;
1932
+ setCountdown(null);
1933
+ }, []);
1934
+ (0, import_react8.useEffect)(() => clearCountdown, [clearCountdown]);
1935
+ (0, import_react8.useEffect)(() => {
1936
+ if (engine.recording) clearCountdown();
1937
+ }, [engine.recording, clearCountdown]);
1938
+ const onPhoto = (0, import_react8.useCallback)(() => {
1939
+ if (countdown !== null) {
1940
+ clearCountdown();
1941
+ return;
1942
+ }
1943
+ if (timerSetting === 0) {
1944
+ engine.onCapturePhoto({ aspect });
1945
+ return;
1946
+ }
1947
+ let remaining = timerSetting;
1948
+ setCountdown(remaining);
1949
+ countdownRef.current = setInterval(() => {
1950
+ remaining -= 1;
1951
+ if (remaining <= 0) {
1952
+ clearCountdown();
1953
+ engine.onCapturePhoto({ aspect });
1954
+ } else {
1955
+ setCountdown(remaining);
1956
+ }
1957
+ }, 1e3);
1958
+ }, [aspect, clearCountdown, countdown, engine, timerSetting]);
1959
+ const blocked = engine.blocked !== null;
1960
+ const coreRail = [
1961
+ ...engine.onFlipCamera ? [
1962
+ {
1963
+ id: "flip",
1964
+ label: "Switch camera",
1965
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.RefreshCw, { className: "h-[22px] w-[22px]" }),
1966
+ primary: true,
1967
+ onPress: engine.onFlipCamera
1968
+ }
1969
+ ] : [],
1970
+ ...controls.torchSupported ? [
1971
+ {
1972
+ id: "flash",
1973
+ label: "Flash",
1974
+ icon: controls.torchOn ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Zap, { className: "h-[22px] w-[22px]", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.ZapOff, { className: "h-[22px] w-[22px]" }),
1975
+ active: controls.torchOn,
1976
+ primary: true,
1977
+ onPress: controls.toggleTorch
1978
+ }
1979
+ ] : [],
1980
+ {
1981
+ id: "timer",
1982
+ label: "Timer",
1983
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Timer, { className: "h-[22px] w-[22px]" }),
1984
+ active: timerSetting !== 0,
1985
+ valueLabel: timerSetting === 0 ? void 0 : `${timerSetting}s`,
1986
+ onPress: () => setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0)
1987
+ },
1988
+ {
1989
+ id: "grid",
1990
+ label: "Grid",
1991
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Grid3x3, { className: "h-[22px] w-[22px]" }),
1992
+ active: gridOn,
1993
+ onPress: () => setGridOn((g) => !g)
1994
+ },
1995
+ {
1996
+ id: "aspect",
1997
+ label: "Aspect",
1998
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Proportions, { className: "h-[22px] w-[22px]" }),
1999
+ active: aspect !== "full",
2000
+ valueLabel: aspect === "full" ? void 0 : aspect,
2001
+ onPress: () => setAspect(
2002
+ (a) => ASPECT_CYCLE2[(ASPECT_CYCLE2.indexOf(a) + 1) % ASPECT_CYCLE2.length] ?? "full"
2003
+ )
2004
+ },
2005
+ ...controls.exposureSupported ? [
2006
+ {
2007
+ id: "exposure",
2008
+ label: "Exposure",
2009
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.SunMedium, { className: "h-[22px] w-[22px]" }),
2010
+ active: controls.exposure !== 0,
2011
+ valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
2012
+ onPress: () => controls.setExposure(controls.exposure === 0 ? 1 : 0)
2013
+ }
2014
+ ] : []
2015
+ ];
2016
+ const railActions = [...coreRail, ...slots.railActions ?? []];
2017
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
2018
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "absolute inset-0", children: preview }),
2019
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GridOverlay, { visible: gridOn && !blocked }),
2020
+ aspect !== "full" && !blocked && !engine.recording && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2021
+ "div",
2022
+ {
2023
+ "aria-hidden": true,
2024
+ className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
2025
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2026
+ "div",
2027
+ {
2028
+ className: "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]",
2029
+ style: {
2030
+ aspectRatio: aspect === "1:1" ? "1 / 1" : aspect === "4:3" ? "3 / 4" : "9 / 16",
2031
+ width: aspect === "16:9" ? "100%" : void 0,
2032
+ height: aspect === "16:9" ? void 0 : "70%",
2033
+ maxWidth: "100%",
2034
+ maxHeight: "100%"
2035
+ }
2036
+ }
2037
+ )
2038
+ }
2039
+ ),
2040
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CountdownOverlay, { seconds: countdown }),
2041
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2042
+ "div",
2043
+ {
2044
+ className: "pointer-events-none absolute inset-x-0 top-0 z-20 px-2",
2045
+ style: safeTop,
2046
+ children: [
2047
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex min-h-[44px] items-center gap-2 py-1.5", children: [
2048
+ onClose ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2049
+ "button",
2050
+ {
2051
+ type: "button",
2052
+ onClick: onClose,
2053
+ "aria-label": "Close camera",
2054
+ className: "pointer-events-auto flex h-10 w-10 shrink-0 touch-manipulation items-center justify-center rounded-full text-white drop-shadow-[0_1px_3px_rgba(0,0,0,0.6)] transition-colors hover:bg-white/10",
2055
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, { className: "h-6 w-6" })
2056
+ }
2057
+ ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" }),
2058
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-auto flex min-w-0 flex-1 justify-center", children: slots.topEntry }),
2059
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" })
2060
+ ] }),
2061
+ slots.topCenter ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-none pb-1 text-center", children: slots.topCenter }) : null
2062
+ ]
2063
+ }
2064
+ ),
2065
+ slots.statusChips ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2066
+ "div",
2067
+ {
2068
+ className: "pointer-events-none absolute inset-x-0 z-20 flex flex-col items-center gap-1.5 px-3",
2069
+ style: {
2070
+ top: `calc(env(safe-area-inset-top, 0px) + ${controlsHidden ? 12 : 64}px)`
2071
+ },
2072
+ children: slots.statusChips
2073
+ }
2074
+ ) : null,
2075
+ !controlsHidden && !blocked && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2076
+ "div",
2077
+ {
2078
+ className: "pointer-events-none absolute right-1 z-20 flex justify-end",
2079
+ style: { top: `calc(env(safe-area-inset-top, 0px) + 64px)` },
2080
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CaptureRail, { actions: railActions })
2081
+ }
2082
+ ),
2083
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-x-0 bottom-0 z-20", style: safeBottom, children: [
2084
+ media && media.items.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "px-2 pb-1.5", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2085
+ CaptureFilmstrip,
2086
+ {
2087
+ items: media.items,
2088
+ onOpen: (item) => setViewerKey(item.key)
2089
+ }
2090
+ ) }) : null,
2091
+ slots.aboveShutter ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "px-2 pb-1.5", children: slots.aboveShutter }) : null,
2092
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex items-center justify-between px-3 pb-2", children: [
2093
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex w-[86px] justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2094
+ "button",
2095
+ {
2096
+ type: "button",
2097
+ onClick: cloud.onOpenLibrary,
2098
+ "aria-label": "Your media \u2014 library and upload",
2099
+ className: "relative h-12 w-12 touch-manipulation overflow-hidden rounded-xl bg-black/40 ring-1 ring-white/30 backdrop-blur-md transition-transform active:scale-95",
2100
+ children: cloud.recentsThumb ?? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "flex h-full w-full items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Images, { className: "h-5 w-5 text-white/80" }) })
2101
+ }
2102
+ ) }),
2103
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2104
+ HoldShutter,
2105
+ {
2106
+ recording: engine.recording,
2107
+ elapsedSeconds: engine.recordElapsedSeconds,
2108
+ ...maxRecordSeconds !== void 0 ? { maxSeconds: maxRecordSeconds } : {},
2109
+ disabled: shutterDisabled || blocked,
2110
+ ...onRecordIntent ? { onPressStart: onRecordIntent } : {},
2111
+ onPhoto,
2112
+ onStartRecording: engine.onStartRecording,
2113
+ onStopRecording: engine.onStopRecording
2114
+ }
2115
+ ),
2116
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex w-[86px] justify-end", children: slots.shutterTrailing })
2117
+ ] }),
2118
+ !engine.recording && !blocked ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("p", { className: "pb-1.5 text-center text-[11px] text-white/60 drop-shadow-[0_1px_2px_rgba(0,0,0,0.8)]", children: "Tap for a photo \xB7 hold to record" }) : null
2119
+ ] }),
2120
+ blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2121
+ CaptureSheet,
2122
+ {
2123
+ open: true,
2124
+ onClose: () => setBlockedDismissed(true),
2125
+ body: blockedSheet.body,
2126
+ title: "Camera unavailable",
2127
+ actions: blockedSheet.actions
2128
+ }
2129
+ ),
2130
+ media && viewerKey !== null && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2131
+ MediaViewer,
2132
+ {
2133
+ items: media.items,
2134
+ initialKey: viewerKey,
2135
+ keysDisabled: editTarget !== null,
2136
+ onClose: () => setViewerKey(null),
2137
+ onDelete: media.onDelete ? (item) => {
2138
+ media.onDelete?.(item.key);
2139
+ invalidateMedia(item.key);
2140
+ } : void 0,
2141
+ onEdit: media.onReplacePhoto ? (item) => {
2142
+ const url = getMediaUrl(item);
2143
+ if (url) setEditTarget({ key: item.key, url });
2144
+ } : void 0
2145
+ }
2146
+ ),
2147
+ media?.onReplacePhoto && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2148
+ ImageEditSheet,
2149
+ {
2150
+ open: editTarget !== null,
2151
+ src: editTarget?.url ?? null,
2152
+ onClose: () => setEditTarget(null),
2153
+ onSave: (blob) => {
2154
+ const target = editTarget;
2155
+ setEditTarget(null);
2156
+ setViewerKey(null);
2157
+ if (target) {
2158
+ media.onReplacePhoto?.(target.key, blob);
2159
+ invalidateMedia(target.key);
2160
+ }
2161
+ }
2162
+ }
2163
+ ),
2164
+ slots.overlays
2165
+ ] });
2166
+ }
2167
+
2168
+ // src/components/CaptureExpandingField.tsx
2169
+ var import_react9 = require("react");
2170
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2171
+ function CaptureExpandingField({
2172
+ label,
2173
+ placeholder,
2174
+ icon,
2175
+ onCommit,
2176
+ defaultExpanded = false,
2177
+ disabled = false,
2178
+ currentValue = null,
2179
+ className
2180
+ }) {
2181
+ const [expanded, setExpanded] = (0, import_react9.useState)(defaultExpanded);
2182
+ const [draft, setDraft] = (0, import_react9.useState)("");
2183
+ const inputRef = (0, import_react9.useRef)(null);
2184
+ const draftRef = (0, import_react9.useRef)(draft);
2185
+ draftRef.current = draft;
2186
+ const onCommitRef = (0, import_react9.useRef)(onCommit);
2187
+ onCommitRef.current = onCommit;
2188
+ (0, import_react9.useEffect)(() => {
2189
+ return () => {
2190
+ const trimmed = draftRef.current.trim();
2191
+ if (trimmed) onCommitRef.current(trimmed);
2192
+ };
2193
+ }, []);
2194
+ const commit = (0, import_react9.useCallback)(() => {
2195
+ const trimmed = draftRef.current.trim();
2196
+ if (trimmed) onCommitRef.current(trimmed);
2197
+ setDraft("");
2198
+ }, []);
2199
+ const open = (0, import_react9.useCallback)(() => {
2200
+ setExpanded(true);
2201
+ requestAnimationFrame(() => inputRef.current?.focus());
2202
+ }, []);
2203
+ const close = (0, import_react9.useCallback)(() => {
2204
+ commit();
2205
+ setExpanded(false);
2206
+ }, [commit]);
2207
+ if (!expanded) {
2208
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2209
+ "button",
2210
+ {
2211
+ type: "button",
2212
+ onClick: open,
2213
+ disabled,
2214
+ "aria-label": label,
2215
+ "aria-expanded": false,
2216
+ className: cn(
2217
+ "flex h-9 max-w-[62vw] items-center gap-1.5 rounded-full px-3.5",
2218
+ "bg-black/45 text-[13px] font-semibold text-white backdrop-blur-md",
2219
+ "transition-colors hover:bg-black/60 active:bg-black/70",
2220
+ disabled && "opacity-40",
2221
+ className
2222
+ ),
2223
+ children: [
2224
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "shrink-0", children: icon }) : null,
2225
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "truncate", children: currentValue?.trim() || label })
2226
+ ]
2227
+ }
2228
+ );
2229
+ }
2230
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2231
+ "div",
2232
+ {
2233
+ className: cn(
2234
+ "flex h-9 w-[min(78vw,320px)] items-center gap-1.5 rounded-full px-3",
2235
+ "bg-black/60 backdrop-blur-md",
2236
+ className
2237
+ ),
2238
+ children: [
2239
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "shrink-0 text-white/70", children: icon }) : null,
2240
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2241
+ "input",
2242
+ {
2243
+ ref: inputRef,
2244
+ value: draft,
2245
+ onChange: (e) => setDraft(e.target.value),
2246
+ onBlur: close,
2247
+ onKeyDown: (e) => {
2248
+ if (e.key === "Enter") {
2249
+ commit();
2250
+ e.target.blur();
2251
+ }
2252
+ if (e.key === "Escape") {
2253
+ setDraft("");
2254
+ setExpanded(false);
2255
+ }
2256
+ },
2257
+ placeholder: placeholder ?? label,
2258
+ "aria-label": label,
2259
+ enterKeyHint: "done",
2260
+ autoCapitalize: "characters",
2261
+ autoCorrect: "off",
2262
+ spellCheck: false,
2263
+ className: cn(
2264
+ "min-w-0 flex-1 bg-transparent text-base text-white",
2265
+ "placeholder:text-white/40 focus:outline-none"
2266
+ )
2267
+ }
2268
+ ),
2269
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2270
+ "button",
2271
+ {
2272
+ type: "button",
2273
+ onMouseDown: (e) => e.preventDefault(),
2274
+ onClick: () => {
2275
+ setDraft("");
2276
+ setExpanded(false);
2277
+ },
2278
+ "aria-label": `Close ${label}`,
2279
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-white/70 hover:bg-white/10 hover:text-white",
2280
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2281
+ "svg",
2282
+ {
2283
+ viewBox: "0 0 24 24",
2284
+ fill: "none",
2285
+ stroke: "currentColor",
2286
+ strokeWidth: "2.5",
2287
+ strokeLinecap: "round",
2288
+ className: "h-3.5 w-3.5",
2289
+ "aria-hidden": "true",
2290
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: "M18 6 6 18M6 6l12 12" })
2291
+ }
2292
+ )
2293
+ }
2294
+ )
2295
+ ]
2296
+ }
2297
+ );
2298
+ }
2299
+
2300
+ // src/components/CameraFeed.tsx
2301
+ var import_react10 = require("react");
2302
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1649
2303
  function CameraFeed({
1650
2304
  engine,
1651
2305
  mirror = false
1652
2306
  }) {
1653
2307
  const { stream, videoRef } = engine;
1654
- (0, import_react6.useEffect)(() => {
2308
+ (0, import_react10.useEffect)(() => {
1655
2309
  const video = videoRef.current;
1656
2310
  if (!video) return;
1657
2311
  if (video.srcObject !== stream) video.srcObject = stream;
@@ -1659,7 +2313,7 @@ function CameraFeed({
1659
2313
  video.srcObject = null;
1660
2314
  };
1661
2315
  }, [stream, videoRef]);
1662
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2316
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1663
2317
  "video",
1664
2318
  {
1665
2319
  ref: videoRef,
@@ -1677,7 +2331,7 @@ function CameraFeed({
1677
2331
  }
1678
2332
 
1679
2333
  // src/engine/useDefaultEngine.ts
1680
- var import_react7 = require("react");
2334
+ var import_react11 = require("react");
1681
2335
  var RECORDING_MIME_LADDER = [
1682
2336
  'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
1683
2337
  "video/mp4",
@@ -1707,18 +2361,18 @@ function useDefaultCaptureEngine(options) {
1707
2361
  facingMode: initialFacing = "environment",
1708
2362
  photoQuality = 0.92
1709
2363
  } = options;
1710
- const [stream, setStream] = (0, import_react7.useState)(null);
1711
- const [blocked, setBlocked] = (0, import_react7.useState)(null);
1712
- const [facing, setFacing] = (0, import_react7.useState)(initialFacing);
1713
- const [multipleCameras, setMultipleCameras] = (0, import_react7.useState)(false);
1714
- const [recording, setRecording] = (0, import_react7.useState)(false);
1715
- const [recordElapsedSeconds, setRecordElapsedSeconds] = (0, import_react7.useState)(0);
1716
- const videoRef = (0, import_react7.useRef)(null);
1717
- const streamRef = (0, import_react7.useRef)(null);
1718
- const recorderRef = (0, import_react7.useRef)(null);
1719
- const callbacksRef = (0, import_react7.useRef)({ onPhoto, onVideo, onFiles });
2364
+ const [stream, setStream] = (0, import_react11.useState)(null);
2365
+ const [blocked, setBlocked] = (0, import_react11.useState)(null);
2366
+ const [facing, setFacing] = (0, import_react11.useState)(initialFacing);
2367
+ const [multipleCameras, setMultipleCameras] = (0, import_react11.useState)(false);
2368
+ const [recording, setRecording] = (0, import_react11.useState)(false);
2369
+ const [recordElapsedSeconds, setRecordElapsedSeconds] = (0, import_react11.useState)(0);
2370
+ const videoRef = (0, import_react11.useRef)(null);
2371
+ const streamRef = (0, import_react11.useRef)(null);
2372
+ const recorderRef = (0, import_react11.useRef)(null);
2373
+ const callbacksRef = (0, import_react11.useRef)({ onPhoto, onVideo, onFiles });
1720
2374
  callbacksRef.current = { onPhoto, onVideo, onFiles };
1721
- (0, import_react7.useEffect)(() => {
2375
+ (0, import_react11.useEffect)(() => {
1722
2376
  let cancelled = false;
1723
2377
  let acquired = null;
1724
2378
  if (typeof navigator === "undefined" || !navigator.mediaDevices) {
@@ -1761,7 +2415,7 @@ function useDefaultCaptureEngine(options) {
1761
2415
  setStream(null);
1762
2416
  };
1763
2417
  }, [facing]);
1764
- (0, import_react7.useEffect)(() => {
2418
+ (0, import_react11.useEffect)(() => {
1765
2419
  return () => {
1766
2420
  const r = recorderRef.current;
1767
2421
  if (r) {
@@ -1771,7 +2425,7 @@ function useDefaultCaptureEngine(options) {
1771
2425
  }
1772
2426
  };
1773
2427
  }, []);
1774
- const onCapturePhoto = (0, import_react7.useCallback)(
2428
+ const onCapturePhoto = (0, import_react11.useCallback)(
1775
2429
  (opts) => {
1776
2430
  const video = videoRef.current;
1777
2431
  if (!video || video.videoWidth === 0) return;
@@ -1811,7 +2465,7 @@ function useDefaultCaptureEngine(options) {
1811
2465
  },
1812
2466
  [photoQuality]
1813
2467
  );
1814
- const onStartRecording = (0, import_react7.useCallback)(() => {
2468
+ const onStartRecording = (0, import_react11.useCallback)(() => {
1815
2469
  const base = streamRef.current;
1816
2470
  if (!base || recorderRef.current) return;
1817
2471
  void (async () => {
@@ -1878,11 +2532,11 @@ function useDefaultCaptureEngine(options) {
1878
2532
  setRecording(true);
1879
2533
  })();
1880
2534
  }, [withAudio]);
1881
- const onStopRecording = (0, import_react7.useCallback)(() => {
2535
+ const onStopRecording = (0, import_react11.useCallback)(() => {
1882
2536
  const r = recorderRef.current;
1883
2537
  if (r && r.recorder.state !== "inactive") r.recorder.stop();
1884
2538
  }, []);
1885
- const onUpload = (0, import_react7.useCallback)(() => {
2539
+ const onUpload = (0, import_react11.useCallback)(() => {
1886
2540
  const input = document.createElement("input");
1887
2541
  input.type = "file";
1888
2542
  input.accept = "image/*,video/*";
@@ -1893,10 +2547,10 @@ function useDefaultCaptureEngine(options) {
1893
2547
  };
1894
2548
  input.click();
1895
2549
  }, []);
1896
- const onFlipCamera = (0, import_react7.useCallback)(() => {
2550
+ const onFlipCamera = (0, import_react11.useCallback)(() => {
1897
2551
  setFacing((f) => f === "environment" ? "user" : "environment");
1898
2552
  }, []);
1899
- return (0, import_react7.useMemo)(
2553
+ return (0, import_react11.useMemo)(
1900
2554
  () => ({
1901
2555
  stream,
1902
2556
  videoRef,