snapshot-trace 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 snapshot-trace contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # snapshot-trace
2
+
3
+ Runtime diagnostic recorder for any web app, designed for AI-assisted debugging.
4
+ Any code calls `emit(...)`; a ●REC button in your UI buffers the events; ■STOP
5
+ writes them as JSON to `/tmp/snapshot-trace.json` — a fixed path your coding
6
+ agent reads directly. No console-log copy-paste.
7
+
8
+ Framework-agnostic: one zero-dependency browser module + one standalone Node
9
+ listener. Works on Vite, Next, webpack, CRA, or a static page.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm i -D snapshot-trace
15
+ ```
16
+
17
+ ## Setup (the one manual step)
18
+
19
+ Run the listener alongside your dev server:
20
+
21
+ ```jsonc
22
+ // package.json
23
+ "scripts": { "snapshot:serve": "snapshot-trace serve" }
24
+ ```
25
+
26
+ ```bash
27
+ npm run snapshot:serve # second terminal
28
+ ```
29
+
30
+ Then add a ●REC button (Vue/React/vanilla snippets in `templates/`, or ask your
31
+ AI — `npx snapshot-trace skill`). The recorder disables itself outside dev.
32
+
33
+ ## Use
34
+
35
+ ```ts
36
+ import { emit } from 'snapshot-trace'
37
+ emit('checkout:submit', { cartId, total })
38
+ ```
39
+
40
+ Click ●REC → reproduce → ■STOP. The snapshot lands at `/tmp/snapshot-trace.json`.
41
+
42
+ ## AI skill
43
+
44
+ ```bash
45
+ npx snapshot-trace skill # print the skill
46
+ npx snapshot-trace skill --install # install into .claude/skills/
47
+ ```
48
+
49
+ ## Config
50
+
51
+ | Flag | Env | Default |
52
+ |---|---|---|
53
+ | `--port` | `SNAPSHOT_TRACE_PORT` | `7878` |
54
+ | `--out` | `SNAPSHOT_TRACE_OUT` | `/tmp/snapshot-trace.json` |
55
+
56
+ ## License
57
+
58
+ MIT
package/dist/cli.mjs ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ // src/cli.mjs
3
+ import { fileURLToPath, pathToFileURL } from 'node:url'
4
+ import { dirname, join } from 'node:path'
5
+ import { readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs'
6
+ import { startFromArgv } from './server.mjs'
7
+
8
+ const HERE = dirname(fileURLToPath(import.meta.url))
9
+ // In dist/, SKILL.md sits at ../skill/SKILL.md relative to the bin; in src/ too.
10
+ function skillPath() {
11
+ return join(HERE, '..', 'skill', 'SKILL.md')
12
+ }
13
+
14
+ export async function run(argv) {
15
+ const [cmd, ...rest] = argv
16
+ if (cmd === 'serve') {
17
+ startFromArgv(rest)
18
+ return 0
19
+ }
20
+ if (cmd === 'skill') {
21
+ const text = readFileSync(skillPath(), 'utf8')
22
+ if (rest.includes('--install')) {
23
+ const dest = join(process.cwd(), '.claude', 'skills', 'snapshot-trace', 'SKILL.md')
24
+ mkdirSync(dirname(dest), { recursive: true })
25
+ writeFileSync(dest, text, 'utf8')
26
+ process.stdout.write(`installed skill to ${dest}\n`)
27
+ } else {
28
+ process.stdout.write(text)
29
+ }
30
+ return 0
31
+ }
32
+ process.stderr.write(
33
+ 'usage: snapshot-trace <serve|skill> [options]\n' +
34
+ ' serve [--port N] [--out PATH]\n' +
35
+ ' skill [--install]\n',
36
+ )
37
+ return 1
38
+ }
39
+
40
+ // Only auto-run when invoked directly (incl. through the npm bin symlink),
41
+ // not when imported by tests. Resolve argv[1] through realpath so the
42
+ // .bin symlink (and macOS /tmp→/private/tmp) collapses before comparing.
43
+ const invokedDirectly =
44
+ process.argv[1] &&
45
+ pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url
46
+ if (invokedDirectly) {
47
+ run(process.argv.slice(2)).then((code) => { if (code) process.exit(code) })
48
+ }
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/recorder.ts
21
+ var recorder_exports = {};
22
+ __export(recorder_exports, {
23
+ createRecorder: () => createRecorder,
24
+ emit: () => emit,
25
+ resolveEnabled: () => resolveEnabled
26
+ });
27
+ module.exports = __toCommonJS(recorder_exports);
28
+ var import_meta = {};
29
+ function win() {
30
+ return typeof window !== "undefined" ? window : void 0;
31
+ }
32
+ function emit(event, detail) {
33
+ win()?.__snapshotTraceBus?.(event, detail);
34
+ }
35
+ function createRecorder(options = {}) {
36
+ const endpoint = options.endpoint ?? "http://localhost:7878/snapshot";
37
+ const enabled = resolveEnabled(options.enabled);
38
+ let recording = false;
39
+ let frames = [];
40
+ let startTs = 0;
41
+ let bus = null;
42
+ let reclaim = null;
43
+ const subs = /* @__PURE__ */ new Set();
44
+ const notify = () => subs.forEach((c) => c());
45
+ function installBus() {
46
+ bus = (event, detail) => {
47
+ if (!recording) return;
48
+ frames.push({ t: Date.now() - startTs, event, detail });
49
+ };
50
+ const w = win();
51
+ if (w) w.__snapshotTraceBus = bus;
52
+ }
53
+ function start() {
54
+ if (!enabled || recording) return;
55
+ frames = [];
56
+ startTs = Date.now();
57
+ recording = true;
58
+ installBus();
59
+ reclaim = setInterval(() => {
60
+ if (win()?.__snapshotTraceBus !== bus) installBus();
61
+ }, 500);
62
+ notify();
63
+ }
64
+ async function stop() {
65
+ if (!recording) return;
66
+ recording = false;
67
+ if (reclaim) {
68
+ clearInterval(reclaim);
69
+ reclaim = null;
70
+ }
71
+ const snapshot = {
72
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
73
+ source: "snapshot-trace",
74
+ frameCount: frames.length,
75
+ frames
76
+ };
77
+ const payload = JSON.stringify(snapshot, null, 2);
78
+ notify();
79
+ await flush(endpoint, payload);
80
+ }
81
+ function toggle() {
82
+ recording ? void stop() : start();
83
+ }
84
+ return {
85
+ start,
86
+ stop,
87
+ toggle,
88
+ isRecording: () => recording,
89
+ subscribe(cb) {
90
+ subs.add(cb);
91
+ return () => subs.delete(cb);
92
+ }
93
+ };
94
+ }
95
+ async function flush(endpoint, payload) {
96
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
97
+ navigator.clipboard.writeText(payload).catch(() => {
98
+ });
99
+ }
100
+ try {
101
+ await fetch(endpoint, { method: "POST", body: payload });
102
+ } catch {
103
+ }
104
+ }
105
+ function viteDev() {
106
+ try {
107
+ const env = import_meta.env;
108
+ return env ? Boolean(env.DEV) : void 0;
109
+ } catch {
110
+ return void 0;
111
+ }
112
+ }
113
+ function resolveEnabled(explicit, devSignal = viteDev) {
114
+ if (typeof explicit === "boolean") return explicit;
115
+ const vd = devSignal();
116
+ if (typeof vd === "boolean") return vd;
117
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV) {
118
+ return process.env.NODE_ENV !== "production";
119
+ }
120
+ const host = win()?.location?.hostname;
121
+ return host === "localhost" || host === "127.0.0.1";
122
+ }
123
+ // Annotate the CommonJS export names for ESM import in node:
124
+ 0 && (module.exports = {
125
+ createRecorder,
126
+ emit,
127
+ resolveEnabled
128
+ });
@@ -0,0 +1,21 @@
1
+ type Frame = {
2
+ t: number;
3
+ event: string;
4
+ detail: unknown;
5
+ };
6
+ type Recorder = {
7
+ start(): void;
8
+ stop(): Promise<void>;
9
+ toggle(): void;
10
+ isRecording(): boolean;
11
+ subscribe(cb: () => void): () => void;
12
+ };
13
+ /** Drop anywhere in app code. No-ops unless a recorder is active and recording. */
14
+ declare function emit(event: string, detail?: unknown): void;
15
+ declare function createRecorder(options?: {
16
+ endpoint?: string;
17
+ enabled?: boolean;
18
+ }): Recorder;
19
+ declare function resolveEnabled(explicit?: boolean, devSignal?: () => boolean | undefined): boolean;
20
+
21
+ export { type Frame, type Recorder, createRecorder, emit, resolveEnabled };
@@ -0,0 +1,21 @@
1
+ type Frame = {
2
+ t: number;
3
+ event: string;
4
+ detail: unknown;
5
+ };
6
+ type Recorder = {
7
+ start(): void;
8
+ stop(): Promise<void>;
9
+ toggle(): void;
10
+ isRecording(): boolean;
11
+ subscribe(cb: () => void): () => void;
12
+ };
13
+ /** Drop anywhere in app code. No-ops unless a recorder is active and recording. */
14
+ declare function emit(event: string, detail?: unknown): void;
15
+ declare function createRecorder(options?: {
16
+ endpoint?: string;
17
+ enabled?: boolean;
18
+ }): Recorder;
19
+ declare function resolveEnabled(explicit?: boolean, devSignal?: () => boolean | undefined): boolean;
20
+
21
+ export { type Frame, type Recorder, createRecorder, emit, resolveEnabled };
@@ -0,0 +1,100 @@
1
+ // src/recorder.ts
2
+ function win() {
3
+ return typeof window !== "undefined" ? window : void 0;
4
+ }
5
+ function emit(event, detail) {
6
+ win()?.__snapshotTraceBus?.(event, detail);
7
+ }
8
+ function createRecorder(options = {}) {
9
+ const endpoint = options.endpoint ?? "http://localhost:7878/snapshot";
10
+ const enabled = resolveEnabled(options.enabled);
11
+ let recording = false;
12
+ let frames = [];
13
+ let startTs = 0;
14
+ let bus = null;
15
+ let reclaim = null;
16
+ const subs = /* @__PURE__ */ new Set();
17
+ const notify = () => subs.forEach((c) => c());
18
+ function installBus() {
19
+ bus = (event, detail) => {
20
+ if (!recording) return;
21
+ frames.push({ t: Date.now() - startTs, event, detail });
22
+ };
23
+ const w = win();
24
+ if (w) w.__snapshotTraceBus = bus;
25
+ }
26
+ function start() {
27
+ if (!enabled || recording) return;
28
+ frames = [];
29
+ startTs = Date.now();
30
+ recording = true;
31
+ installBus();
32
+ reclaim = setInterval(() => {
33
+ if (win()?.__snapshotTraceBus !== bus) installBus();
34
+ }, 500);
35
+ notify();
36
+ }
37
+ async function stop() {
38
+ if (!recording) return;
39
+ recording = false;
40
+ if (reclaim) {
41
+ clearInterval(reclaim);
42
+ reclaim = null;
43
+ }
44
+ const snapshot = {
45
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
46
+ source: "snapshot-trace",
47
+ frameCount: frames.length,
48
+ frames
49
+ };
50
+ const payload = JSON.stringify(snapshot, null, 2);
51
+ notify();
52
+ await flush(endpoint, payload);
53
+ }
54
+ function toggle() {
55
+ recording ? void stop() : start();
56
+ }
57
+ return {
58
+ start,
59
+ stop,
60
+ toggle,
61
+ isRecording: () => recording,
62
+ subscribe(cb) {
63
+ subs.add(cb);
64
+ return () => subs.delete(cb);
65
+ }
66
+ };
67
+ }
68
+ async function flush(endpoint, payload) {
69
+ if (typeof navigator !== "undefined" && navigator.clipboard) {
70
+ navigator.clipboard.writeText(payload).catch(() => {
71
+ });
72
+ }
73
+ try {
74
+ await fetch(endpoint, { method: "POST", body: payload });
75
+ } catch {
76
+ }
77
+ }
78
+ function viteDev() {
79
+ try {
80
+ const env = import.meta.env;
81
+ return env ? Boolean(env.DEV) : void 0;
82
+ } catch {
83
+ return void 0;
84
+ }
85
+ }
86
+ function resolveEnabled(explicit, devSignal = viteDev) {
87
+ if (typeof explicit === "boolean") return explicit;
88
+ const vd = devSignal();
89
+ if (typeof vd === "boolean") return vd;
90
+ if (typeof process !== "undefined" && process.env && process.env.NODE_ENV) {
91
+ return process.env.NODE_ENV !== "production";
92
+ }
93
+ const host = win()?.location?.hostname;
94
+ return host === "localhost" || host === "127.0.0.1";
95
+ }
96
+ export {
97
+ createRecorder,
98
+ emit,
99
+ resolveEnabled
100
+ };
@@ -0,0 +1,63 @@
1
+ import { createServer as httpCreate } from 'node:http'
2
+ import { writeFileSync, mkdirSync } from 'node:fs'
3
+ import { dirname } from 'node:path'
4
+
5
+ const CORS = {
6
+ 'Access-Control-Allow-Origin': '*',
7
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
8
+ 'Access-Control-Allow-Headers': 'Content-Type',
9
+ }
10
+
11
+ export function createServer({ port = 7878, out = '/tmp/snapshot-trace.json' } = {}) {
12
+ const server = httpCreate((req, res) => {
13
+ if (req.url !== '/snapshot') {
14
+ res.writeHead(404, CORS).end('not found')
15
+ return
16
+ }
17
+ if (req.method === 'OPTIONS') {
18
+ res.writeHead(204, CORS).end()
19
+ return
20
+ }
21
+ if (req.method !== 'POST') {
22
+ res.writeHead(405, CORS).end('POST only')
23
+ return
24
+ }
25
+ const chunks = []
26
+ req.on('data', (c) => chunks.push(c))
27
+ req.on('end', () => {
28
+ try {
29
+ const body = Buffer.concat(chunks).toString('utf8')
30
+ mkdirSync(dirname(out), { recursive: true })
31
+ writeFileSync(out, body, 'utf8')
32
+ res.writeHead(200, { ...CORS, 'Content-Type': 'application/json' })
33
+ res.end(JSON.stringify({ ok: true, path: out, bytes: Buffer.byteLength(body) }))
34
+ } catch (err) {
35
+ res.writeHead(500, CORS).end(String(err))
36
+ }
37
+ })
38
+ })
39
+ return server
40
+ }
41
+
42
+ export function startFromArgv(argv) {
43
+ const opts = parseArgs(argv)
44
+ const port = opts.port ?? (Number(process.env.SNAPSHOT_TRACE_PORT) || 7878)
45
+ const out = opts.out ?? process.env.SNAPSHOT_TRACE_OUT ?? '/tmp/snapshot-trace.json'
46
+ const server = createServer({ port, out })
47
+ server.listen(port, '127.0.0.1', () => {
48
+ process.stdout.write(
49
+ `snapshot-trace listening on http://localhost:${port}\n` +
50
+ ` writing snapshots to ${out}\n`,
51
+ )
52
+ })
53
+ return server
54
+ }
55
+
56
+ export function parseArgs(argv) {
57
+ const out = {}
58
+ for (let i = 0; i < argv.length; i++) {
59
+ if (argv[i] === '--port') out.port = Number(argv[++i])
60
+ else if (argv[i] === '--out') out.out = argv[++i]
61
+ }
62
+ return out
63
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "snapshot-trace",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic runtime diagnostic recorder whose output a coding agent reads directly.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": ["debugging", "diagnostics", "recorder", "tracing", "devtools", "ai", "agent", "vite", "framework-agnostic"],
8
+ "repository": { "type": "git", "url": "git+https://github.com/nachogames/snapshot-trace.git" },
9
+ "homepage": "https://github.com/nachogames/snapshot-trace#readme",
10
+ "bugs": { "url": "https://github.com/nachogames/snapshot-trace/issues" },
11
+ "bin": { "snapshot-trace": "./dist/cli.mjs" },
12
+ "main": "./dist/recorder.cjs",
13
+ "module": "./dist/recorder.js",
14
+ "types": "./dist/recorder.d.ts",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/recorder.d.ts",
18
+ "import": "./dist/recorder.js",
19
+ "require": "./dist/recorder.cjs"
20
+ }
21
+ },
22
+ "files": ["dist", "skill", "templates", "README.md", "LICENSE"],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "prepublishOnly": "npm run build && npm test"
28
+ },
29
+ "engines": { "node": ">=18" },
30
+ "devDependencies": {
31
+ "@types/node": "^20.0.0",
32
+ "tsup": "^8.0.0",
33
+ "typescript": "^5.4.0",
34
+ "vitest": "^1.6.0"
35
+ }
36
+ }
package/skill/SKILL.md ADDED
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: snapshot-trace
3
+ description: Add ad-hoc diagnostic events to a global recorder so the user captures them with a ●REC button and hands back a JSON file you read directly. Use when a frontend bug is reproducible but code-reading can't tell what's actually firing — instead of console.log spam, emit() to the bus and have the user record a ●REC→reproduce→■STOP cycle.
4
+ ---
5
+
6
+ # snapshot-trace
7
+
8
+ A framework-agnostic runtime recorder. Any code calls `emit('event', detail)`; a ●REC button buffers events; ■STOP flushes them as JSON to `/tmp/snapshot-trace.json`, which you read directly. Prefer this over `console.log` for runtime debugging — it's structured, ordered, and needs no copy-paste.
9
+
10
+ ## One-time setup (do this when the project isn't wired yet)
11
+
12
+ 1. Install: `npm i -D snapshot-trace` (or pnpm/yarn equivalent).
13
+ 2. Add a dev script so the listener runs during development:
14
+ ```jsonc
15
+ "scripts": { "snapshot:serve": "snapshot-trace serve" }
16
+ ```
17
+ The user runs `npm run snapshot:serve` in a second terminal alongside their dev server. (Do NOT auto-edit their existing `dev` script.)
18
+ 3. Add a ●REC button in the project's framework. Pick the matching template and place it somewhere visible in dev:
19
+ - Vue → `templates/vue.txt`
20
+ - React → `templates/react.txt`
21
+ - Plain DOM → `templates/vanilla.txt`
22
+
23
+ The button just wraps `createRecorder()`; it self-disables outside dev.
24
+ 4. Confirm `emit` is importable: `import { emit } from 'snapshot-trace'`.
25
+
26
+ ## The debugging loop
27
+
28
+ 1. Form a hypothesis about what's firing.
29
+ 2. Drop `emit` calls at suspected hot spots. One line, safe anywhere:
30
+ ```ts
31
+ import { emit } from 'snapshot-trace'
32
+ emit('source:phase', { /* small serializable detail */ })
33
+ ```
34
+ Include a stack when you need to know the caller:
35
+ ```ts
36
+ emit('updateThing:call', { id, stack: new Error().stack?.split('\n').slice(2, 12).join('\n') })
37
+ ```
38
+ 3. Hand off precisely:
39
+ > Make sure `npm run snapshot:serve` is running. Hard-refresh, click ●REC, reproduce the bug, click ■STOP.
40
+ 4. Read `/tmp/snapshot-trace.json` directly. Cite frames by `t` and `event`.
41
+ 5. Apply the fix.
42
+ 6. **Strip the `emit` hooks you added before committing:**
43
+ ```bash
44
+ grep -rn "emit('" src/ # find the ones you added; remove them
45
+ ```
46
+
47
+ ## Naming convention
48
+
49
+ `<source>:<phase>` so the snapshot greps cleanly: `updateTask:PUT`, `dragStart:enter`, `onSelect:fire`.
50
+
51
+ ## Snapshot shape
52
+
53
+ ```json
54
+ { "ts": "<ISO>", "source": "snapshot-trace", "frameCount": 12,
55
+ "frames": [ { "t": 0, "event": "a:b", "detail": { } } ] }
56
+ ```
57
+ `t` is ms since ●REC. Frames are in insertion order.
58
+
59
+ ## Red flags
60
+
61
+ - Asking the user to copy-paste console output — wrong; the file handoff exists for this.
62
+ - Auto-editing their dev-server config — never. The listener is a separate process.
63
+ - Forgetting to remove your `emit` hooks before commit.
64
+ - Hooking a path the user can't reach — verify your hook is on the reproducible path first.
@@ -0,0 +1,12 @@
1
+ import { useSyncExternalStore, useRef } from 'react'
2
+ import { createRecorder } from 'snapshot-trace'
3
+
4
+ export function SnapshotRecButton() {
5
+ const rec = useRef(createRecorder()).current
6
+ const recording = useSyncExternalStore(rec.subscribe, rec.isRecording, () => false)
7
+ return (
8
+ <button onClick={() => rec.toggle()} style={{ color: recording ? 'red' : undefined }}>
9
+ {recording ? '■STOP' : '●REC'}
10
+ </button>
11
+ )
12
+ }
@@ -0,0 +1,13 @@
1
+ <!-- Plain-DOM REC button. Renders only in dev (createRecorder gates itself). -->
2
+ <button id="snapshot-rec">●REC</button>
3
+ <script type="module">
4
+ import { createRecorder } from 'snapshot-trace'
5
+ const rec = createRecorder()
6
+ const btn = document.getElementById('snapshot-rec')
7
+ const paint = () => {
8
+ btn.textContent = rec.isRecording() ? '■STOP' : '●REC'
9
+ btn.style.color = rec.isRecording() ? 'red' : ''
10
+ }
11
+ rec.subscribe(paint); paint()
12
+ btn.addEventListener('click', () => rec.toggle())
13
+ </script>
@@ -0,0 +1,15 @@
1
+ <script setup lang="ts">
2
+ import { ref, onUnmounted } from 'vue'
3
+ import { createRecorder } from 'snapshot-trace'
4
+
5
+ const rec = createRecorder()
6
+ const recording = ref(rec.isRecording())
7
+ const off = rec.subscribe(() => { recording.value = rec.isRecording() })
8
+ onUnmounted(off)
9
+ </script>
10
+
11
+ <template>
12
+ <button :style="{ color: recording ? 'red' : undefined }" @click="rec.toggle()">
13
+ {{ recording ? '■STOP' : '●REC' }}
14
+ </button>
15
+ </template>