@ai-matrx/capture 0.2.1 → 0.3.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/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,670 @@ 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 = [
2017
+ ...coreRail.map((a) => blocked ? { ...a, disabled: true } : a),
2018
+ ...slots.railActions ?? []
2019
+ ];
2020
+ return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
2021
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "absolute inset-0", children: preview }),
2022
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GridOverlay, { visible: gridOn && !blocked }),
2023
+ aspect !== "full" && !blocked && !engine.recording && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2024
+ "div",
2025
+ {
2026
+ "aria-hidden": true,
2027
+ className: "pointer-events-none absolute inset-0 z-10 flex items-center justify-center",
2028
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2029
+ "div",
2030
+ {
2031
+ className: "shadow-[0_0_0_9999px_rgba(0,0,0,0.45)]",
2032
+ style: {
2033
+ aspectRatio: aspect === "1:1" ? "1 / 1" : aspect === "4:3" ? "3 / 4" : "9 / 16",
2034
+ width: aspect === "16:9" ? "100%" : void 0,
2035
+ height: aspect === "16:9" ? void 0 : "70%",
2036
+ maxWidth: "100%",
2037
+ maxHeight: "100%"
2038
+ }
2039
+ }
2040
+ )
2041
+ }
2042
+ ),
2043
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CountdownOverlay, { seconds: countdown }),
2044
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)(
2045
+ "div",
2046
+ {
2047
+ className: "pointer-events-none absolute inset-x-0 top-0 z-20 px-2",
2048
+ style: safeTop,
2049
+ children: [
2050
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex min-h-[44px] items-center gap-2 py-1.5", children: [
2051
+ onClose ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2052
+ "button",
2053
+ {
2054
+ type: "button",
2055
+ onClick: onClose,
2056
+ "aria-label": "Close camera",
2057
+ 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",
2058
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.X, { className: "h-6 w-6" })
2059
+ }
2060
+ ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" }),
2061
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-auto flex min-w-0 flex-1 justify-center", children: slots.topEntry }),
2062
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" })
2063
+ ] }),
2064
+ slots.topCenter ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-none pb-1 text-center", children: slots.topCenter }) : null
2065
+ ]
2066
+ }
2067
+ ),
2068
+ slots.statusChips ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2069
+ "div",
2070
+ {
2071
+ className: "pointer-events-none absolute inset-x-0 z-20 flex flex-col items-center gap-1.5 px-3",
2072
+ style: {
2073
+ // Clears the top row AND the topCenter line beneath it — chips
2074
+ // rendered at the label's own height read as a collision.
2075
+ top: `calc(env(safe-area-inset-top, 0px) + ${controlsHidden ? 12 : slots.topCenter ? 92 : 64}px)`
2076
+ },
2077
+ children: slots.statusChips
2078
+ }
2079
+ ) : null,
2080
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2081
+ "div",
2082
+ {
2083
+ className: "pointer-events-none absolute right-1 z-20 flex justify-end",
2084
+ style: { top: `calc(env(safe-area-inset-top, 0px) + 64px)` },
2085
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(CaptureRail, { actions: railActions })
2086
+ }
2087
+ ),
2088
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-x-0 bottom-0 z-20", style: safeBottom, children: [
2089
+ 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)(
2090
+ CaptureFilmstrip,
2091
+ {
2092
+ items: media.items,
2093
+ onOpen: (item) => setViewerKey(item.key)
2094
+ }
2095
+ ) }) : null,
2096
+ slots.aboveShutter ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "px-2 pb-1.5", children: slots.aboveShutter }) : null,
2097
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "flex items-center justify-between px-3 pb-2", children: [
2098
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex w-[86px] justify-start", children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2099
+ "button",
2100
+ {
2101
+ type: "button",
2102
+ onClick: cloud.onOpenLibrary,
2103
+ "aria-label": "Your media \u2014 library and upload",
2104
+ 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",
2105
+ 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" }) })
2106
+ }
2107
+ ) }),
2108
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2109
+ HoldShutter,
2110
+ {
2111
+ recording: engine.recording,
2112
+ elapsedSeconds: engine.recordElapsedSeconds,
2113
+ ...maxRecordSeconds !== void 0 ? { maxSeconds: maxRecordSeconds } : {},
2114
+ disabled: shutterDisabled || blocked,
2115
+ ...onRecordIntent ? { onPressStart: onRecordIntent } : {},
2116
+ onPhoto,
2117
+ onStartRecording: engine.onStartRecording,
2118
+ onStopRecording: engine.onStopRecording
2119
+ }
2120
+ ),
2121
+ /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "flex w-[86px] justify-end", children: slots.shutterTrailing })
2122
+ ] }),
2123
+ !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
2124
+ ] }),
2125
+ blocked && blockedSheet && !blockedDismissed && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2126
+ CaptureSheet,
2127
+ {
2128
+ open: true,
2129
+ onClose: () => setBlockedDismissed(true),
2130
+ body: blockedSheet.body,
2131
+ title: "Camera unavailable",
2132
+ actions: blockedSheet.actions
2133
+ }
2134
+ ),
2135
+ media && viewerKey !== null && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2136
+ MediaViewer,
2137
+ {
2138
+ items: media.items,
2139
+ initialKey: viewerKey,
2140
+ keysDisabled: editTarget !== null,
2141
+ onClose: () => setViewerKey(null),
2142
+ onDelete: media.onDelete ? (item) => {
2143
+ media.onDelete?.(item.key);
2144
+ invalidateMedia(item.key);
2145
+ } : void 0,
2146
+ onEdit: media.onReplacePhoto ? (item) => {
2147
+ const url = getMediaUrl(item);
2148
+ if (url) setEditTarget({ key: item.key, url });
2149
+ } : void 0
2150
+ }
2151
+ ),
2152
+ media?.onReplacePhoto && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2153
+ ImageEditSheet,
2154
+ {
2155
+ open: editTarget !== null,
2156
+ src: editTarget?.url ?? null,
2157
+ onClose: () => setEditTarget(null),
2158
+ onSave: (blob) => {
2159
+ const target = editTarget;
2160
+ setEditTarget(null);
2161
+ setViewerKey(null);
2162
+ if (target) {
2163
+ media.onReplacePhoto?.(target.key, blob);
2164
+ invalidateMedia(target.key);
2165
+ }
2166
+ }
2167
+ }
2168
+ ),
2169
+ slots.overlays
2170
+ ] });
2171
+ }
2172
+
2173
+ // src/components/CaptureExpandingField.tsx
2174
+ var import_react9 = require("react");
2175
+ var import_jsx_runtime15 = require("react/jsx-runtime");
2176
+ function CaptureExpandingField({
2177
+ label,
2178
+ placeholder,
2179
+ icon,
2180
+ onCommit,
2181
+ defaultExpanded = false,
2182
+ disabled = false,
2183
+ currentValue = null,
2184
+ className
2185
+ }) {
2186
+ const [expanded, setExpanded] = (0, import_react9.useState)(defaultExpanded);
2187
+ const [draft, setDraft] = (0, import_react9.useState)("");
2188
+ const inputRef = (0, import_react9.useRef)(null);
2189
+ const draftRef = (0, import_react9.useRef)(draft);
2190
+ draftRef.current = draft;
2191
+ const onCommitRef = (0, import_react9.useRef)(onCommit);
2192
+ onCommitRef.current = onCommit;
2193
+ (0, import_react9.useEffect)(() => {
2194
+ return () => {
2195
+ const trimmed = draftRef.current.trim();
2196
+ if (trimmed) onCommitRef.current(trimmed);
2197
+ };
2198
+ }, []);
2199
+ const commit = (0, import_react9.useCallback)(() => {
2200
+ const trimmed = draftRef.current.trim();
2201
+ if (trimmed) onCommitRef.current(trimmed);
2202
+ setDraft("");
2203
+ }, []);
2204
+ const open = (0, import_react9.useCallback)(() => {
2205
+ setExpanded(true);
2206
+ requestAnimationFrame(() => inputRef.current?.focus());
2207
+ }, []);
2208
+ const close = (0, import_react9.useCallback)(() => {
2209
+ commit();
2210
+ setExpanded(false);
2211
+ }, [commit]);
2212
+ if (!expanded) {
2213
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2214
+ "button",
2215
+ {
2216
+ type: "button",
2217
+ onClick: open,
2218
+ disabled,
2219
+ "aria-label": label,
2220
+ "aria-expanded": false,
2221
+ className: cn(
2222
+ "flex h-9 max-w-[62vw] items-center gap-1.5 rounded-full px-3.5",
2223
+ "bg-black/45 text-[13px] font-semibold text-white backdrop-blur-md",
2224
+ "transition-colors hover:bg-black/60 active:bg-black/70",
2225
+ disabled && "opacity-40",
2226
+ className
2227
+ ),
2228
+ children: [
2229
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "shrink-0", children: icon }) : null,
2230
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "truncate", children: currentValue?.trim() || label })
2231
+ ]
2232
+ }
2233
+ );
2234
+ }
2235
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
2236
+ "div",
2237
+ {
2238
+ className: cn(
2239
+ "flex h-9 w-[min(78vw,320px)] items-center gap-1.5 rounded-full px-3",
2240
+ "bg-black/60 backdrop-blur-md",
2241
+ className
2242
+ ),
2243
+ children: [
2244
+ icon ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "shrink-0 text-white/70", children: icon }) : null,
2245
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2246
+ "input",
2247
+ {
2248
+ ref: inputRef,
2249
+ value: draft,
2250
+ onChange: (e) => setDraft(e.target.value),
2251
+ onBlur: close,
2252
+ onKeyDown: (e) => {
2253
+ if (e.key === "Enter") {
2254
+ commit();
2255
+ e.target.blur();
2256
+ }
2257
+ if (e.key === "Escape") {
2258
+ setDraft("");
2259
+ setExpanded(false);
2260
+ }
2261
+ },
2262
+ placeholder: placeholder ?? label,
2263
+ "aria-label": label,
2264
+ enterKeyHint: "done",
2265
+ autoCapitalize: "characters",
2266
+ autoCorrect: "off",
2267
+ spellCheck: false,
2268
+ className: cn(
2269
+ "min-w-0 flex-1 bg-transparent text-base text-white",
2270
+ "placeholder:text-white/40 focus:outline-none"
2271
+ )
2272
+ }
2273
+ ),
2274
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2275
+ "button",
2276
+ {
2277
+ type: "button",
2278
+ onMouseDown: (e) => e.preventDefault(),
2279
+ onClick: () => {
2280
+ setDraft("");
2281
+ setExpanded(false);
2282
+ },
2283
+ "aria-label": `Close ${label}`,
2284
+ className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full text-white/70 hover:bg-white/10 hover:text-white",
2285
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2286
+ "svg",
2287
+ {
2288
+ viewBox: "0 0 24 24",
2289
+ fill: "none",
2290
+ stroke: "currentColor",
2291
+ strokeWidth: "2.5",
2292
+ strokeLinecap: "round",
2293
+ className: "h-3.5 w-3.5",
2294
+ "aria-hidden": "true",
2295
+ children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("path", { d: "M18 6 6 18M6 6l12 12" })
2296
+ }
2297
+ )
2298
+ }
2299
+ )
2300
+ ]
2301
+ }
2302
+ );
2303
+ }
2304
+
2305
+ // src/components/CameraFeed.tsx
2306
+ var import_react10 = require("react");
2307
+ var import_jsx_runtime16 = require("react/jsx-runtime");
1649
2308
  function CameraFeed({
1650
2309
  engine,
1651
2310
  mirror = false
1652
2311
  }) {
1653
2312
  const { stream, videoRef } = engine;
1654
- (0, import_react6.useEffect)(() => {
2313
+ (0, import_react10.useEffect)(() => {
1655
2314
  const video = videoRef.current;
1656
2315
  if (!video) return;
1657
2316
  if (video.srcObject !== stream) video.srcObject = stream;
@@ -1659,7 +2318,7 @@ function CameraFeed({
1659
2318
  video.srcObject = null;
1660
2319
  };
1661
2320
  }, [stream, videoRef]);
1662
- return /* @__PURE__ */ (0, import_jsx_runtime12.jsx)(
2321
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
1663
2322
  "video",
1664
2323
  {
1665
2324
  ref: videoRef,
@@ -1677,7 +2336,7 @@ function CameraFeed({
1677
2336
  }
1678
2337
 
1679
2338
  // src/engine/useDefaultEngine.ts
1680
- var import_react7 = require("react");
2339
+ var import_react11 = require("react");
1681
2340
  var RECORDING_MIME_LADDER = [
1682
2341
  'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
1683
2342
  "video/mp4",
@@ -1707,18 +2366,18 @@ function useDefaultCaptureEngine(options) {
1707
2366
  facingMode: initialFacing = "environment",
1708
2367
  photoQuality = 0.92
1709
2368
  } = 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 });
2369
+ const [stream, setStream] = (0, import_react11.useState)(null);
2370
+ const [blocked, setBlocked] = (0, import_react11.useState)(null);
2371
+ const [facing, setFacing] = (0, import_react11.useState)(initialFacing);
2372
+ const [multipleCameras, setMultipleCameras] = (0, import_react11.useState)(false);
2373
+ const [recording, setRecording] = (0, import_react11.useState)(false);
2374
+ const [recordElapsedSeconds, setRecordElapsedSeconds] = (0, import_react11.useState)(0);
2375
+ const videoRef = (0, import_react11.useRef)(null);
2376
+ const streamRef = (0, import_react11.useRef)(null);
2377
+ const recorderRef = (0, import_react11.useRef)(null);
2378
+ const callbacksRef = (0, import_react11.useRef)({ onPhoto, onVideo, onFiles });
1720
2379
  callbacksRef.current = { onPhoto, onVideo, onFiles };
1721
- (0, import_react7.useEffect)(() => {
2380
+ (0, import_react11.useEffect)(() => {
1722
2381
  let cancelled = false;
1723
2382
  let acquired = null;
1724
2383
  if (typeof navigator === "undefined" || !navigator.mediaDevices) {
@@ -1761,7 +2420,7 @@ function useDefaultCaptureEngine(options) {
1761
2420
  setStream(null);
1762
2421
  };
1763
2422
  }, [facing]);
1764
- (0, import_react7.useEffect)(() => {
2423
+ (0, import_react11.useEffect)(() => {
1765
2424
  return () => {
1766
2425
  const r = recorderRef.current;
1767
2426
  if (r) {
@@ -1771,7 +2430,7 @@ function useDefaultCaptureEngine(options) {
1771
2430
  }
1772
2431
  };
1773
2432
  }, []);
1774
- const onCapturePhoto = (0, import_react7.useCallback)(
2433
+ const onCapturePhoto = (0, import_react11.useCallback)(
1775
2434
  (opts) => {
1776
2435
  const video = videoRef.current;
1777
2436
  if (!video || video.videoWidth === 0) return;
@@ -1811,7 +2470,7 @@ function useDefaultCaptureEngine(options) {
1811
2470
  },
1812
2471
  [photoQuality]
1813
2472
  );
1814
- const onStartRecording = (0, import_react7.useCallback)(() => {
2473
+ const onStartRecording = (0, import_react11.useCallback)(() => {
1815
2474
  const base = streamRef.current;
1816
2475
  if (!base || recorderRef.current) return;
1817
2476
  void (async () => {
@@ -1878,11 +2537,11 @@ function useDefaultCaptureEngine(options) {
1878
2537
  setRecording(true);
1879
2538
  })();
1880
2539
  }, [withAudio]);
1881
- const onStopRecording = (0, import_react7.useCallback)(() => {
2540
+ const onStopRecording = (0, import_react11.useCallback)(() => {
1882
2541
  const r = recorderRef.current;
1883
2542
  if (r && r.recorder.state !== "inactive") r.recorder.stop();
1884
2543
  }, []);
1885
- const onUpload = (0, import_react7.useCallback)(() => {
2544
+ const onUpload = (0, import_react11.useCallback)(() => {
1886
2545
  const input = document.createElement("input");
1887
2546
  input.type = "file";
1888
2547
  input.accept = "image/*,video/*";
@@ -1893,10 +2552,10 @@ function useDefaultCaptureEngine(options) {
1893
2552
  };
1894
2553
  input.click();
1895
2554
  }, []);
1896
- const onFlipCamera = (0, import_react7.useCallback)(() => {
2555
+ const onFlipCamera = (0, import_react11.useCallback)(() => {
1897
2556
  setFacing((f) => f === "environment" ? "user" : "environment");
1898
2557
  }, []);
1899
- return (0, import_react7.useMemo)(
2558
+ return (0, import_react11.useMemo)(
1900
2559
  () => ({
1901
2560
  stream,
1902
2561
  videoRef,