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.
Files changed (43) hide show
  1. package/LICENSE +91 -0
  2. package/README.md +198 -0
  3. package/bin/retake.js +319 -0
  4. package/package.json +87 -0
  5. package/src/code-versions.js +357 -0
  6. package/src/core.js +265 -0
  7. package/src/plugin.js +137 -0
  8. package/src/runtime/00-core.js +308 -0
  9. package/src/runtime/10-animations.js +362 -0
  10. package/src/runtime/20-media.js +157 -0
  11. package/src/runtime/30-recorder.js +315 -0
  12. package/src/runtime/32-state.js +315 -0
  13. package/src/runtime/35-network.js +789 -0
  14. package/src/runtime/36-scripts.js +129 -0
  15. package/src/runtime/37-next.js +150 -0
  16. package/src/runtime/38-observers.js +252 -0
  17. package/src/runtime/40-input.js +795 -0
  18. package/src/runtime/45-hover.js +78 -0
  19. package/src/runtime/50-engine.js +341 -0
  20. package/src/runtime/60-preview.js +437 -0
  21. package/src/runtime/65-timeline.js +289 -0
  22. package/src/runtime/66-activity.js +114 -0
  23. package/src/runtime/67-csssource.js +226 -0
  24. package/src/runtime/70-boot.js +460 -0
  25. package/src/server/api.js +285 -0
  26. package/src/server/child.js +174 -0
  27. package/src/server/detect.js +167 -0
  28. package/src/server/front.js +647 -0
  29. package/src/server/mcp.js +332 -0
  30. package/src/shell/00-state.js +85 -0
  31. package/src/shell/05-api.js +136 -0
  32. package/src/shell/10-dock.js +754 -0
  33. package/src/shell/12-checkpoint.js +121 -0
  34. package/src/shell/15-session.js +291 -0
  35. package/src/shell/20-timeline.js +734 -0
  36. package/src/shell/25-input.js +537 -0
  37. package/src/shell/30-notes.js +870 -0
  38. package/src/shell/40-code.js +80 -0
  39. package/src/shell/90-handle.js +15 -0
  40. package/src/shell/shell.css +295 -0
  41. package/src/shell/shell.html +59 -0
  42. package/types/client.d.ts +73 -0
  43. 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
+ }