saccade 0.1.0 → 0.2.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/index.d.cts CHANGED
@@ -2,15 +2,8 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
 
5
- type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
- type SaccadeProps = {
7
- /** Panel position. Default: 'bottom-left' */
8
- position?: SaccadePosition;
9
- };
10
- declare function Saccade({ position }: SaccadeProps): react.ReactPortal | null;
11
-
12
5
  type ExportFilter = 'active' | 'all-animations' | 'all-elements';
13
- type OutputDetailLevel = 'compact' | 'standard' | 'detailed' | 'forensic';
6
+ type OutputDetailLevel = 'brief' | 'moderate' | 'detailed' | 'granular';
14
7
  type Rect = {
15
8
  x: number;
16
9
  y: number;
@@ -112,6 +105,18 @@ declare class SaccadeEngine {
112
105
  getCapture(): TimelineCapture | null;
113
106
  setSpeed(speed: number): void;
114
107
  getSpeed(): number;
108
+ /**
109
+ * Install the timing patches immediately, without changing speed.
110
+ *
111
+ * Call this as early as possible (before app code, GSAP, or Framer Motion
112
+ * run) so they capture the patched time functions rather than the originals.
113
+ * Idempotent and harmless to call more than once. `setSpeed` and
114
+ * `startRecording` also install on demand, so this is only needed to win the
115
+ * early-load race against libraries that cache `Date.now`/`performance.now`.
116
+ */
117
+ install(): void;
118
+ /** Register a module-imported GSAP instance so saccade can slow it. */
119
+ registerGSAP(gsap: any): void;
115
120
  startRecording(boundingBox?: Rect | null): void;
116
121
  stopRecording(): TimelineCapture;
117
122
  seekTo(timeMs: number): void;
@@ -123,8 +128,23 @@ declare class SaccadeEngine {
123
128
  destroy(): void;
124
129
  }
125
130
 
126
- declare function SaccadeProvider({ children }: {
131
+ type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
132
+ type SaccadeProps = {
133
+ /** Panel position. Default: 'bottom-left' */
134
+ position?: SaccadePosition;
135
+ /** Engine the panel should drive. Defaults to the shared singleton, so app
136
+ * code can control the same engine the panel does (e.g. via
137
+ * `getSharedEngine()` or `import 'saccade/install'`). Pass your own only if
138
+ * you deliberately want an isolated engine. */
139
+ engine?: SaccadeEngine;
140
+ };
141
+ declare function Saccade({ position, engine }: SaccadeProps): react.ReactPortal | null;
142
+
143
+ declare function SaccadeProvider({ children, engine }: {
127
144
  children: ReactNode;
145
+ /** Engine to provide. Defaults to the process-wide shared singleton so the
146
+ * panel and app code (and `saccade/install`) all drive the same instance. */
147
+ engine?: SaccadeEngine;
128
148
  }): react_jsx_runtime.JSX.Element;
129
149
  declare function useSaccadeEngine(): SaccadeEngine;
130
150
 
@@ -151,4 +171,8 @@ declare function useSpeed(): {
151
171
  togglePause: () => void;
152
172
  };
153
173
 
154
- export { type AnimationInfo, type ExportFilter, type FrameSnapshot, type OutputDetailLevel, type Rect, Saccade, SaccadeEngine, type SaccadePosition, type SaccadeProps, SaccadeProvider, type SaccadeState, type TimelineCapture, type TimelineExport, useSaccadeEngine, useSpeed, useTimeline };
174
+ declare function getSharedEngine(): SaccadeEngine;
175
+ /** Test/teardown hook — drops the singleton so the next get() makes a fresh one. */
176
+ declare function resetSharedEngine(): void;
177
+
178
+ export { type AnimationInfo, type ExportFilter, type FrameSnapshot, type OutputDetailLevel, type Rect, Saccade, SaccadeEngine, type SaccadePosition, type SaccadeProps, SaccadeProvider, type SaccadeState, type TimelineCapture, type TimelineExport, getSharedEngine, resetSharedEngine, useSaccadeEngine, useSpeed, useTimeline };
package/dist/index.d.ts CHANGED
@@ -2,15 +2,8 @@ import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
 
5
- type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
6
- type SaccadeProps = {
7
- /** Panel position. Default: 'bottom-left' */
8
- position?: SaccadePosition;
9
- };
10
- declare function Saccade({ position }: SaccadeProps): react.ReactPortal | null;
11
-
12
5
  type ExportFilter = 'active' | 'all-animations' | 'all-elements';
13
- type OutputDetailLevel = 'compact' | 'standard' | 'detailed' | 'forensic';
6
+ type OutputDetailLevel = 'brief' | 'moderate' | 'detailed' | 'granular';
14
7
  type Rect = {
15
8
  x: number;
16
9
  y: number;
@@ -112,6 +105,18 @@ declare class SaccadeEngine {
112
105
  getCapture(): TimelineCapture | null;
113
106
  setSpeed(speed: number): void;
114
107
  getSpeed(): number;
108
+ /**
109
+ * Install the timing patches immediately, without changing speed.
110
+ *
111
+ * Call this as early as possible (before app code, GSAP, or Framer Motion
112
+ * run) so they capture the patched time functions rather than the originals.
113
+ * Idempotent and harmless to call more than once. `setSpeed` and
114
+ * `startRecording` also install on demand, so this is only needed to win the
115
+ * early-load race against libraries that cache `Date.now`/`performance.now`.
116
+ */
117
+ install(): void;
118
+ /** Register a module-imported GSAP instance so saccade can slow it. */
119
+ registerGSAP(gsap: any): void;
115
120
  startRecording(boundingBox?: Rect | null): void;
116
121
  stopRecording(): TimelineCapture;
117
122
  seekTo(timeMs: number): void;
@@ -123,8 +128,23 @@ declare class SaccadeEngine {
123
128
  destroy(): void;
124
129
  }
125
130
 
126
- declare function SaccadeProvider({ children }: {
131
+ type SaccadePosition = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
132
+ type SaccadeProps = {
133
+ /** Panel position. Default: 'bottom-left' */
134
+ position?: SaccadePosition;
135
+ /** Engine the panel should drive. Defaults to the shared singleton, so app
136
+ * code can control the same engine the panel does (e.g. via
137
+ * `getSharedEngine()` or `import 'saccade/install'`). Pass your own only if
138
+ * you deliberately want an isolated engine. */
139
+ engine?: SaccadeEngine;
140
+ };
141
+ declare function Saccade({ position, engine }: SaccadeProps): react.ReactPortal | null;
142
+
143
+ declare function SaccadeProvider({ children, engine }: {
127
144
  children: ReactNode;
145
+ /** Engine to provide. Defaults to the process-wide shared singleton so the
146
+ * panel and app code (and `saccade/install`) all drive the same instance. */
147
+ engine?: SaccadeEngine;
128
148
  }): react_jsx_runtime.JSX.Element;
129
149
  declare function useSaccadeEngine(): SaccadeEngine;
130
150
 
@@ -151,4 +171,8 @@ declare function useSpeed(): {
151
171
  togglePause: () => void;
152
172
  };
153
173
 
154
- export { type AnimationInfo, type ExportFilter, type FrameSnapshot, type OutputDetailLevel, type Rect, Saccade, SaccadeEngine, type SaccadePosition, type SaccadeProps, SaccadeProvider, type SaccadeState, type TimelineCapture, type TimelineExport, useSaccadeEngine, useSpeed, useTimeline };
174
+ declare function getSharedEngine(): SaccadeEngine;
175
+ /** Test/teardown hook — drops the singleton so the next get() makes a fresh one. */
176
+ declare function resetSharedEngine(): void;
177
+
178
+ export { type AnimationInfo, type ExportFilter, type FrameSnapshot, type OutputDetailLevel, type Rect, Saccade, SaccadeEngine, type SaccadePosition, type SaccadeProps, SaccadeProvider, type SaccadeState, type TimelineCapture, type TimelineExport, getSharedEngine, resetSharedEngine, useSaccadeEngine, useSpeed, useTimeline };
package/dist/index.mjs CHANGED
@@ -20,6 +20,7 @@ var TimingController = class {
20
20
  this._origAnimate = null;
21
21
  this.installed = false;
22
22
  this.animPollId = 0;
23
+ this.gsapInstance = null;
23
24
  // WeakMap tracking for animations and media
24
25
  this.trackedAnims = /* @__PURE__ */ new WeakMap();
25
26
  this.trackedMedia = /* @__PURE__ */ new WeakMap();
@@ -195,13 +196,19 @@ var TimingController = class {
195
196
  } catch {
196
197
  }
197
198
  }
199
+ /**
200
+ * Register a GSAP instance (for ES-module imports where window.gsap is
201
+ * undefined). Applies the current timeScale immediately if speed !== 1.
202
+ */
203
+ registerGSAP(gsap) {
204
+ this.gsapInstance = gsap;
205
+ if (this.speed !== 1) this.patchGSAP();
206
+ }
198
207
  /** Sync GSAP's global timeline if present. */
199
208
  patchGSAP() {
200
209
  try {
201
- const gsap = window.gsap;
202
- if (gsap?.globalTimeline) {
203
- gsap.globalTimeline.timeScale(this.speed || 1e-3);
204
- }
210
+ const gsap = this.gsapInstance ?? window.gsap;
211
+ gsap?.globalTimeline?.timeScale(this.speed || 1e-3);
205
212
  } catch {
206
213
  }
207
214
  }
@@ -247,10 +254,11 @@ var TimingController = class {
247
254
  } catch {
248
255
  }
249
256
  try {
250
- const gsap = window.gsap;
251
- if (gsap?.globalTimeline) gsap.globalTimeline.timeScale(1);
257
+ const gsap = this.gsapInstance ?? window.gsap;
258
+ gsap?.globalTimeline?.timeScale(1);
252
259
  } catch {
253
260
  }
261
+ this.gsapInstance = null;
254
262
  delete window.__LAPSE_ORIGINAL_RAF__;
255
263
  delete window.__saccadeInstalled;
256
264
  this.installed = false;
@@ -1525,7 +1533,7 @@ function generateExport(animations, frames, timeMs, filter = "active") {
1525
1533
  animations: animExports
1526
1534
  };
1527
1535
  }
1528
- function formatExportForLLM(exp, detail = "standard") {
1536
+ function formatExportForLLM(exp, detail = "moderate") {
1529
1537
  const lines = [];
1530
1538
  const grouped = /* @__PURE__ */ new Map();
1531
1539
  for (const anim of exp.animations) {
@@ -1540,7 +1548,7 @@ function formatExportForLLM(exp, detail = "standard") {
1540
1548
  const [from, to] = prop.range.split(" \u2192 ");
1541
1549
  return !(from && to && from.trim() === to.trim());
1542
1550
  }
1543
- if (detail === "compact") {
1551
+ if (detail === "brief") {
1544
1552
  lines.push(`# Animation State at ${exp.timestamp}`);
1545
1553
  lines.push("");
1546
1554
  for (const [, group] of grouped) {
@@ -1565,7 +1573,7 @@ function formatExportForLLM(exp, detail = "standard") {
1565
1573
  }
1566
1574
  lines.push(`# Animation State at ${exp.timestamp}`);
1567
1575
  lines.push("");
1568
- if (detail === "forensic") {
1576
+ if (detail === "granular") {
1569
1577
  lines.push("**Environment:**");
1570
1578
  lines.push(`- Viewport: ${window.innerWidth}\xD7${window.innerHeight}`);
1571
1579
  lines.push(`- URL: ${window.location.href}`);
@@ -1632,7 +1640,7 @@ function formatExportForLLM(exp, detail = "standard") {
1632
1640
  lines.push(`Transitions: ${[...transitionSet].join(", ")}`);
1633
1641
  lines.push("");
1634
1642
  for (const line of cssPropLines) lines.push(line);
1635
- if (detail === "detailed" || detail === "forensic") {
1643
+ if (detail === "detailed" || detail === "granular") {
1636
1644
  const allVars = {};
1637
1645
  for (const anim of cssAnims) {
1638
1646
  if (anim.resolvedVars) Object.assign(allVars, anim.resolvedVars);
@@ -1687,6 +1695,22 @@ var SaccadeEngine = class {
1687
1695
  getSpeed() {
1688
1696
  return this.timing.getSpeed();
1689
1697
  }
1698
+ /**
1699
+ * Install the timing patches immediately, without changing speed.
1700
+ *
1701
+ * Call this as early as possible (before app code, GSAP, or Framer Motion
1702
+ * run) so they capture the patched time functions rather than the originals.
1703
+ * Idempotent and harmless to call more than once. `setSpeed` and
1704
+ * `startRecording` also install on demand, so this is only needed to win the
1705
+ * early-load race against libraries that cache `Date.now`/`performance.now`.
1706
+ */
1707
+ install() {
1708
+ this.timing.install();
1709
+ }
1710
+ /** Register a module-imported GSAP instance so saccade can slow it. */
1711
+ registerGSAP(gsap) {
1712
+ this.timing.registerGSAP(gsap);
1713
+ }
1690
1714
  // -- Timeline recording ---------------------------------------------------
1691
1715
  startRecording(boundingBox) {
1692
1716
  if (this._state !== "idle") return;
@@ -1761,7 +1785,7 @@ var SaccadeEngine = class {
1761
1785
  filter
1762
1786
  );
1763
1787
  }
1764
- exportForLLM(timeMs, filter = "active", detail = "standard") {
1788
+ exportForLLM(timeMs, filter = "active", detail = "moderate") {
1765
1789
  const exp = this.generateExport(timeMs, filter);
1766
1790
  if (!exp) return "";
1767
1791
  return formatExportForLLM(exp, detail);
@@ -1786,13 +1810,29 @@ var SaccadeEngine = class {
1786
1810
  }
1787
1811
  };
1788
1812
 
1813
+ // src/core/shared.ts
1814
+ var KEY = "__saccadeSharedEngine__";
1815
+ function getSharedEngine() {
1816
+ const g = globalThis;
1817
+ if (!g[KEY]) g[KEY] = new SaccadeEngine();
1818
+ return g[KEY];
1819
+ }
1820
+ function resetSharedEngine() {
1821
+ const g = globalThis;
1822
+ g[KEY]?.destroy();
1823
+ g[KEY] = null;
1824
+ }
1825
+
1789
1826
  // src/react/SaccadeContext.tsx
1790
1827
  import { jsx } from "react/jsx-runtime";
1791
1828
  var SaccadeContext = createContext(null);
1792
- function SaccadeProvider({ children }) {
1829
+ function SaccadeProvider({
1830
+ children,
1831
+ engine
1832
+ }) {
1793
1833
  const engineRef = useRef(null);
1794
1834
  if (!engineRef.current) {
1795
- engineRef.current = new SaccadeEngine();
1835
+ engineRef.current = engine ?? getSharedEngine();
1796
1836
  }
1797
1837
  return /* @__PURE__ */ jsx(SaccadeContext.Provider, { value: engineRef.current, children });
1798
1838
  }
@@ -1809,10 +1849,10 @@ import { useRef as useRef5, useCallback as useCallback5 } from "react";
1809
1849
  import { useCallback, useRef as useRef2, useState, useEffect } from "react";
1810
1850
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
1811
1851
  var DETAIL_LABELS = {
1812
- compact: "Compact",
1813
- standard: "Standard",
1852
+ brief: "Brief",
1853
+ moderate: "Moderate",
1814
1854
  detailed: "Detailed",
1815
- forensic: "Forensic"
1855
+ granular: "Granular"
1816
1856
  };
1817
1857
  function CopyCheckIcon({ copied }) {
1818
1858
  const spring = "cubic-bezier(0.34, 1.15, 0.64, 1)";
@@ -1866,10 +1906,10 @@ function CopyCheckIcon({ copied }) {
1866
1906
  ] });
1867
1907
  }
1868
1908
  var DETAIL_BRIGHT_COUNT = {
1869
- compact: 1,
1870
- standard: 2,
1909
+ brief: 1,
1910
+ moderate: 2,
1871
1911
  detailed: 3,
1872
- forensic: 4
1912
+ granular: 4
1873
1913
  };
1874
1914
  function DetailIcon({ level }) {
1875
1915
  const bright = DETAIL_BRIGHT_COUNT[level];
@@ -2194,7 +2234,9 @@ function SpeedControl({ speed, isPaused, onSetSpeed, onTogglePause }) {
2194
2234
 
2195
2235
  // src/react/useTimeline.ts
2196
2236
  import { useState as useState2, useCallback as useCallback3, useEffect as useEffect2, useRef as useRef4, useSyncExternalStore } from "react";
2197
- var DETAIL_LEVELS = ["compact", "standard", "detailed", "forensic"];
2237
+ var _realSetTimeout = setTimeout.bind(window);
2238
+ var _realClearTimeout = clearTimeout.bind(window);
2239
+ var DETAIL_LEVELS = ["brief", "moderate", "detailed", "granular"];
2198
2240
  function useTimeline() {
2199
2241
  const engine = useSaccadeEngine();
2200
2242
  const state = useSyncExternalStore(
@@ -2205,7 +2247,7 @@ function useTimeline() {
2205
2247
  const [scrubTime, setScrubTime] = useState2(0);
2206
2248
  const [copied, setCopied] = useState2(false);
2207
2249
  const [exportFilter, setExportFilter] = useState2("all-animations");
2208
- const [detailLevel, setDetailLevel] = useState2("standard");
2250
+ const [detailLevel, setDetailLevel] = useState2("moderate");
2209
2251
  const copiedTimeout = useRef4(null);
2210
2252
  const pendingSeek = useRef4(null);
2211
2253
  const rafId = useRef4(0);
@@ -2279,8 +2321,8 @@ function useTimeline() {
2279
2321
  navigator.clipboard.writeText(text).catch(() => {
2280
2322
  });
2281
2323
  setCopied(true);
2282
- if (copiedTimeout.current) clearTimeout(copiedTimeout.current);
2283
- copiedTimeout.current = setTimeout(() => setCopied(false), 2e3);
2324
+ if (copiedTimeout.current) _realClearTimeout(copiedTimeout.current);
2325
+ copiedTimeout.current = _realSetTimeout(() => setCopied(false), 1800);
2284
2326
  return text;
2285
2327
  },
2286
2328
  [engine, capture, scrubTime, exportFilter, detailLevel]
@@ -2832,7 +2874,7 @@ var PANEL_STYLES = (
2832
2874
 
2833
2875
  // src/react/Saccade.tsx
2834
2876
  import { jsx as jsx5 } from "react/jsx-runtime";
2835
- function Saccade({ position = "bottom-left" }) {
2877
+ function Saccade({ position = "bottom-left", engine }) {
2836
2878
  const [shadowRoot, setShadowRoot] = useState4(null);
2837
2879
  const hostRef = useRef6(null);
2838
2880
  useEffect4(() => {
@@ -2856,7 +2898,7 @@ function Saccade({ position = "bottom-left" }) {
2856
2898
  }, [position]);
2857
2899
  if (!shadowRoot) return null;
2858
2900
  return createPortal(
2859
- /* @__PURE__ */ jsx5(SaccadeProvider, { children: /* @__PURE__ */ jsx5(SaccadePanel, {}) }),
2901
+ /* @__PURE__ */ jsx5(SaccadeProvider, { engine, children: /* @__PURE__ */ jsx5(SaccadePanel, {}) }),
2860
2902
  shadowRoot.lastElementChild || shadowRoot
2861
2903
  );
2862
2904
  }
@@ -2864,6 +2906,8 @@ export {
2864
2906
  Saccade,
2865
2907
  SaccadeEngine,
2866
2908
  SaccadeProvider,
2909
+ getSharedEngine,
2910
+ resetSharedEngine,
2867
2911
  useSaccadeEngine,
2868
2912
  useSpeed,
2869
2913
  useTimeline