@ai-matrx/capture 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -62,3 +62,12 @@ The package-completeness release: the whole capture loop now lives in the box (A
62
62
  - Law 4 now scans EVERY `CameraCapture*.tsx` for the required cloud port, by filename pattern — a third chrome inherits THE CLOUD LAW by existing, not by someone remembering.
63
63
 
64
64
  Deliberately not copied from the app this borrows its shape from: fixed duration choices (they belong to that product's format, not to a camera) and the oversized promo pill for a new feature (chrome is not a billboard).
65
+
66
+ ## 0.3.1 — 2026-08-30
67
+
68
+ - v3: the rail renders while the camera is blocked (host actions keep working through the upload lane; core camera actions disable instead of vanishing), and status chips clear the `topCenter` line instead of colliding with it.
69
+
70
+ ## 0.4.0 — 2026-08-30
71
+
72
+ - **Viewer: videos swipe exactly like photos.** The video slide is now a package-owned control-less player — the STAGE owns every gesture (native `controls` made the whole slide a pointer sink), a tap toggles playback, a centre glyph shows the paused state, and a thin bottom bar shows progress. Audio keeps native controls.
73
+ - **HoldShutter: lock is a GESTURE, never a timer.** Recording follows the finger — release stops, full stop. While holding, slide UP ~64px to the lock target that appears above the button; locked, release keeps recording and the next tap stops. The 1.2s auto-latch is gone (it silently converted "I let go" into "still recording").
package/dist/react.cjs CHANGED
@@ -654,6 +654,7 @@ function MediaViewer({
654
654
  const clamped = Math.min(index, count - 1);
655
655
  const current = count > 0 ? items[clamped] : void 0;
656
656
  const [confirmingDelete, setConfirmingDelete] = (0, import_react3.useState)(false);
657
+ const activeVideoRef = (0, import_react3.useRef)(null);
657
658
  const dragRef = (0, import_react3.useRef)(null);
658
659
  const [drag, setDrag] = (0, import_react3.useState)(null);
659
660
  const [settling, setSettling] = (0, import_react3.useState)(false);
@@ -694,7 +695,7 @@ function MediaViewer({
694
695
  return () => window.removeEventListener("keydown", onKey);
695
696
  }, [go, onClose, keysDisabled]);
696
697
  const onPointerDown = (0, import_react3.useCallback)((e) => {
697
- if (e.target.closest("video, audio")) return;
698
+ if (e.target.closest("audio")) return;
698
699
  dragRef.current = {
699
700
  x0: e.clientX,
700
701
  y0: e.clientY,
@@ -741,6 +742,14 @@ function MediaViewer({
741
742
  return;
742
743
  }
743
744
  }
745
+ if (d.axis === null) {
746
+ const video = activeVideoRef.current;
747
+ if (video) {
748
+ if (video.paused) void video.play().catch(() => {
749
+ });
750
+ else video.pause();
751
+ }
752
+ }
744
753
  setDrag(null);
745
754
  setSettling(true);
746
755
  },
@@ -817,7 +826,15 @@ function MediaViewer({
817
826
  onPointerMove,
818
827
  onPointerUp: endDrag,
819
828
  onPointerCancel: endDrag,
820
- children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(ViewerSlide, { item: current, active: true }, current.key)
829
+ children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
830
+ ViewerSlide,
831
+ {
832
+ item: current,
833
+ active: true,
834
+ videoRef: activeVideoRef
835
+ },
836
+ current.key
837
+ )
821
838
  }
822
839
  ),
823
840
  /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
@@ -910,7 +927,8 @@ function MediaViewer({
910
927
  }
911
928
  function ViewerSlide({
912
929
  item,
913
- active
930
+ active,
931
+ videoRef
914
932
  }) {
915
933
  const url = useMediaUrl(item);
916
934
  if (!url) {
@@ -939,20 +957,7 @@ function ViewerSlide({
939
957
  ] });
940
958
  }
941
959
  if (item.kind === "video") {
942
- return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
943
- "video",
944
- {
945
- src: url,
946
- controls: active,
947
- playsInline: true,
948
- preload: active ? "auto" : "metadata",
949
- muted: !active,
950
- className: cn(
951
- "absolute inset-0 h-full w-full object-contain",
952
- active ? "pointer-events-auto" : "pointer-events-none"
953
- )
954
- }
955
- );
960
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(VideoSlide, { url, active, videoRef });
956
961
  }
957
962
  return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
958
963
  "img",
@@ -965,6 +970,51 @@ function ViewerSlide({
965
970
  }
966
971
  );
967
972
  }
973
+ function VideoSlide({
974
+ url,
975
+ active,
976
+ videoRef
977
+ }) {
978
+ const [paused, setPaused] = (0, import_react3.useState)(true);
979
+ const [progress, setProgress] = (0, import_react3.useState)(0);
980
+ const localRef = (0, import_react3.useRef)(null);
981
+ (0, import_react3.useEffect)(() => {
982
+ if (!active || !videoRef) return;
983
+ videoRef.current = localRef.current;
984
+ return () => {
985
+ if (videoRef.current === localRef.current) videoRef.current = null;
986
+ };
987
+ }, [active, videoRef]);
988
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsxs)("div", { className: "pointer-events-none absolute inset-0", children: [
989
+ /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
990
+ "video",
991
+ {
992
+ ref: localRef,
993
+ src: url,
994
+ playsInline: true,
995
+ preload: active ? "auto" : "metadata",
996
+ muted: !active,
997
+ loop: false,
998
+ onPlay: () => setPaused(false),
999
+ onPause: () => setPaused(true),
1000
+ onEnded: () => setPaused(true),
1001
+ onTimeUpdate: (e) => {
1002
+ const v = e.currentTarget;
1003
+ setProgress(v.duration > 0 ? v.currentTime / v.duration : 0);
1004
+ },
1005
+ className: "absolute inset-0 h-full w-full object-contain"
1006
+ }
1007
+ ),
1008
+ active && paused && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "absolute inset-0 flex items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "flex h-16 w-16 items-center justify-center rounded-full bg-black/45", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(import_lucide_react2.Play, { className: "ml-1 h-8 w-8 fill-white text-white" }) }) }),
1009
+ active && !paused && /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className: "absolute inset-x-4 bottom-2 h-0.5 overflow-hidden rounded-full bg-white/25", children: /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
1010
+ "span",
1011
+ {
1012
+ className: "block h-full rounded-full bg-white",
1013
+ style: { width: `${Math.round(progress * 100)}%` }
1014
+ }
1015
+ ) })
1016
+ ] });
1017
+ }
968
1018
 
969
1019
  // src/components/ImageEditSheet.tsx
970
1020
  var import_react4 = require("react");
@@ -1649,7 +1699,7 @@ function CameraCapture({
1649
1699
 
1650
1700
  // src/components/CameraCaptureV3.tsx
1651
1701
  var import_react8 = require("react");
1652
- var import_lucide_react5 = require("lucide-react");
1702
+ var import_lucide_react6 = require("lucide-react");
1653
1703
 
1654
1704
  // src/components/CaptureRail.tsx
1655
1705
  var import_react6 = require("react");
@@ -1741,9 +1791,10 @@ function Chevron({ className }) {
1741
1791
 
1742
1792
  // src/components/HoldShutter.tsx
1743
1793
  var import_react7 = require("react");
1794
+ var import_lucide_react5 = require("lucide-react");
1744
1795
  var import_jsx_runtime13 = require("react/jsx-runtime");
1745
1796
  var HOLD_MS = 320;
1746
- var LOCK_AFTER_MS = 1200;
1797
+ var LOCK_DRAG_PX = 64;
1747
1798
  function HoldShutter({
1748
1799
  recording,
1749
1800
  elapsedSeconds,
@@ -1756,6 +1807,7 @@ function HoldShutter({
1756
1807
  }) {
1757
1808
  const holdTimer = (0, import_react7.useRef)(null);
1758
1809
  const startedRecording = (0, import_react7.useRef)(false);
1810
+ const pressOriginY = (0, import_react7.useRef)(0);
1759
1811
  const [pressed, setPressed] = (0, import_react7.useState)(false);
1760
1812
  const [locked, setLocked] = (0, import_react7.useState)(false);
1761
1813
  const clearHold = (0, import_react7.useCallback)(() => {
@@ -1765,17 +1817,14 @@ function HoldShutter({
1765
1817
  }
1766
1818
  }, []);
1767
1819
  (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]);
1820
+ if (!recording) setLocked(false);
1821
+ }, [recording]);
1774
1822
  (0, import_react7.useEffect)(() => clearHold, [clearHold]);
1775
1823
  const onPointerDown = (0, import_react7.useCallback)(
1776
1824
  (event) => {
1777
1825
  if (disabled) return;
1778
1826
  event.currentTarget.setPointerCapture?.(event.pointerId);
1827
+ pressOriginY.current = event.clientY;
1779
1828
  setPressed(true);
1780
1829
  onPressStart?.();
1781
1830
  if (recording && locked) return;
@@ -1788,6 +1837,16 @@ function HoldShutter({
1788
1837
  },
1789
1838
  [disabled, locked, onPressStart, onStartRecording, recording]
1790
1839
  );
1840
+ const onPointerMove = (0, import_react7.useCallback)(
1841
+ (event) => {
1842
+ if (!pressed || locked || !startedRecording.current) return;
1843
+ if (pressOriginY.current - event.clientY >= LOCK_DRAG_PX) {
1844
+ setLocked(true);
1845
+ navigator.vibrate?.(30);
1846
+ }
1847
+ },
1848
+ [pressed, locked]
1849
+ );
1791
1850
  const endPress = (0, import_react7.useCallback)(() => {
1792
1851
  if (!pressed) return;
1793
1852
  setPressed(false);
@@ -1843,12 +1902,34 @@ function HoldShutter({
1843
1902
  ]
1844
1903
  }
1845
1904
  ) : null,
1905
+ recording && pressed && !locked && /* @__PURE__ */ (0, import_jsx_runtime13.jsxs)(
1906
+ "span",
1907
+ {
1908
+ className: "pointer-events-none absolute left-1/2 flex -translate-x-1/2 flex-col items-center gap-0.5",
1909
+ style: { top: -LOCK_DRAG_PX - 22 },
1910
+ "aria-hidden": true,
1911
+ children: [
1912
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "flex h-9 w-9 items-center justify-center rounded-full bg-black/60 ring-1 ring-white/30", children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Lock, { className: "h-4 w-4 text-white/90" }) }),
1913
+ /* @__PURE__ */ (0, import_jsx_runtime13.jsx)("span", { className: "text-[10px] font-medium text-white/80 drop-shadow", children: "Slide up to lock" })
1914
+ ]
1915
+ }
1916
+ ),
1917
+ recording && locked && /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1918
+ "span",
1919
+ {
1920
+ className: "pointer-events-none absolute left-1/2 flex h-9 w-9 -translate-x-1/2 items-center justify-center rounded-full bg-[#FF3B30]",
1921
+ style: { top: -LOCK_DRAG_PX - 22 },
1922
+ "aria-hidden": true,
1923
+ children: /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(import_lucide_react5.Lock, { className: "h-4 w-4 text-white", fill: "currentColor" })
1924
+ }
1925
+ ),
1846
1926
  /* @__PURE__ */ (0, import_jsx_runtime13.jsx)(
1847
1927
  "button",
1848
1928
  {
1849
1929
  type: "button",
1850
1930
  disabled,
1851
1931
  onPointerDown,
1932
+ onPointerMove,
1852
1933
  onPointerUp: endPress,
1853
1934
  onPointerCancel: endPress,
1854
1935
  onLostPointerCapture: endPress,
@@ -1962,7 +2043,7 @@ function CameraCaptureV3({
1962
2043
  {
1963
2044
  id: "flip",
1964
2045
  label: "Switch camera",
1965
- icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.RefreshCw, { className: "h-[22px] w-[22px]" }),
2046
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.RefreshCw, { className: "h-[22px] w-[22px]" }),
1966
2047
  primary: true,
1967
2048
  onPress: engine.onFlipCamera
1968
2049
  }
@@ -1971,7 +2052,7 @@ function CameraCaptureV3({
1971
2052
  {
1972
2053
  id: "flash",
1973
2054
  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]" }),
2055
+ icon: controls.torchOn ? /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.Zap, { className: "h-[22px] w-[22px]", fill: "currentColor" }) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.ZapOff, { className: "h-[22px] w-[22px]" }),
1975
2056
  active: controls.torchOn,
1976
2057
  primary: true,
1977
2058
  onPress: controls.toggleTorch
@@ -1980,7 +2061,7 @@ function CameraCaptureV3({
1980
2061
  {
1981
2062
  id: "timer",
1982
2063
  label: "Timer",
1983
- icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Timer, { className: "h-[22px] w-[22px]" }),
2064
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.Timer, { className: "h-[22px] w-[22px]" }),
1984
2065
  active: timerSetting !== 0,
1985
2066
  valueLabel: timerSetting === 0 ? void 0 : `${timerSetting}s`,
1986
2067
  onPress: () => setTimerSetting((t) => t === 0 ? 3 : t === 3 ? 10 : 0)
@@ -1988,14 +2069,14 @@ function CameraCaptureV3({
1988
2069
  {
1989
2070
  id: "grid",
1990
2071
  label: "Grid",
1991
- icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Grid3x3, { className: "h-[22px] w-[22px]" }),
2072
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.Grid3x3, { className: "h-[22px] w-[22px]" }),
1992
2073
  active: gridOn,
1993
2074
  onPress: () => setGridOn((g) => !g)
1994
2075
  },
1995
2076
  {
1996
2077
  id: "aspect",
1997
2078
  label: "Aspect",
1998
- icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.Proportions, { className: "h-[22px] w-[22px]" }),
2079
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.Proportions, { className: "h-[22px] w-[22px]" }),
1999
2080
  active: aspect !== "full",
2000
2081
  valueLabel: aspect === "full" ? void 0 : aspect,
2001
2082
  onPress: () => setAspect(
@@ -2006,14 +2087,17 @@ function CameraCaptureV3({
2006
2087
  {
2007
2088
  id: "exposure",
2008
2089
  label: "Exposure",
2009
- icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react5.SunMedium, { className: "h-[22px] w-[22px]" }),
2090
+ icon: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.SunMedium, { className: "h-[22px] w-[22px]" }),
2010
2091
  active: controls.exposure !== 0,
2011
2092
  valueLabel: controls.exposure === 0 ? void 0 : `${controls.exposure > 0 ? "+" : ""}${controls.exposure}`,
2012
2093
  onPress: () => controls.setExposure(controls.exposure === 0 ? 1 : 0)
2013
2094
  }
2014
2095
  ] : []
2015
2096
  ];
2016
- const railActions = [...coreRail, ...slots.railActions ?? []];
2097
+ const railActions = [
2098
+ ...coreRail.map((a) => blocked ? { ...a, disabled: true } : a),
2099
+ ...slots.railActions ?? []
2100
+ ];
2017
2101
  return /* @__PURE__ */ (0, import_jsx_runtime14.jsxs)("div", { className: "absolute inset-0 select-none overflow-hidden bg-black", children: [
2018
2102
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "absolute inset-0", children: preview }),
2019
2103
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(GridOverlay, { visible: gridOn && !blocked }),
@@ -2052,7 +2136,7 @@ function CameraCaptureV3({
2052
2136
  onClick: onClose,
2053
2137
  "aria-label": "Close camera",
2054
2138
  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" })
2139
+ children: /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(import_lucide_react6.X, { className: "h-6 w-6" })
2056
2140
  }
2057
2141
  ) : /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("span", { className: "h-10 w-10 shrink-0" }),
2058
2142
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)("div", { className: "pointer-events-auto flex min-w-0 flex-1 justify-center", children: slots.topEntry }),
@@ -2067,12 +2151,14 @@ function CameraCaptureV3({
2067
2151
  {
2068
2152
  className: "pointer-events-none absolute inset-x-0 z-20 flex flex-col items-center gap-1.5 px-3",
2069
2153
  style: {
2070
- top: `calc(env(safe-area-inset-top, 0px) + ${controlsHidden ? 12 : 64}px)`
2154
+ // Clears the top row AND the topCenter line beneath it — chips
2155
+ // rendered at the label's own height read as a collision.
2156
+ top: `calc(env(safe-area-inset-top, 0px) + ${controlsHidden ? 12 : slots.topCenter ? 92 : 64}px)`
2071
2157
  },
2072
2158
  children: slots.statusChips
2073
2159
  }
2074
2160
  ) : null,
2075
- !controlsHidden && !blocked && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2161
+ !controlsHidden && /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(
2076
2162
  "div",
2077
2163
  {
2078
2164
  className: "pointer-events-none absolute right-1 z-20 flex justify-end",
@@ -2097,7 +2183,7 @@ function CameraCaptureV3({
2097
2183
  onClick: cloud.onOpenLibrary,
2098
2184
  "aria-label": "Your media \u2014 library and upload",
2099
2185
  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" }) })
2186
+ 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_react6.Images, { className: "h-5 w-5 text-white/80" }) })
2101
2187
  }
2102
2188
  ) }),
2103
2189
  /* @__PURE__ */ (0, import_jsx_runtime14.jsx)(