retake-dev 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/LICENSE +91 -0
- package/README.md +198 -0
- package/bin/retake.js +319 -0
- package/package.json +87 -0
- package/src/code-versions.js +357 -0
- package/src/core.js +265 -0
- package/src/plugin.js +137 -0
- package/src/runtime/00-core.js +308 -0
- package/src/runtime/10-animations.js +362 -0
- package/src/runtime/20-media.js +157 -0
- package/src/runtime/30-recorder.js +315 -0
- package/src/runtime/32-state.js +315 -0
- package/src/runtime/35-network.js +789 -0
- package/src/runtime/36-scripts.js +129 -0
- package/src/runtime/37-next.js +150 -0
- package/src/runtime/38-observers.js +252 -0
- package/src/runtime/40-input.js +795 -0
- package/src/runtime/45-hover.js +78 -0
- package/src/runtime/50-engine.js +341 -0
- package/src/runtime/60-preview.js +437 -0
- package/src/runtime/65-timeline.js +289 -0
- package/src/runtime/66-activity.js +114 -0
- package/src/runtime/67-csssource.js +226 -0
- package/src/runtime/70-boot.js +460 -0
- package/src/server/api.js +285 -0
- package/src/server/child.js +174 -0
- package/src/server/detect.js +167 -0
- package/src/server/front.js +647 -0
- package/src/server/mcp.js +332 -0
- package/src/shell/00-state.js +85 -0
- package/src/shell/05-api.js +136 -0
- package/src/shell/10-dock.js +754 -0
- package/src/shell/12-checkpoint.js +121 -0
- package/src/shell/15-session.js +291 -0
- package/src/shell/20-timeline.js +734 -0
- package/src/shell/25-input.js +537 -0
- package/src/shell/30-notes.js +870 -0
- package/src/shell/40-code.js +80 -0
- package/src/shell/90-handle.js +15 -0
- package/src/shell/shell.css +295 -0
- package/src/shell/shell.html +59 -0
- package/types/client.d.ts +73 -0
- package/types/index.d.ts +111 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
// What the dock draws: markers (things you did) and clips (things that
|
|
2
|
+
// animated), derived from the recording and the animation log. See
|
|
3
|
+
// CONTRACT.md ("Runtime API").
|
|
4
|
+
|
|
5
|
+
// ---- describing elements ------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
function cssEscape(s) {
|
|
8
|
+
return W.CSS && CSS.escape ? CSS.escape(s) : String(s).replace(/[^\w-]/g, (c) => "\\" + c)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// A readable, valid selector: an id if there is one, else tag.class chains
|
|
12
|
+
// with :nth-of-type where needed, up to <body>.
|
|
13
|
+
function selectorOf(el) {
|
|
14
|
+
if (!el || el.nodeType !== 1) return null
|
|
15
|
+
const parts = []
|
|
16
|
+
for (let n = el; n && n.nodeType === 1 && n.tagName !== "HTML"; n = n.parentElement) {
|
|
17
|
+
if (n.id) {
|
|
18
|
+
parts.unshift(`#${cssEscape(n.id)}`)
|
|
19
|
+
break
|
|
20
|
+
}
|
|
21
|
+
let part = n.tagName.toLowerCase()
|
|
22
|
+
const cls = [...n.classList].filter((c) => /^[a-z][\w-]*$/i.test(c)).slice(0, 2)
|
|
23
|
+
if (cls.length) part += "." + cls.map(cssEscape).join(".")
|
|
24
|
+
const same = n.parentElement ? [...n.parentElement.children].filter((c) => c.tagName === n.tagName) : []
|
|
25
|
+
if (same.length > 1) part += `:nth-of-type(${same.indexOf(n) + 1})`
|
|
26
|
+
parts.unshift(part)
|
|
27
|
+
if (n.tagName === "BODY") break
|
|
28
|
+
}
|
|
29
|
+
return parts.join(" > ")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Nearest named React component, unwrapping memo/forwardRef.
|
|
33
|
+
function componentOf(el) {
|
|
34
|
+
try {
|
|
35
|
+
const key = el && Object.keys(el).find((k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"))
|
|
36
|
+
for (let f = key && el[key]; f; f = f.return) {
|
|
37
|
+
let t = f.type
|
|
38
|
+
if (t && typeof t === "object") t = t.render || t.type || null
|
|
39
|
+
if (t && typeof t === "object") t = t.render || null
|
|
40
|
+
const name = typeof t === "function" ? t.displayName || t.name : null
|
|
41
|
+
if (name && /^[A-Z]/.test(name)) return name
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function labelOf(el) {
|
|
48
|
+
if (!el || el.nodeType !== 1) return ""
|
|
49
|
+
const aria = el.getAttribute("aria-label") || el.getAttribute("title") || el.getAttribute("placeholder")
|
|
50
|
+
const text = aria || (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ")
|
|
51
|
+
const tag = el.tagName.toLowerCase()
|
|
52
|
+
return text ? text.slice(0, 40) : tag + (el.id ? "#" + el.id : "")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ---- markers ------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
// Markers are the user's actions only (causes, not effects): clicks,
|
|
58
|
+
// submits, route changes, focus into a field, and typing, one marker per
|
|
59
|
+
// burst (keystrokes under 800ms apart in one field) with an end.
|
|
60
|
+
const TYPE_GAP = 800
|
|
61
|
+
let markerCache = null
|
|
62
|
+
function markersOf() {
|
|
63
|
+
const key = rec.events.length + ":" + (rec.routes ? rec.routes.length : 0) + ":" + (rec.start || 0)
|
|
64
|
+
if (markerCache && markerCache.key === key) return markerCache.value
|
|
65
|
+
const out = []
|
|
66
|
+
let burst = null
|
|
67
|
+
const start = rec.start || 0
|
|
68
|
+
for (const ev of rec.events) {
|
|
69
|
+
if (ev.t < start) continue
|
|
70
|
+
if (ev.type === "click") {
|
|
71
|
+
// Clicking into a field focused it first (on mousedown): one marker, the click.
|
|
72
|
+
const prev = out[out.length - 1]
|
|
73
|
+
if (prev && prev.kind === "focus" && prev.selector === ev.css && ev.t - prev.t < 1000) out.pop()
|
|
74
|
+
out.push({ t: ev.t, kind: "click", label: ev.label || "click", selector: ev.css || undefined })
|
|
75
|
+
}
|
|
76
|
+
else if (ev.type === "submit") out.push({ t: ev.t, kind: "submit", label: ev.label || "submit", selector: ev.css || undefined })
|
|
77
|
+
else if (ev.type === "focusin" && ev.editable) {
|
|
78
|
+
// Clicking into a field already made a click marker there.
|
|
79
|
+
const prev = out[out.length - 1]
|
|
80
|
+
if (!(prev && prev.kind === "click" && prev.selector === ev.css && ev.t - prev.t < 50)) out.push({ t: ev.t, kind: "focus", label: ev.label || "field", selector: ev.css || undefined })
|
|
81
|
+
} else if (ev.type === "input") {
|
|
82
|
+
const typed = /^insert/.test(ev.inputType || "insertText") ? (ev.data ? ev.data.length : 1) : 0
|
|
83
|
+
if (burst && burst.selector === (ev.css || undefined) && ev.t - burst.end < TYPE_GAP) {
|
|
84
|
+
burst.end = ev.t
|
|
85
|
+
burst.chars += typed
|
|
86
|
+
} else {
|
|
87
|
+
burst = { t: ev.t, end: ev.t, kind: "type", chars: typed, selector: ev.css || undefined }
|
|
88
|
+
out.push(burst)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
for (const m of out) {
|
|
93
|
+
if (m.kind === "type") {
|
|
94
|
+
m.label = `typed ${m.chars} char${m.chars === 1 ? "" : "s"}`
|
|
95
|
+
delete m.chars
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const r of rec.routes || []) if (r.t >= start) out.push({ t: r.t, kind: "route", label: r.path })
|
|
99
|
+
out.sort((a, b) => a.t - b.t)
|
|
100
|
+
markerCache = { key, value: out }
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- clips --------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function clipKind(a) {
|
|
107
|
+
if (typeof CSSTransition !== "undefined" && a instanceof CSSTransition) return "transition"
|
|
108
|
+
if (typeof CSSAnimation !== "undefined" && a instanceof CSSAnimation) return "css-animation"
|
|
109
|
+
return "waapi"
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function clipEnd(e) {
|
|
113
|
+
if (e.vEnd != null) return e.vEnd
|
|
114
|
+
const t = e.timing || {}
|
|
115
|
+
const total = (Number(t.delay) || 0) + (Number(t.duration) || 0) * (t.iterations == null ? 1 : t.iterations) + (Number(t.endDelay) || 0)
|
|
116
|
+
return Number.isFinite(total) ? e.vStart + total / (e.rate || 1) : null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Clips live in the recording (rec.clips), so a rebuilt frame still knows
|
|
120
|
+
// the whole timeline's clips, including the ones after the playhead. They're
|
|
121
|
+
// described once, when the animation starts at the live edge.
|
|
122
|
+
const clipOfEntry = new WeakMap()
|
|
123
|
+
function describeClip(e) {
|
|
124
|
+
const a = e.anim
|
|
125
|
+
const kind = clipKind(a)
|
|
126
|
+
const props = [...new Set((e.keyframes || []).flatMap((k) => Object.keys(k).filter((p) => !["offset", "easing", "composite", "computedOffset"].includes(p))))]
|
|
127
|
+
const property = kind === "transition" ? a.transitionProperty : props.join(", ") || undefined
|
|
128
|
+
const label = kind === "css-animation" ? a.animationName : kind === "transition" ? `${a.transitionProperty} transition` : props.length ? props.join(", ") : "animation"
|
|
129
|
+
const pseudo = (a.effect && a.effect.pseudoElement) || null
|
|
130
|
+
return { kind, label, selector: selectorOf(e.target) + (pseudo || ""), component: componentOf(e.target) || undefined, property, pseudoElement: pseudo }
|
|
131
|
+
}
|
|
132
|
+
// The recorded start of the clip a new animation replays, if there is one:
|
|
133
|
+
// same element, kind and property/name, not yet claimed, started near now.
|
|
134
|
+
const claimedClips = new WeakSet()
|
|
135
|
+
function recordedStart(a) {
|
|
136
|
+
const target = a.effect && a.effect.target
|
|
137
|
+
if (!target || !rec || !rec.clips) return null
|
|
138
|
+
const path = pathOf(target)
|
|
139
|
+
if (!path || !Array.isArray(path)) return null
|
|
140
|
+
const key = path.join(",")
|
|
141
|
+
const kind = clipKind(a)
|
|
142
|
+
const pseudo = (a.effect && a.effect.pseudoElement) || null
|
|
143
|
+
const name = kind === "transition" ? a.transitionProperty : kind === "css-animation" ? a.animationName : null
|
|
144
|
+
let best = null
|
|
145
|
+
for (const c of rec.clips) {
|
|
146
|
+
if (claimedClips.has(c) || c.kind !== kind || (c.pseudoElement || null) !== pseudo || !c.path) continue
|
|
147
|
+
if (c.start > clock.now + 250 || c.start < clock.now - 250) continue
|
|
148
|
+
if (name != null && (kind === "transition" ? c.property : c.label) !== name) continue
|
|
149
|
+
if (c.path.join(",") !== key) continue
|
|
150
|
+
if (!best || Math.abs(c.start - clock.now) < Math.abs(best.start - clock.now)) best = c
|
|
151
|
+
}
|
|
152
|
+
if (!best) return null
|
|
153
|
+
claimedClips.add(best)
|
|
154
|
+
return best.start
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function recordClip(e) {
|
|
158
|
+
if (!rec || hasFuture() || clock.seeking) return // replaying: rec.clips already has it
|
|
159
|
+
const clips = rec.clips || (rec.clips = [])
|
|
160
|
+
/** @type {Record<string, any>} */
|
|
161
|
+
const c = { id: `c${clips.length + 1}`, start: e.vStart, end: clipEnd(e), ...describeClip(e), path: pathOf(e.target) }
|
|
162
|
+
const iters = e.timing && e.timing.iterations
|
|
163
|
+
if (iters === Infinity) c.iterations = "infinite"
|
|
164
|
+
else if (iters > 1) c.iterations = iters
|
|
165
|
+
if (c.property === undefined) delete c.property
|
|
166
|
+
if (c.component === undefined) delete c.component
|
|
167
|
+
clips.push(c)
|
|
168
|
+
clipOfEntry.set(e, c)
|
|
169
|
+
}
|
|
170
|
+
function endClip(e) {
|
|
171
|
+
const c = clipOfEntry.get(e)
|
|
172
|
+
if (c && (c.end == null || e.vEnd < c.end)) c.end = e.vEnd
|
|
173
|
+
}
|
|
174
|
+
function clipsOf() {
|
|
175
|
+
const start = rec.start || 0
|
|
176
|
+
return (rec.clips || []).filter((c) => c.start >= start || (c.end != null && c.end >= start))
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function timeline() {
|
|
180
|
+
return {
|
|
181
|
+
now: clock.now,
|
|
182
|
+
end: Math.max(rec.end, clock.now),
|
|
183
|
+
viewport: rec.viewport || { w: innerWidth, h: innerHeight },
|
|
184
|
+
markers: markersOf(),
|
|
185
|
+
clips: clipsOf(),
|
|
186
|
+
activity: activitySamples(),
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Clips on an element and everything inside it (for the dock's ⌘-hover
|
|
191
|
+
// lens). Clips carry the DOM path of their element from when they were
|
|
192
|
+
// recorded, so this is a prefix match, in any frame (or, for a path that
|
|
193
|
+
// starts at an id inside the element, where it leads).
|
|
194
|
+
const clipKeys = new WeakMap()
|
|
195
|
+
function clipsFor(target) {
|
|
196
|
+
let el = target
|
|
197
|
+
if (typeof target === "string") {
|
|
198
|
+
try {
|
|
199
|
+
el = document.querySelector(target)
|
|
200
|
+
} catch {
|
|
201
|
+
el = null
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const path = el && pathOf(el)
|
|
205
|
+
if (!path || !Array.isArray(path)) return []
|
|
206
|
+
const key = path.join(",")
|
|
207
|
+
const out = []
|
|
208
|
+
for (const c of clipsOf()) {
|
|
209
|
+
if (!c.path) continue
|
|
210
|
+
let k = clipKeys.get(c)
|
|
211
|
+
if (k == null) clipKeys.set(c, (k = c.path.join(",")))
|
|
212
|
+
if (k === key || k.startsWith(key + ",")) out.push(c)
|
|
213
|
+
else if (pathV2()) {
|
|
214
|
+
// Paths start at the nearest id (F51): one inside el can start below it.
|
|
215
|
+
const node = resolvePath(c.path)
|
|
216
|
+
if (node && node !== el && el.contains(node)) out.push(c)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return out
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clipAt(t, selector) {
|
|
223
|
+
let best = null
|
|
224
|
+
for (const c of clipsOf()) {
|
|
225
|
+
const end = c.end == null ? Math.max(rec.end, clock.now) : c.end
|
|
226
|
+
if (c.start > t || t > end) continue
|
|
227
|
+
if (selector) {
|
|
228
|
+
if (c.selector !== selector) {
|
|
229
|
+
let el = null
|
|
230
|
+
try {
|
|
231
|
+
el = document.querySelector(selector)
|
|
232
|
+
} catch {}
|
|
233
|
+
const target = c.selector && document.querySelector(c.selector)
|
|
234
|
+
if (!el || !target || !(el === target || el.contains(target))) continue
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (!best || c.start > best.start) best = c
|
|
238
|
+
}
|
|
239
|
+
return best ? { clip: best, offset: t - best.start } : null
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ---- interaction model -------------------------------------------------------
|
|
243
|
+
// At the live edge the app is live and recorded. In the past (rewound,
|
|
244
|
+
// previewing, rebuilding, or playing back recorded future) it's view-only.
|
|
245
|
+
|
|
246
|
+
let toolActive = false
|
|
247
|
+
function setToolActive(on) {
|
|
248
|
+
toolActive = !!on
|
|
249
|
+
}
|
|
250
|
+
// Live = playing at the live edge. Paused or in the past it's view-only.
|
|
251
|
+
function isInteractive() {
|
|
252
|
+
return !!rec && clock.playing && !hasFuture() && !previewing && !clock.seeking && !(shell && shell.rebuilding) && !toolActive
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ---- routes (for markers) ---------------------------------------------------------
|
|
256
|
+
|
|
257
|
+
const cleanRoute = (path) => path.replace(/([?&])__wb=app&?/, "$1").replace(/[?&]$/, "")
|
|
258
|
+
function noteRoute() {
|
|
259
|
+
if (!rec || clock.seeking || hasFuture()) return
|
|
260
|
+
const routes = rec.routes || (rec.routes = [])
|
|
261
|
+
const clean = cleanRoute(location.pathname + location.search + location.hash)
|
|
262
|
+
if (routes.length && routes[routes.length - 1].path === clean) return
|
|
263
|
+
routes.push({ t: clock.now, path: clean })
|
|
264
|
+
}
|
|
265
|
+
// The app's route at t on this document's page: its last pushState/replaceState
|
|
266
|
+
// (or popstate) by then, else the URL the page loaded at. A preview shows the
|
|
267
|
+
// DOM of t without touching history, so the dock's address bar asks this.
|
|
268
|
+
function routeAt(t) {
|
|
269
|
+
const seg = segmentAt(t)
|
|
270
|
+
let path = null
|
|
271
|
+
for (const r of rec.routes || []) if (r.t >= seg.t && r.t <= t) path = r.path
|
|
272
|
+
if (path) return path
|
|
273
|
+
try {
|
|
274
|
+
const u = new URL(seg.url)
|
|
275
|
+
return cleanRoute(u.pathname + u.search + u.hash)
|
|
276
|
+
} catch {
|
|
277
|
+
return null
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
for (const m of ["pushState", "replaceState"]) {
|
|
281
|
+
const orig = history[m]
|
|
282
|
+
history[m] = function (...args) {
|
|
283
|
+
const r = orig.apply(this, args)
|
|
284
|
+
noteRoute()
|
|
285
|
+
return r
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
W.addEventListener("popstate", noteRoute)
|
|
289
|
+
W.addEventListener("hashchange", noteRoute)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// How much of the screen changed, over time: a cheap waveform for the dock.
|
|
2
|
+
// Every 100ms of virtual time while recording, the on-screen boxes of running
|
|
3
|
+
// finite animations and of elements the DOM changed are painted onto a coarse
|
|
4
|
+
// grid over the viewport (so overlaps count once); the covered share is the
|
|
5
|
+
// raw value. A rolling median over the last ~5s is subtracted, so constant
|
|
6
|
+
// ambient motion (spinners, pulsing dots) trends to 0 and real changes spike.
|
|
7
|
+
// No screenshots, no pixel diffs. Samples live in the recording
|
|
8
|
+
// (rec.activity), so replays and seeks never recompute them.
|
|
9
|
+
|
|
10
|
+
const ACT_STEP = 100
|
|
11
|
+
const ACT_BASE = 50 // samples in the rolling median (~5s)
|
|
12
|
+
const GRID_W = 32
|
|
13
|
+
const GRID_H = 20
|
|
14
|
+
const ACT_MAX_ELS = 150
|
|
15
|
+
const actGrid = new Uint8Array(GRID_W * GRID_H)
|
|
16
|
+
let actDomAt = 0
|
|
17
|
+
let actRaw = [] // recent raw values, for the baseline
|
|
18
|
+
const actStats = { samples: 0, ms: 0, max: 0 }
|
|
19
|
+
|
|
20
|
+
function markRect(r, vw, vh) {
|
|
21
|
+
if (!r || r.width <= 0 || r.height <= 0) return
|
|
22
|
+
const x0 = Math.max(0, Math.floor((r.left / vw) * GRID_W))
|
|
23
|
+
const x1 = Math.min(GRID_W - 1, Math.floor(((r.right - 0.01) / vw) * GRID_W))
|
|
24
|
+
const y0 = Math.max(0, Math.floor((r.top / vh) * GRID_H))
|
|
25
|
+
const y1 = Math.min(GRID_H - 1, Math.floor(((r.bottom - 0.01) / vh) * GRID_H))
|
|
26
|
+
for (let y = y0; y <= y1; y++) for (let x = x0; x <= x1; x++) actGrid[y * GRID_W + x] = 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rawActivity() {
|
|
30
|
+
const vw = W.innerWidth || 1
|
|
31
|
+
const vh = W.innerHeight || 1
|
|
32
|
+
actGrid.fill(0)
|
|
33
|
+
const els = new Set()
|
|
34
|
+
// (a) running, finite animations and transitions
|
|
35
|
+
for (const [a, st] of managed) {
|
|
36
|
+
if (st.done || st.userPaused) continue
|
|
37
|
+
const timing = a.effect && a.effect.getTiming ? a.effect.getTiming() : null
|
|
38
|
+
if (!timing || timing.iterations === Infinity) continue
|
|
39
|
+
const t = a.effect.target
|
|
40
|
+
if (t && t.nodeType === 1) els.add(t)
|
|
41
|
+
}
|
|
42
|
+
// (b) elements the DOM changed since the last sample
|
|
43
|
+
if (observer && !previewing) logMutations(observer.takeRecords())
|
|
44
|
+
let removedFrom = null
|
|
45
|
+
for (let i = actDomAt; i < domLog.length && els.size < ACT_MAX_ELS; i++) {
|
|
46
|
+
const e = domLog[i]
|
|
47
|
+
if (e.kind === "attr" || e.kind === "value") els.add(e.node)
|
|
48
|
+
else if (e.kind === "text") e.node.parentElement && els.add(e.node.parentElement)
|
|
49
|
+
else {
|
|
50
|
+
for (const n of e.added) if (n.nodeType === 1) els.add(n)
|
|
51
|
+
else if (n.parentElement) els.add(n.parentElement)
|
|
52
|
+
// Something left: the space it took was its container's.
|
|
53
|
+
if (e.removed.length) (removedFrom || (removedFrom = new Set())).add(e.node)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
actDomAt = domLog.length
|
|
57
|
+
if (removedFrom) for (const n of removedFrom) if (n.nodeType === 1 && n !== document.body && n !== document.documentElement) els.add(n)
|
|
58
|
+
for (const el of els) {
|
|
59
|
+
if (!el.isConnected || el.nodeType !== 1) continue
|
|
60
|
+
markRect(el.getBoundingClientRect(), vw, vh)
|
|
61
|
+
}
|
|
62
|
+
let covered = 0
|
|
63
|
+
for (let i = 0; i < actGrid.length; i++) covered += actGrid[i]
|
|
64
|
+
return covered / actGrid.length
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function median(arr) {
|
|
68
|
+
if (!arr.length) return 0
|
|
69
|
+
const s = arr.slice().sort((a, b) => a - b)
|
|
70
|
+
return s[s.length >> 1]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Called at each frame boundary. Takes a sample for every 100ms window the
|
|
74
|
+
// clock has passed through, while recording at the live edge.
|
|
75
|
+
function sampleActivity() {
|
|
76
|
+
if (!rec || clock.seeking || hasFuture() || previewing) {
|
|
77
|
+
actDomAt = domLog.length // what a replay does isn't new activity
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
const a = rec.activity || (rec.activity = { start: Math.floor(clock.now / ACT_STEP) * ACT_STEP, step: ACT_STEP, v: [] })
|
|
81
|
+
const due = Math.floor((clock.now - a.start) / ACT_STEP)
|
|
82
|
+
if (a.v.length >= due) return
|
|
83
|
+
const t0 = real.perfNow()
|
|
84
|
+
const raw = rawActivity()
|
|
85
|
+
actRaw.push(raw)
|
|
86
|
+
if (actRaw.length > ACT_BASE) actRaw = actRaw.slice(-ACT_BASE)
|
|
87
|
+
const v = Math.max(0, raw - median(actRaw))
|
|
88
|
+
a.v.push(Math.round(v * 100))
|
|
89
|
+
while (a.v.length < due) a.v.push(0) // windows skipped in one jump
|
|
90
|
+
const ms = real.perfNow() - t0
|
|
91
|
+
actStats.samples++
|
|
92
|
+
actStats.ms += ms
|
|
93
|
+
if (ms > actStats.max) actStats.max = ms
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let activityOut = null
|
|
97
|
+
function activitySamples() {
|
|
98
|
+
const a = rec.activity
|
|
99
|
+
if (!a) return []
|
|
100
|
+
if (activityOut && activityOut.n === a.v.length && activityOut.start === a.start) return activityOut.value
|
|
101
|
+
const value = a.v.map((v, i) => ({ t: a.start + i * a.step, v: v / 100 }))
|
|
102
|
+
// The same data as { step, values, t0 } too (the dock's waveform reads that).
|
|
103
|
+
value.step = a.step
|
|
104
|
+
value.t0 = a.start
|
|
105
|
+
value.values = a.v.map((v) => v / 100)
|
|
106
|
+
activityOut = { n: a.v.length, start: a.start, value }
|
|
107
|
+
return value
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Cutting the future at a fork drops its samples too.
|
|
111
|
+
function cutActivity(t) {
|
|
112
|
+
const a = rec.activity
|
|
113
|
+
if (a) a.v.length = Math.max(0, Math.min(a.v.length, Math.floor((t - a.start) / a.step)))
|
|
114
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Where a CSS animation or transition comes from: the rule that sets it on
|
|
2
|
+
// the element (or its ::before/::after), and the @keyframes it runs, as
|
|
3
|
+
// file:line in the project's own source. Vite injects each CSS file as a
|
|
4
|
+
// <style data-vite-dev-id="/abs/file.css"> with an inline source map
|
|
5
|
+
// (css.devSourcemap, which the plugin turns on), so rules map back to the
|
|
6
|
+
// original .css/.scss/module file. Tailwind utilities come back as the
|
|
7
|
+
// utility class plus where Tailwind is configured.
|
|
8
|
+
|
|
9
|
+
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
10
|
+
const B64I = Object.fromEntries([...B64].map((c, i) => [c, i]))
|
|
11
|
+
|
|
12
|
+
// Source map "mappings" → per generated line, segments [col, src, line, col].
|
|
13
|
+
function decodeMappings(str) {
|
|
14
|
+
const lines = []
|
|
15
|
+
let src = 0
|
|
16
|
+
let sLine = 0
|
|
17
|
+
let sCol = 0
|
|
18
|
+
for (const lineStr of str.split(";")) {
|
|
19
|
+
const segs = []
|
|
20
|
+
let col = 0
|
|
21
|
+
for (const seg of lineStr.split(",")) {
|
|
22
|
+
if (!seg) continue
|
|
23
|
+
const vals = []
|
|
24
|
+
let v = 0
|
|
25
|
+
let shift = 0
|
|
26
|
+
for (const ch of seg) {
|
|
27
|
+
const d = B64I[ch]
|
|
28
|
+
v += (d & 31) << shift
|
|
29
|
+
if (d & 32) shift += 5
|
|
30
|
+
else {
|
|
31
|
+
vals.push(v & 1 ? -(v >> 1) : v >> 1)
|
|
32
|
+
v = 0
|
|
33
|
+
shift = 0
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
col += vals[0]
|
|
37
|
+
if (vals.length >= 4) {
|
|
38
|
+
src += vals[1]
|
|
39
|
+
sLine += vals[2]
|
|
40
|
+
sCol += vals[3]
|
|
41
|
+
segs.push([col, src, sLine, sCol])
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
lines.push(segs)
|
|
45
|
+
}
|
|
46
|
+
return lines
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const mapCache = new WeakMap() // style element -> { map, lines } | null
|
|
50
|
+
function sourceMapOf(styleEl) {
|
|
51
|
+
if (mapCache.has(styleEl)) return mapCache.get(styleEl)
|
|
52
|
+
let out = null
|
|
53
|
+
try {
|
|
54
|
+
const m = /\/\*# sourceMappingURL=data:application\/json;(?:charset=[^;]+;)?base64,([A-Za-z0-9+/=]+)\s*\*\//.exec(styleEl.textContent || "")
|
|
55
|
+
if (m) {
|
|
56
|
+
const map = JSON.parse(new TextDecoder().decode(fromB64(m[1])))
|
|
57
|
+
out = { map, lines: decodeMappings(map.mappings || "") }
|
|
58
|
+
}
|
|
59
|
+
} catch {}
|
|
60
|
+
mapCache.set(styleEl, out)
|
|
61
|
+
return out
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function walkRules(list, visit) {
|
|
65
|
+
for (const r of list) {
|
|
66
|
+
visit(r)
|
|
67
|
+
if (r.cssRules) walkRules(r.cssRules, visit)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The element part and the pseudo-element part of a selector list entry.
|
|
72
|
+
function splitPseudo(sel) {
|
|
73
|
+
const m = /::?(before|after)\s*$/i.exec(sel)
|
|
74
|
+
return m ? { base: sel.slice(0, m.index) || "*", pseudo: "::" + m[1].toLowerCase() } : { base: sel, pseudo: null }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The full selector of a (possibly nested) style rule.
|
|
78
|
+
function fullSelector(rule) {
|
|
79
|
+
let sel = rule.selectorText
|
|
80
|
+
for (let p = rule.parentRule; p; p = p.parentRule) {
|
|
81
|
+
if (p.selectorText) sel = sel.includes("&") ? sel.replace(/&/g, p.selectorText) : `${p.selectorText} ${sel}`
|
|
82
|
+
}
|
|
83
|
+
return sel
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Last rule (in document order) that matches the element/pseudo and sets the property.
|
|
87
|
+
function findRule(el, pseudo, test) {
|
|
88
|
+
let found = null
|
|
89
|
+
for (const sheet of document.styleSheets) {
|
|
90
|
+
let rules
|
|
91
|
+
try {
|
|
92
|
+
rules = sheet.cssRules
|
|
93
|
+
} catch {
|
|
94
|
+
continue
|
|
95
|
+
}
|
|
96
|
+
walkRules(rules, (r) => {
|
|
97
|
+
if (!r.selectorText || !r.style || !test(r.style)) return
|
|
98
|
+
const sel = fullSelector(r)
|
|
99
|
+
for (const part of sel.split(",")) {
|
|
100
|
+
const { base, pseudo: p } = splitPseudo(part.trim())
|
|
101
|
+
if ((p || null) !== (pseudo || null)) continue
|
|
102
|
+
try {
|
|
103
|
+
if (el.matches(base.replace(/\[data-rt-hover\]/g, ":hover"))) {
|
|
104
|
+
found = { rule: r, sheet, selector: part.trim() }
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
return found
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function findKeyframes(name) {
|
|
115
|
+
let found = null
|
|
116
|
+
for (const sheet of document.styleSheets) {
|
|
117
|
+
let rules
|
|
118
|
+
try {
|
|
119
|
+
rules = sheet.cssRules
|
|
120
|
+
} catch {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
walkRules(rules, (r) => {
|
|
124
|
+
if (typeof CSSKeyframesRule !== "undefined" && r instanceof CSSKeyframesRule && r.name === name) found = { rule: r, sheet, selector: `@keyframes ${name}` }
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
return found
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// file:line of a rule in its <style>, mapped to the original source if a map exists.
|
|
131
|
+
function locate(hit) {
|
|
132
|
+
const owner = hit.sheet.ownerNode
|
|
133
|
+
const devId = owner && owner.getAttribute && owner.getAttribute("data-vite-dev-id")
|
|
134
|
+
const href = hit.sheet.href
|
|
135
|
+
const text = (owner && owner.textContent) || ""
|
|
136
|
+
// Where the rule's header sits in the served CSS (first match).
|
|
137
|
+
const head = hit.rule instanceof CSSKeyframesRule ? `@keyframes ${hit.rule.name}` : hit.rule.selectorText
|
|
138
|
+
let idx = text.indexOf(head)
|
|
139
|
+
if (idx < 0) idx = text.indexOf(head.split(",")[0].trim())
|
|
140
|
+
const out = { file: devId || href || null, line: null, selector: hit.selector }
|
|
141
|
+
if (idx < 0) return out
|
|
142
|
+
const before = text.slice(0, idx)
|
|
143
|
+
const genLine = before.split("\n").length - 1
|
|
144
|
+
const genCol = idx - before.lastIndexOf("\n") - 1
|
|
145
|
+
out.line = genLine + 1
|
|
146
|
+
const sm = owner && sourceMapOf(owner)
|
|
147
|
+
if (sm && sm.lines[genLine]) {
|
|
148
|
+
let seg = null
|
|
149
|
+
for (const s of sm.lines[genLine]) if (s[0] <= genCol) seg = s
|
|
150
|
+
if (!seg) seg = sm.lines[genLine][0]
|
|
151
|
+
if (seg) {
|
|
152
|
+
const srcName = sm.map.sources[seg[1]]
|
|
153
|
+
out.line = seg[2] + 1
|
|
154
|
+
if (srcName) {
|
|
155
|
+
try {
|
|
156
|
+
out.file = srcName.startsWith("/") ? srcName : new URL(srcName, "file://" + (devId || "/")).pathname
|
|
157
|
+
} catch {
|
|
158
|
+
out.file = srcName
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return out
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// A Tailwind utility (e.g. .animate-pulse) from Tailwind's generated CSS?
|
|
167
|
+
function tailwindUtility(hit) {
|
|
168
|
+
const sel = hit.selector.replace(/\\/g, "")
|
|
169
|
+
const m = /^\.([\w:/[\]().%-]+?)(?::[\w-]+)*(?:::?(?:before|after))?$/.exec(sel)
|
|
170
|
+
if (!m) return null
|
|
171
|
+
const text = (hit.sheet.ownerNode && hit.sheet.ownerNode.textContent) || ""
|
|
172
|
+
if (!/tailwindcss|--tw-/.test(text)) return null
|
|
173
|
+
return m[1]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
let tailwindConfig
|
|
177
|
+
function tailwindConfigFile() {
|
|
178
|
+
if (tailwindConfig !== undefined) return Promise.resolve(tailwindConfig)
|
|
179
|
+
return real
|
|
180
|
+
.fetch("/__retake/tailwind")
|
|
181
|
+
.then((r) => (r.ok ? r.json() : {}))
|
|
182
|
+
.then((j) => (tailwindConfig = j.config || null))
|
|
183
|
+
.catch(() => (tailwindConfig = null))
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// What the caller has in hand: an Animation, a clip (from timeline()/clipsFor()), or a clip id.
|
|
187
|
+
function resolveAnimationTarget(x) {
|
|
188
|
+
if (typeof x === "string") x = (rec.clips || []).find((c) => c.id === x) || x
|
|
189
|
+
if (x && typeof Animation !== "undefined" && x instanceof Animation) {
|
|
190
|
+
const kind = clipKind(x)
|
|
191
|
+
const fx = /** @type {KeyframeEffect | null} */ (x.effect)
|
|
192
|
+
const css = /** @type {any} */ (x) // a CSSAnimation or a CSSTransition
|
|
193
|
+
return { el: fx && fx.target, pseudo: (fx && fx.pseudoElement) || null, kind, name: kind === "css-animation" ? css.animationName : null, prop: kind === "transition" ? css.transitionProperty : null }
|
|
194
|
+
}
|
|
195
|
+
if (x && x.path) {
|
|
196
|
+
const el = resolvePath(x.path)
|
|
197
|
+
return { el, pseudo: x.pseudoElement || null, kind: x.kind, name: x.kind === "css-animation" ? x.label : null, prop: x.kind === "transition" ? x.property : null }
|
|
198
|
+
}
|
|
199
|
+
return null
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function cssSourceFor(x) {
|
|
203
|
+
const t = resolveAnimationTarget(x)
|
|
204
|
+
if (!t || !t.el || t.el.nodeType !== 1) return null
|
|
205
|
+
let hit = null
|
|
206
|
+
if (t.kind === "css-animation" && t.name) {
|
|
207
|
+
hit = findRule(t.el, t.pseudo, (st) => (st.animationName || "").split(",").map((s) => s.trim()).includes(t.name))
|
|
208
|
+
} else if (t.kind === "transition" && t.prop) {
|
|
209
|
+
hit = findRule(t.el, t.pseudo, (st) => {
|
|
210
|
+
const props = (st.transitionProperty || "").split(",").map((s) => s.trim())
|
|
211
|
+
return props.includes(t.prop) || props.includes("all") || (!!st.transition && !st.transitionProperty)
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
if (!hit) return null
|
|
215
|
+
const out = locate(hit)
|
|
216
|
+
if (t.kind === "css-animation" && t.name) {
|
|
217
|
+
const kf = findKeyframes(t.name)
|
|
218
|
+
if (kf) out.keyframes = { name: t.name, ...locate(kf) }
|
|
219
|
+
}
|
|
220
|
+
const utility = tailwindUtility(hit)
|
|
221
|
+
if (utility) {
|
|
222
|
+
out.utility = utility
|
|
223
|
+
out.config = await tailwindConfigFile()
|
|
224
|
+
}
|
|
225
|
+
return out
|
|
226
|
+
}
|