retake-dev 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +76 -1
- package/README.md +141 -7
- package/bin/retake.js +14 -1
- package/package.json +6 -1
- package/src/core.js +14 -1
- package/src/next-dev.js +183 -0
- package/src/next.js +81 -0
- package/src/note-text.js +844 -0
- package/src/plugin.js +6 -2
- package/src/runtime/10-animations.js +9 -0
- package/src/runtime/60-preview.js +3 -1
- package/src/runtime/62-motion.js +106 -0
- package/src/runtime/65-timeline.js +39 -0
- package/src/runtime/67-csssource.js +73 -1
- package/src/runtime/70-boot.js +9 -2
- package/src/server/api.js +73 -1
- package/src/server/front.js +3 -1
- package/src/server/mcp.js +213 -16
- package/src/server/moments.js +317 -0
- package/src/server/sourcemap.js +192 -0
- package/src/shell/10-dock.js +27 -5
- package/src/shell/15-session.js +11 -5
- package/src/shell/20-timeline.js +30 -5
- package/src/shell/25-input.js +25 -2
- package/src/shell/30-notes.js +704 -110
- package/src/shell/32-anims.js +1353 -0
- package/src/shell/shell.css +24 -0
- package/src/shell/shell.html +1 -0
- package/types/client.d.ts +7 -0
- package/types/index.d.ts +2 -0
- package/types/next.d.ts +50 -0
package/src/shell/30-notes.js
CHANGED
|
@@ -28,16 +28,30 @@ const noClick = (el) => {
|
|
|
28
28
|
return false
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
// The element an event is really about: inside an open shadow root, the
|
|
32
|
+
// element clicked (the event's target is retargeted to the host).
|
|
33
|
+
const innerTarget = (e) => {
|
|
34
|
+
try {
|
|
35
|
+
const p = e.composedPath && e.composedPath()
|
|
36
|
+
if (p && p[0] && p[0].nodeType) return p[0].nodeType === 1 ? p[0] : p[0].parentElement
|
|
37
|
+
} catch {}
|
|
38
|
+
return e.target && (e.target.nodeType === 1 ? e.target : e.target.parentElement)
|
|
39
|
+
}
|
|
31
40
|
shell.inspect = (e) => {
|
|
32
|
-
const el = e
|
|
41
|
+
const el = innerTarget(e)
|
|
33
42
|
if (e.type === "pointermove" || e.type === "pointerover") {
|
|
43
|
+
lastPointer = { x: e.clientX, y: e.clientY }
|
|
34
44
|
buildLayers(e.clientX, e.clientY, usable(el))
|
|
35
45
|
} else if (e.type === "wheel") {
|
|
36
46
|
if (Math.abs(e.deltaY) >= 4) cycleLayer(e.deltaY > 0 ? 1 : -1)
|
|
37
47
|
} else if (e.type === "keydown" && e.key === "Tab") {
|
|
38
48
|
cycleLayer(e.shiftKey ? -1 : 1)
|
|
39
49
|
} else if ((e.type === "click" || (e.type === "pointerup" && noClick(el))) && usable(el)) {
|
|
50
|
+
// The list the highlight showed, unless it was built somewhere else (F90):
|
|
51
|
+
// then the one at the click.
|
|
52
|
+
if (!layersAt || layersAt.x !== e.clientX || layersAt.y !== e.clientY) buildLayers(e.clientX, e.clientY, usable(el))
|
|
40
53
|
const p = pick()
|
|
54
|
+
if (p) p.picked = pickedInfo(p)
|
|
41
55
|
if (mode() === "select") setScope(p ? p.el : usable(el))
|
|
42
56
|
else openComposer(p ? p.el : usable(el), { x: e.clientX, y: e.clientY }, p)
|
|
43
57
|
} else if (e.type === "keydown" && e.key === "Escape") {
|
|
@@ -50,27 +64,43 @@ shell.inspect = (e) => {
|
|
|
50
64
|
shell.appPointerDown = () => {
|
|
51
65
|
if (!card.hidden && !draft) closeCard()
|
|
52
66
|
}
|
|
67
|
+
// ⌘ held over the live app: the note tools work on a paused moment (said
|
|
68
|
+
// once per press).
|
|
69
|
+
let liveHinted = false
|
|
53
70
|
shell.pointer = (x, y, meta) => {
|
|
54
71
|
lastPointer = { x, y }
|
|
55
|
-
if (meta !== undefined && meta !== D.metaHeld)
|
|
72
|
+
if (meta !== undefined && meta !== D.metaHeld) setMeta(meta)
|
|
73
|
+
if (meta && !liveHinted && D.last && D.last.started && D.last.playing && !isStill()) {
|
|
74
|
+
liveHinted = true
|
|
75
|
+
flash("Pause (Space) to comment", { warn: false })
|
|
76
|
+
}
|
|
56
77
|
}
|
|
57
|
-
|
|
78
|
+
// From a frame's runtime: ⌘ down or up, or the window on show lost focus
|
|
79
|
+
// (a frame built behind or being swapped out doesn't say so, F99).
|
|
80
|
+
shell.meta = (down) => setMeta(down)
|
|
81
|
+
function setMeta(down) {
|
|
58
82
|
if (D.metaHeld === down) return
|
|
59
83
|
D.metaHeld = down
|
|
84
|
+
if (!down) liveHinted = false
|
|
60
85
|
syncPicking()
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
// ---- the layer picker: what's under the pointer, including pseudo-elements ----------
|
|
64
|
-
// ⌘ over the app lists the layers at that point
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
89
|
+
// ⌘ over the app lists the layers at that point: each element, and after it
|
|
90
|
+
// any animation running on its ::before/::after (a shimmer on a skeleton,
|
|
91
|
+
// say). What the user can see comes first: an element with its own text,
|
|
92
|
+
// media or a control, or that paints something; then the rest in z-order;
|
|
93
|
+
// then decorative layers (an empty overlay: a glow, a stretched link, a
|
|
94
|
+
// scrim) and invisible ones (opacity 0, a closed menu sheet). Wheel or Tab
|
|
95
|
+
// moves through them; a click picks the highlighted one.
|
|
68
96
|
|
|
69
|
-
let layers = [] // [{ el, pseudo, anim, name, label }]
|
|
97
|
+
let layers = [] // [{ el, pseudo, anim, name, label, rank, why, z }]
|
|
70
98
|
let layerIdx = 0
|
|
71
99
|
let layersKey = ""
|
|
100
|
+
let layersAt = null // the point the list was built at
|
|
72
101
|
const layersEl = $("#wb-layers")
|
|
73
102
|
const pick = () => layers[layerIdx] || null
|
|
103
|
+
const MAX_LAYERS = 8
|
|
74
104
|
|
|
75
105
|
const shortLabel = (el) => {
|
|
76
106
|
const tag = el.tagName.toLowerCase()
|
|
@@ -90,19 +120,128 @@ function animTiming(a) {
|
|
|
90
120
|
}
|
|
91
121
|
const animName = (a) => a.animationName || (a.transitionProperty ? `${a.transitionProperty} transition` : "animation")
|
|
92
122
|
|
|
93
|
-
|
|
123
|
+
const MEDIA = "img, svg, video, canvas, picture, iframe, object, embed"
|
|
124
|
+
const CONTROL = "button, a[href], input, select, textarea, summary, label, [role=button], [role=link], [role=checkbox], [role=switch], [role=tab], [role=menuitem], [contenteditable=true]"
|
|
125
|
+
const isSvg = (el) => !!el && el instanceof el.ownerDocument.defaultView.SVGElement
|
|
126
|
+
const svgRootOf = (el) => (el.tagName.toLowerCase() === "svg" && !el.ownerSVGElement ? el : el.closest("svg:not(svg svg)") || el.ownerSVGElement || el)
|
|
127
|
+
// The control an svg is the icon of: within two levels above it.
|
|
128
|
+
function svgControl(svg) {
|
|
129
|
+
for (let n = svg.parentElement, i = 0; n && i < 2; n = n.parentElement, i++) if (n.matches(CONTROL.replace("a[href]", "a"))) return n
|
|
130
|
+
return null
|
|
131
|
+
}
|
|
132
|
+
const ownText = (el) => [...el.childNodes].some((n) => n.nodeType === 3 && /\S/.test(n.data))
|
|
133
|
+
const textOf = (el) => (el.innerText || el.textContent || "").trim()
|
|
134
|
+
function painted(cs) {
|
|
135
|
+
const bg = cs.backgroundColor
|
|
136
|
+
if (bg && !/rgba\(.*,\s*0\)$/.test(bg) && bg !== "transparent") return true
|
|
137
|
+
if (cs.backgroundImage && cs.backgroundImage !== "none") return true
|
|
138
|
+
if (cs.boxShadow && cs.boxShadow !== "none") return true
|
|
139
|
+
return bordered(cs)
|
|
140
|
+
}
|
|
141
|
+
const bordered = (cs) => ["Top", "Right", "Bottom", "Left"].some((s) => parseFloat(cs[`border${s}Width`]) > 0 && cs[`border${s}Style`] !== "none" && cs[`border${s}Style`] !== "hidden")
|
|
142
|
+
// Is it on screen at all (opacity 0 on it or an ancestor, visibility hidden)?
|
|
143
|
+
function seen(el) {
|
|
144
|
+
try {
|
|
145
|
+
if (typeof el.checkVisibility === "function") return el.checkVisibility({ opacityProperty: true, visibilityProperty: true })
|
|
146
|
+
for (let n = el; n && n.nodeType === 1; n = n.parentElement) if (el.ownerDocument.defaultView.getComputedStyle(n).opacity === "0") return false
|
|
147
|
+
} catch {}
|
|
148
|
+
return true
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Everything at the point, topmost first: inside open shadow roots (before
|
|
152
|
+
// their host), and embedded iframes (out of hit testing while the app is
|
|
153
|
+
// view-only: no click reaches them) on top.
|
|
154
|
+
function hitList(doc, x, y) {
|
|
94
155
|
let els = []
|
|
95
156
|
try {
|
|
96
|
-
els =
|
|
157
|
+
els = doc.elementsFromPoint(x, y)
|
|
97
158
|
} catch {}
|
|
98
|
-
const seen = new Set()
|
|
99
159
|
const out = []
|
|
100
|
-
|
|
101
|
-
const el
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
160
|
+
const walk = (list, depth) => {
|
|
161
|
+
for (const el of list) {
|
|
162
|
+
const root = el.shadowRoot
|
|
163
|
+
if (root && depth < 3) {
|
|
164
|
+
let inner = []
|
|
165
|
+
try {
|
|
166
|
+
inner = root.elementsFromPoint(x, y).filter((n) => n.getRootNode() === root)
|
|
167
|
+
} catch {}
|
|
168
|
+
walk(inner, depth + 1)
|
|
169
|
+
}
|
|
170
|
+
out.push(el)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
walk(els, 0)
|
|
174
|
+
try {
|
|
175
|
+
for (const f of doc.querySelectorAll("iframe")) {
|
|
176
|
+
const r = f.getBoundingClientRect()
|
|
177
|
+
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom && r.width && r.height && !out.includes(f)) out.unshift(f)
|
|
178
|
+
}
|
|
179
|
+
} catch {}
|
|
180
|
+
return out
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// The elements an element at the point stands for: itself; for a shape inside
|
|
184
|
+
// an svg, the shape (stroke animations live there), the whole <svg>, and the
|
|
185
|
+
// control the svg is the icon of.
|
|
186
|
+
function candidates(raw) {
|
|
187
|
+
if (!raw || raw.tagName === "HTML" || raw.tagName === "BODY") return []
|
|
188
|
+
if (!isSvg(raw)) return [raw]
|
|
189
|
+
const svg = svgRootOf(raw)
|
|
190
|
+
const list = raw === svg ? [svg] : [raw, svg]
|
|
191
|
+
const control = svgControl(svg)
|
|
192
|
+
if (control) list.push(control)
|
|
193
|
+
return list
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// 0 content (text, media, a control, or it paints something), 1 an empty box,
|
|
197
|
+
// 2 decorative, 3 invisible.
|
|
198
|
+
function rankOf(el, below) {
|
|
199
|
+
if (el.tagName === "IFRAME") return { rank: 0 }
|
|
200
|
+
if (!seen(el)) return { rank: 3, why: "hidden" }
|
|
201
|
+
let cs = null
|
|
202
|
+
try {
|
|
203
|
+
cs = el.ownerDocument.defaultView.getComputedStyle(el)
|
|
204
|
+
} catch {}
|
|
205
|
+
const media = isSvg(el) || el.matches(MEDIA) || !!el.querySelector(MEDIA)
|
|
206
|
+
const text = !!textOf(el)
|
|
207
|
+
if (!media && !text && el.getAttribute("aria-hidden") === "true") return { rank: 2, why: "decorative" }
|
|
208
|
+
if (!media && !text && cs && (cs.position === "absolute" || cs.position === "fixed") && !bordered(cs)) {
|
|
209
|
+
const r = el.getBoundingClientRect()
|
|
210
|
+
const W = el.ownerDocument.defaultView
|
|
211
|
+
const big = r.width * r.height > 0.5 * W.innerWidth * W.innerHeight
|
|
212
|
+
const covers = below.some((b) => {
|
|
213
|
+
const q = b.getBoundingClientRect()
|
|
214
|
+
return q.left >= r.left - 1 && q.top >= r.top - 1 && q.right <= r.right + 1 && q.bottom <= r.bottom + 1
|
|
215
|
+
})
|
|
216
|
+
if (big || covers) return { rank: 2, why: "decorative" }
|
|
217
|
+
}
|
|
218
|
+
if (text || media || el.matches(CONTROL) || (cs && painted(cs))) return { rank: 0 }
|
|
219
|
+
return { rank: 1 }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildLayers(x, y, fallback) {
|
|
223
|
+
let doc = null
|
|
224
|
+
try {
|
|
225
|
+
doc = D.frame.contentDocument
|
|
226
|
+
} catch {}
|
|
227
|
+
const els = []
|
|
228
|
+
const seenEls = new Set()
|
|
229
|
+
for (const raw of doc ? hitList(doc, x, y) : []) {
|
|
230
|
+
for (const el of candidates(raw)) {
|
|
231
|
+
if (seenEls.has(el)) continue
|
|
232
|
+
seenEls.add(el)
|
|
233
|
+
els.push(el)
|
|
234
|
+
}
|
|
235
|
+
if (els.length >= MAX_LAYERS) break
|
|
236
|
+
}
|
|
237
|
+
els.length = Math.min(els.length, MAX_LAYERS)
|
|
238
|
+
// Each element's rank, knowing what's under it (an empty overlay that
|
|
239
|
+
// covers the content under it is decorative).
|
|
240
|
+
const groups = els.map((el, z) => {
|
|
241
|
+
const below = els.slice(z + 1).filter((b) => b.tagName !== "IFRAME" && (ownText(b) || b.matches(MEDIA)) && !b.contains(el))
|
|
242
|
+
const { rank, why } = rankOf(el, below)
|
|
243
|
+
const host = { el, pseudo: null, anim: null, name: null, label: layerLabel(el), rank, why: why || null, z }
|
|
244
|
+
const pseudos = []
|
|
106
245
|
let anims = []
|
|
107
246
|
try {
|
|
108
247
|
anims = el.getAnimations({ subtree: true })
|
|
@@ -114,33 +253,67 @@ function buildLayers(x, y, fallback) {
|
|
|
114
253
|
const name = animName(a)
|
|
115
254
|
if (names.has(pe + name)) continue
|
|
116
255
|
names.add(pe + name)
|
|
117
|
-
|
|
256
|
+
pseudos.push({ el, pseudo: pe, anim: a, name, label: `${pe} · ${name} · ${animTiming(a)}`, rank, why: null, z })
|
|
118
257
|
}
|
|
119
|
-
|
|
120
|
-
|
|
258
|
+
return [host, ...pseudos]
|
|
259
|
+
})
|
|
260
|
+
const order = groups.map((g, i) => ({ g, i })).sort((a, b) => a.g[0].rank - b.g[0].rank || a.i - b.i)
|
|
261
|
+
const out = order.flatMap((o) => o.g)
|
|
262
|
+
if (!out.length && fallback) out.push({ el: fallback, pseudo: null, anim: null, name: null, label: layerLabel(fallback), rank: 0, why: null, z: 0 })
|
|
121
263
|
const key = out.map((l) => l.label).join("|")
|
|
122
264
|
if (key !== layersKey) {
|
|
123
265
|
layersKey = key
|
|
124
|
-
layerIdx =
|
|
266
|
+
layerIdx = defaultLayer(out)
|
|
125
267
|
}
|
|
126
268
|
layers = out
|
|
269
|
+
layersAt = { x, y }
|
|
127
270
|
hovered = pick() ? pick().el : null
|
|
128
271
|
}
|
|
129
272
|
|
|
273
|
+
// The first content layer; for a shape in an svg, the svg itself, or the
|
|
274
|
+
// control it's the icon of.
|
|
275
|
+
function defaultLayer(list) {
|
|
276
|
+
let i = list.findIndex((l) => l.rank === 0)
|
|
277
|
+
if (i < 0) i = list.findIndex((l) => l.rank < 2)
|
|
278
|
+
if (i < 0) return 0
|
|
279
|
+
const l = list[i]
|
|
280
|
+
if (isSvg(l.el)) {
|
|
281
|
+
const svg = svgRootOf(l.el)
|
|
282
|
+
const control = svgControl(svg)
|
|
283
|
+
const want = control || svg
|
|
284
|
+
const j = list.findIndex((x) => x.el === want && !x.pseudo)
|
|
285
|
+
if (j >= 0) return j
|
|
286
|
+
}
|
|
287
|
+
return i
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// "button.inner in <fancy-card>'s shadow root"
|
|
291
|
+
function layerLabel(el) {
|
|
292
|
+
const root = el.getRootNode && el.getRootNode()
|
|
293
|
+
const host = root && root.host
|
|
294
|
+
return host ? `${shortLabel(el)} in <${host.tagName.toLowerCase()}>'s shadow root` : shortLabel(el)
|
|
295
|
+
}
|
|
296
|
+
|
|
130
297
|
function cycleLayer(dir) {
|
|
131
298
|
if (layers.length < 2) return
|
|
132
299
|
layerIdx = (layerIdx + dir + layers.length) % layers.length
|
|
133
300
|
hovered = pick().el
|
|
134
301
|
}
|
|
135
302
|
|
|
136
|
-
// The chip beside the pointer: the top layers, the pick highlighted
|
|
303
|
+
// The chip beside the pointer: the top layers, the pick highlighted, a
|
|
304
|
+
// layer that's skipped dimmed with why (so it's clear the wheel reaches it).
|
|
137
305
|
let layersHtml = ""
|
|
138
306
|
function renderLayers() {
|
|
139
307
|
const show = mode() === "comment" && layers.length > 0 && lastPointer
|
|
140
308
|
layersEl.hidden = !show
|
|
141
309
|
if (!show) return
|
|
142
|
-
const
|
|
143
|
-
const
|
|
310
|
+
const shown = layers.map((l, i) => ({ l, i })).filter((x) => x.l.rank < 2 || x.i === layerIdx)
|
|
311
|
+
const skipped = layers.map((l, i) => ({ l, i })).filter((x) => x.l.rank >= 2 && x.i !== layerIdx)
|
|
312
|
+
const top = shown.slice(0, skipped.length ? 3 : 4).concat(skipped.slice(0, 1))
|
|
313
|
+
const rest = layers.length - top.length
|
|
314
|
+
const html =
|
|
315
|
+
top.map(({ l, i }) => `<div class="layer${i === layerIdx ? " on" : ""}${l.pseudo ? " pseudo" : ""}${l.rank >= 2 ? " skipped" : ""}">${esc(l.label)}${l.rank >= 2 && l.why ? ` · ${esc(l.why)}` : ""}</div>`).join("") +
|
|
316
|
+
(rest > 0 ? `<div class="more">+${rest}</div>` : "")
|
|
144
317
|
if (html !== layersHtml) {
|
|
145
318
|
layersHtml = html
|
|
146
319
|
layersEl.innerHTML = html
|
|
@@ -152,15 +325,22 @@ function renderLayers() {
|
|
|
152
325
|
layersEl.style.top = clamp(f.top + lastPointer.y + 16, 8, dock.getBoundingClientRect().top - h - 8) + "px"
|
|
153
326
|
}
|
|
154
327
|
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
|
|
328
|
+
// How the pick was made, for the note: on top, or a layer down, and what was
|
|
329
|
+
// skipped above it.
|
|
330
|
+
function pickedInfo(p) {
|
|
331
|
+
if (!p) return { via: "topmost", skipped: [] }
|
|
332
|
+
const above = layers.filter((l) => !l.pseudo && l.z < p.z)
|
|
333
|
+
return { via: p.z === 0 ? "topmost" : `layer ${p.z + 1}`, skipped: above.map((l) => `${l.label}${l.why ? ` (${l.why})` : ""}`) }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// The page itself isn't a thing to comment on. Inside an SVG icon, the thing
|
|
337
|
+
// you mean is the icon's control (a button or link, within two levels), or
|
|
338
|
+
// at least the whole <svg>.
|
|
158
339
|
function usable(el) {
|
|
159
340
|
if (!el || el.tagName === "HTML" || el.tagName === "BODY") return null
|
|
160
|
-
if (el
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
return control && control.tagName !== "BODY" ? control : svgRoot
|
|
341
|
+
if (isSvg(el)) {
|
|
342
|
+
const svg = svgRootOf(el)
|
|
343
|
+
return svgControl(svg) || svg
|
|
164
344
|
}
|
|
165
345
|
return el
|
|
166
346
|
}
|
|
@@ -172,14 +352,47 @@ function setPicking(m) {
|
|
|
172
352
|
syncPicking()
|
|
173
353
|
}
|
|
174
354
|
|
|
355
|
+
// The layer list is always the one at the pointer (F90): rebuilt when a tool
|
|
356
|
+
// turns on, dropped when it turns off.
|
|
175
357
|
function syncPicking() {
|
|
176
358
|
shell.inspecting = !!mode()
|
|
177
359
|
setToolActive(!!mode())
|
|
360
|
+
// Picking, a click on a pinned spot is a new note there (F108): pins let it through.
|
|
361
|
+
document.body.classList.toggle("picking-notes", mode() === "comment")
|
|
178
362
|
hovered = null
|
|
179
363
|
try {
|
|
180
364
|
const doc = D.frame.contentDocument
|
|
181
365
|
doc.documentElement.style.cursor = mode() ? "crosshair" : ""
|
|
182
|
-
|
|
366
|
+
} catch {}
|
|
367
|
+
if (mode() && lastPointer) buildLayers(lastPointer.x, lastPointer.y, null)
|
|
368
|
+
else if (!mode()) {
|
|
369
|
+
layers = []
|
|
370
|
+
layersKey = ""
|
|
371
|
+
layersAt = null
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Embedded iframes get no input while the app is view-only (F97): a sheet
|
|
376
|
+
// adopted by the frame on show (no DOM change, nothing recorded).
|
|
377
|
+
let embedSheet = null // { doc, sheet }
|
|
378
|
+
function syncEmbeds() {
|
|
379
|
+
let doc = null
|
|
380
|
+
try {
|
|
381
|
+
doc = D.frame && D.frame.contentDocument
|
|
382
|
+
} catch {}
|
|
383
|
+
const want = !!doc && isStill()
|
|
384
|
+
if (embedSheet && (embedSheet.doc !== doc || !want)) {
|
|
385
|
+
try {
|
|
386
|
+
embedSheet.doc.adoptedStyleSheets = embedSheet.doc.adoptedStyleSheets.filter((x) => x !== embedSheet.sheet)
|
|
387
|
+
} catch {}
|
|
388
|
+
embedSheet = null
|
|
389
|
+
}
|
|
390
|
+
if (!want || embedSheet) return
|
|
391
|
+
try {
|
|
392
|
+
const sheet = new doc.defaultView.CSSStyleSheet()
|
|
393
|
+
sheet.replaceSync("iframe { pointer-events: none !important; }")
|
|
394
|
+
doc.adoptedStyleSheets = [...doc.adoptedStyleSheets, sheet]
|
|
395
|
+
embedSheet = { doc, sheet }
|
|
183
396
|
} catch {}
|
|
184
397
|
}
|
|
185
398
|
|
|
@@ -193,8 +406,8 @@ function setScope(el) {
|
|
|
193
406
|
|
|
194
407
|
const cssEsc = (v) => (window.CSS && CSS.escape ? CSS.escape(v) : String(v).replace(/[^\w-]/g, (c) => "\\" + c))
|
|
195
408
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
409
|
+
// The runtime's own selector for an element (the old way: an id, else
|
|
410
|
+
// tag.class chains with :nth-of-type), which older clips carry.
|
|
198
411
|
function cssPath(el) {
|
|
199
412
|
const parts = []
|
|
200
413
|
for (let n = el; n && n.nodeType === 1 && n.tagName !== "HTML"; n = n.parentElement) {
|
|
@@ -213,9 +426,150 @@ function cssPath(el) {
|
|
|
213
426
|
return parts.join(" > ")
|
|
214
427
|
}
|
|
215
428
|
|
|
429
|
+
// ---- stable selectors (F96) ----------------------------------------------------------
|
|
430
|
+
// A selector that still finds the element on a fresh load of the page: no
|
|
431
|
+
// generated ids, no classes the recording saw come and go (`in`, `go`,
|
|
432
|
+
// `is-open`, `opacity-100`), no Tailwind state variants; test ids, labels,
|
|
433
|
+
// hrefs and names where they're unique; :nth-of-type only where needed.
|
|
434
|
+
|
|
435
|
+
// Class names added or removed on any element while the dock watched (every
|
|
436
|
+
// frame it has shown or built: the recording, its replays and previews).
|
|
437
|
+
const toggledClasses = new Set()
|
|
438
|
+
const watchedDocs = new WeakSet()
|
|
439
|
+
const classSet = (v) => new Set(String(v || "").split(/\s+/).filter(Boolean))
|
|
440
|
+
function watchClasses(frame) {
|
|
441
|
+
let doc = null
|
|
442
|
+
try {
|
|
443
|
+
doc = frame && frame.contentDocument
|
|
444
|
+
} catch {}
|
|
445
|
+
if (!doc || !doc.documentElement || watchedDocs.has(doc)) return
|
|
446
|
+
watchedDocs.add(doc)
|
|
447
|
+
try {
|
|
448
|
+
new MutationObserver((records) => {
|
|
449
|
+
for (const r of records) {
|
|
450
|
+
const was = classSet(r.oldValue)
|
|
451
|
+
const now = classSet(/** @type {Element} */ (r.target).getAttribute("class"))
|
|
452
|
+
for (const c of was) if (!now.has(c)) toggledClasses.add(c)
|
|
453
|
+
for (const c of now) if (!was.has(c)) toggledClasses.add(c)
|
|
454
|
+
}
|
|
455
|
+
}).observe(doc, { subtree: true, attributes: true, attributeFilter: ["class"], attributeOldValue: true })
|
|
456
|
+
} catch {}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const GENERATED_ID = /^(:r|«r|:R|radix-|headlessui-|react-aria|mui-|rc-|ember\d)|\d{5,}$|:/
|
|
460
|
+
const goodId = (id) => !!id && !GENERATED_ID.test(id)
|
|
461
|
+
const stableClass = (c) => !toggledClasses.has(c) && !c.includes(":") && /^-?[_a-zA-Z][\w-]*$|^[\w-]*\[[^\]]+\][\w-]*$/.test(c)
|
|
462
|
+
// "_title_18lyj_1" (a CSS module's hashed class) → "title".
|
|
463
|
+
const moduleClass = (c) => {
|
|
464
|
+
const m = /^_?([A-Za-z][\w-]*?)_[a-z0-9]{5}(_\d+)?$/.exec(c)
|
|
465
|
+
return m ? m[1] : null
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const attrEsc = (v) => String(v).replace(/["\\]/g, "\\$&")
|
|
469
|
+
// One step of a selector for n, the most specific stable thing it has.
|
|
470
|
+
function selectorStep(n, unique) {
|
|
471
|
+
const tag = n.tagName.toLowerCase()
|
|
472
|
+
if (goodId(n.id)) return { sel: `#${cssEsc(n.id)}`, anchor: true }
|
|
473
|
+
for (const a of ["data-testid", "data-test", "data-cy"]) {
|
|
474
|
+
const v = n.getAttribute(a)
|
|
475
|
+
if (v) return { sel: `${tag}[${a}="${attrEsc(v)}"]`, anchor: true }
|
|
476
|
+
}
|
|
477
|
+
const tries = []
|
|
478
|
+
const label = n.getAttribute("aria-label")
|
|
479
|
+
if (label) tries.push(`${tag}[aria-label="${attrEsc(label)}"]`)
|
|
480
|
+
if (tag === "a" && n.getAttribute("href")) tries.push(`a[href="${attrEsc(n.getAttribute("href"))}"]`)
|
|
481
|
+
if (/^(input|select|textarea|button)$/.test(tag) && n.getAttribute("name")) tries.push(`${tag}[name="${attrEsc(n.getAttribute("name"))}"]`)
|
|
482
|
+
for (const t of tries) if (unique(t)) return { sel: t, anchor: true }
|
|
483
|
+
const cls = [...n.classList].filter(stableClass).slice(0, 3)
|
|
484
|
+
return { sel: tag + cls.map((c) => "." + cssEsc(c)).join(""), anchor: false, bare: !cls.length }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// { selector, matches, hint } for an element. In a shadow root: the host's
|
|
488
|
+
// selector, " >>> ", and the selector inside it.
|
|
489
|
+
function stableSelector(el) {
|
|
490
|
+
const root = el.getRootNode ? el.getRootNode() : el.ownerDocument
|
|
491
|
+
const all = (s) => {
|
|
492
|
+
try {
|
|
493
|
+
return root.querySelectorAll(s)
|
|
494
|
+
} catch {
|
|
495
|
+
return []
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
const unique = (s) => {
|
|
499
|
+
const m = all(s)
|
|
500
|
+
return m.length === 1 && m[0] === el
|
|
501
|
+
}
|
|
502
|
+
let sel = ""
|
|
503
|
+
for (let n = el, depth = 0; n && n.nodeType === 1 && n.tagName !== "HTML" && depth < 10; n = n.parentElement, depth++) {
|
|
504
|
+
const step = selectorStep(n, (s) => all(sel ? `${s} > ${sel}` : s).length === 1)
|
|
505
|
+
let part = step.sel
|
|
506
|
+
let cand = sel ? `${part} > ${sel}` : part
|
|
507
|
+
if (unique(cand) && (!step.bare || depth > 0)) return finish(cand)
|
|
508
|
+
if (!step.anchor && n.parentElement) {
|
|
509
|
+
const same = [...n.parentElement.children].filter((c) => c.tagName === n.tagName)
|
|
510
|
+
if (same.length > 1) {
|
|
511
|
+
const nth = `${part}:nth-of-type(${same.indexOf(n) + 1})`
|
|
512
|
+
const withNth = sel ? `${nth} > ${sel}` : nth
|
|
513
|
+
if (unique(withNth) && (!step.bare || depth > 0)) return finish(withNth)
|
|
514
|
+
// Kept only where it narrows things down.
|
|
515
|
+
if (all(withNth).length < all(cand).length) cand = withNth
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
sel = cand
|
|
519
|
+
if (step.anchor || n.tagName === "BODY") break
|
|
520
|
+
}
|
|
521
|
+
return finish(sel || el.tagName.toLowerCase())
|
|
522
|
+
function finish(s) {
|
|
523
|
+
const host = root && /** @type {any} */ (root).host
|
|
524
|
+
if (host) {
|
|
525
|
+
const outer = stableSelector(host)
|
|
526
|
+
return { selector: `${outer.selector} >>> ${s}`, matches: all(s).length, shadowHost: outer.selector }
|
|
527
|
+
}
|
|
528
|
+
return { selector: s, matches: all(s).length }
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// The runtime's selectorOf (shared with clips) when it has one.
|
|
533
|
+
function selectorInfo(el) {
|
|
534
|
+
const pt = D.PT
|
|
535
|
+
const inShadow = el.getRootNode && el.getRootNode() !== el.ownerDocument
|
|
536
|
+
if (pt && typeof pt.selectorOf === "function" && !inShadow) {
|
|
537
|
+
try {
|
|
538
|
+
const r = pt.selectorOf(el)
|
|
539
|
+
if (r && typeof r === "object" && r.selector) return { selector: r.selector, matches: r.matches || 1, hint: r.hint || null }
|
|
540
|
+
if (typeof r === "string" && r) return { selector: r, matches: findAll(r, el.ownerDocument).length || 1 }
|
|
541
|
+
} catch {}
|
|
542
|
+
}
|
|
543
|
+
return stableSelector(el)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Elements a selector finds, through " >>> " into open shadow roots.
|
|
547
|
+
function findAll(selector, doc) {
|
|
548
|
+
try {
|
|
549
|
+
const parts = String(selector).split(" >>> ")
|
|
550
|
+
let scope = [doc]
|
|
551
|
+
for (let i = 0; i < parts.length; i++) {
|
|
552
|
+
const found = scope.flatMap((r) => [...r.querySelectorAll(parts[i])])
|
|
553
|
+
scope = i < parts.length - 1 ? found.map((n) => n.shadowRoot).filter(Boolean) : found
|
|
554
|
+
}
|
|
555
|
+
return /** @type {Element[]} */ (scope)
|
|
556
|
+
} catch {
|
|
557
|
+
return []
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const findEl = (selector, doc) => (selector ? findAll(selector, doc)[0] || null : null)
|
|
561
|
+
|
|
562
|
+
// The element's fiber in the tree on screen: the node keeps the fiber it was
|
|
563
|
+
// created with, which after an update may be the stale one of the pair (its
|
|
564
|
+
// props, and its parents' props, from an earlier render). The current props
|
|
565
|
+
// sit on the node (__reactProps$), so the fiber holding them is the current one.
|
|
216
566
|
const fiberOf = (el) => {
|
|
217
|
-
const
|
|
218
|
-
|
|
567
|
+
const keys = Object.keys(el)
|
|
568
|
+
const key = keys.find((k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"))
|
|
569
|
+
const f = key ? el[key] : null
|
|
570
|
+
const pk = keys.find((k) => k.startsWith("__reactProps$"))
|
|
571
|
+
if (f && pk && f.alternate && f.memoizedProps !== el[pk] && f.alternate.memoizedProps === el[pk]) return f.alternate
|
|
572
|
+
return f
|
|
219
573
|
}
|
|
220
574
|
|
|
221
575
|
// A component's name, through memo() and forwardRef() wrappers.
|
|
@@ -231,7 +585,7 @@ function componentName(type) {
|
|
|
231
585
|
|
|
232
586
|
// Library wrappers that sit between an element and the component that wrote
|
|
233
587
|
// it (framer-motion presence, Radix slots, providers). Not what a note means.
|
|
234
|
-
const INTERNAL = /^(PopChild|PopChildMeasure|PresenceChild|AnimatePresence|MotionComponent|MotionDOMComponent|LayoutGroup|LazyMotion|MotionConfig|Slot|SlotClone|Slottable|Primitive\b.*|Presence|Portal|DismissableLayer|FocusScope|RemoveScroll|Suspense|StrictMode|Fragment|.*Provider|.*Consumer|.*Context)$/
|
|
588
|
+
const INTERNAL = /^(SegmentViewNode|InnerLayoutRouter|OuterLayoutRouter|RenderFromTemplateContext|ScrollAndFocusHandler|InnerScrollAndFocusHandler|RedirectBoundary|RedirectErrorBoundary|NotFoundBoundary|HTTPAccessFallbackBoundary|HTTPAccessFallbackErrorBoundary|ErrorBoundary|ErrorBoundaryHandler|LoadingBoundary|ClientPageRoot|ClientSegmentRoot|AppRouter|ServerRoot|HotReload|DevRootHTTPAccessFallbackBoundary|Router|PopChild|PopChildMeasure|PresenceChild|AnimatePresence|MotionComponent|MotionDOMComponent|LayoutGroup|LazyMotion|MotionConfig|Slot|SlotClone|Slottable|Primitive\b.*|Presence|Portal|DismissableLayer|FocusScope|RemoveScroll|Suspense|StrictMode|Fragment|.*Provider|.*Consumer|.*Context)$/
|
|
235
589
|
|
|
236
590
|
// Nearest components, innermost first: the components that rendered the
|
|
237
591
|
// element (React's owner chain), which skips wrappers it was merely passed
|
|
@@ -247,44 +601,104 @@ function reactComponents(el) {
|
|
|
247
601
|
|
|
248
602
|
// Where the element was written: React's debug info. React 18 and older keep
|
|
249
603
|
// _debugSource (already source lines). React 19 keeps a stack (_debugStack)
|
|
250
|
-
// whose
|
|
251
|
-
// (types stripped, JSX compiled
|
|
252
|
-
//
|
|
604
|
+
// whose app frames lead to the JSX; those are lines of the file as served
|
|
605
|
+
// (types stripped, JSX compiled; or a whole bundle, as Next's Turbopack and
|
|
606
|
+
// webpack chunks are), which mapSource() turns back into source lines with
|
|
607
|
+
// the served file's source map. Frames that map into library code (the JSX
|
|
608
|
+
// runtime bundled into the same chunk) are skipped. With no map at all the
|
|
609
|
+
// source stays unmapped (mapped: false) and says it's a compiled bundle.
|
|
610
|
+
// (Vite's pre-bundled deps carry a ?v= hash wherever its cacheDir is.)
|
|
611
|
+
const LIB_URL = /node_modules|\/\.vite\/deps\/|\/deps\/[^/?]+\.js\?v=[0-9a-f]+|react-dom|react\.development|jsx-dev-runtime/
|
|
612
|
+
const LIB_FN = /\bat (?:exports\.|Object\.)?(?:jsxDEV|jsxs?|createElement|jsxWithValidation\w*)\b/
|
|
253
613
|
function sourceOf(el) {
|
|
254
614
|
for (let f = fiberOf(el), i = 0; f && i < 12; f = f.return, i++) {
|
|
615
|
+
// The element's own JSX (exact), or a parent's, where it was passed in (owner).
|
|
616
|
+
const confidence = i === 0 ? "exact" : "owner"
|
|
255
617
|
const src = f._debugSource
|
|
256
|
-
if (src && src.fileName) return { file: shortPath(src.fileName), line: src.lineNumber || null, mapped: true }
|
|
618
|
+
if (src && src.fileName) return { file: shortPath(src.fileName), line: src.lineNumber || null, mapped: true, confidence }
|
|
257
619
|
const stack = f._debugStack && (f._debugStack.stack || String(f._debugStack))
|
|
258
620
|
if (stack) {
|
|
621
|
+
const frames = []
|
|
622
|
+
let server = null
|
|
259
623
|
for (const line of stack.split("\n").slice(1)) {
|
|
260
624
|
const m = line.match(/(\S+?):(\d+):(\d+)\)?\s*$/)
|
|
261
|
-
if (!m ||
|
|
625
|
+
if (!m || LIB_FN.test(line)) continue
|
|
262
626
|
const url = m[1].replace(/^.*?\(/, "")
|
|
627
|
+
if (LIB_URL.test(url)) continue
|
|
628
|
+
// webpack's eval'd modules (Next on webpack): the module's own path, its line is the compiled one.
|
|
629
|
+
if (/^webpack-internal:/.test(url)) {
|
|
630
|
+
frames.push({ file: cleanSource(url.replace(/^webpack-internal:\/\/\/(\([^)]*\)\/)?/, "")), line: null })
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
263
633
|
// A server component's frame (React 19 dev: about://React/Server/file:///…/.next/…chunk.js)
|
|
264
|
-
// is a compiled chunk on the server:
|
|
265
|
-
if (!/^https?:\/\//.test(url))
|
|
266
|
-
|
|
267
|
-
|
|
634
|
+
// is a compiled chunk on the server: the dev server maps it (F95, /__retake/map).
|
|
635
|
+
if (!/^https?:\/\//.test(url)) {
|
|
636
|
+
if (!server) server = { chunk: url.replace(/^about:\/\/React\/Server\//, ""), line: Number(m[2]), col: Number(m[3]) }
|
|
637
|
+
continue
|
|
638
|
+
}
|
|
639
|
+
frames.push({ url, line: Number(m[2]), col: Number(m[3]) })
|
|
640
|
+
if (frames.length >= 8) break
|
|
641
|
+
}
|
|
642
|
+
const first = frames[0]
|
|
643
|
+
if (first && !first.url) return { file: first.file, line: null, mapped: true, confidence }
|
|
644
|
+
if (first) {
|
|
645
|
+
const src = { file: shortPath(first.url), line: first.line, col: first.col, url: first.url, mapped: false, confidence }
|
|
646
|
+
mapSource(src, frames)
|
|
647
|
+
return src
|
|
648
|
+
}
|
|
649
|
+
// Written by a server component: its line, never the client parent's.
|
|
650
|
+
if (server && i === 0) {
|
|
651
|
+
const src = { file: null, line: null, server: true, chunk: server.chunk, col: server.col, chunkLine: server.line, confidence: "unknown" }
|
|
652
|
+
mapServerSource(src)
|
|
268
653
|
return src
|
|
269
654
|
}
|
|
270
655
|
}
|
|
271
656
|
}
|
|
272
657
|
return null
|
|
273
658
|
}
|
|
659
|
+
|
|
660
|
+
// The dev server reads the chunk's source map from disk: fills in the file
|
|
661
|
+
// and line of a server component's element, in place.
|
|
662
|
+
async function mapServerSource(src) {
|
|
663
|
+
if (!net.on) return
|
|
664
|
+
try {
|
|
665
|
+
const q = new URLSearchParams({ url: src.chunk, line: String(src.chunkLine), col: String(src.col || 0) })
|
|
666
|
+
const r = await api("GET", `map?${q}`)
|
|
667
|
+
const v = r && r.value && typeof r.value === "object" ? r.value : null
|
|
668
|
+
if (v && v.file) {
|
|
669
|
+
src.file = v.file
|
|
670
|
+
src.line = v.line || null
|
|
671
|
+
src.mapped = true
|
|
672
|
+
src.confidence = "exact"
|
|
673
|
+
}
|
|
674
|
+
} catch {}
|
|
675
|
+
}
|
|
274
676
|
function shortPath(p) {
|
|
275
677
|
try {
|
|
276
|
-
|
|
678
|
+
// A dev server's URL path is from the project's root (Vite's /@fs/ is an absolute path).
|
|
679
|
+
if (/^https?:/.test(p)) {
|
|
680
|
+
p = new URL(p).pathname
|
|
681
|
+
if (!p.startsWith("/@fs/")) return p.replace(/^\/+/, "")
|
|
682
|
+
}
|
|
277
683
|
} catch {}
|
|
278
|
-
return p.replace(/\?.*$/, "").replace(/^\/@fs/, "")
|
|
684
|
+
return projectPath(p.replace(/\?.*$/, "").replace(/^\/@fs/, ""))
|
|
685
|
+
}
|
|
686
|
+
// An absolute path inside the project, relative to it (macOS's /var is /private/var).
|
|
687
|
+
function projectPath(p) {
|
|
688
|
+
const root = String((window.__retakeConfig && window.__retakeConfig.root) || "").replace(/\/+$/, "")
|
|
689
|
+
if (!root || !p || p[0] !== "/") return p
|
|
690
|
+
for (const r of new Set([root, root.replace(/^\/private\//, "/"), root.startsWith("/private/") ? root : "/private" + root])) if (p.startsWith(r + "/")) return p.slice(r.length + 1)
|
|
691
|
+
return p
|
|
279
692
|
}
|
|
280
693
|
|
|
281
694
|
// ---- source maps: served line → source line ----------------------------------------
|
|
695
|
+
// (server/sourcemap.js does the same for the MCP server.)
|
|
282
696
|
|
|
283
697
|
const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
284
698
|
function decodeMappings(mappings) {
|
|
285
699
|
const lines = []
|
|
286
700
|
let src = 0, oLine = 0, oCol = 0
|
|
287
|
-
for (const text of mappings.split(";")) {
|
|
701
|
+
for (const text of String(mappings || "").split(";")) {
|
|
288
702
|
const segs = []
|
|
289
703
|
let gCol = 0
|
|
290
704
|
for (const seg of text.split(",")) {
|
|
@@ -292,6 +706,7 @@ function decodeMappings(mappings) {
|
|
|
292
706
|
const v = []
|
|
293
707
|
for (let i = 0, shift = 0, acc = 0; i < seg.length; i++) {
|
|
294
708
|
const d = B64.indexOf(seg[i])
|
|
709
|
+
if (d < 0) break
|
|
295
710
|
acc += (d & 31) << shift
|
|
296
711
|
if (d & 32) shift += 5
|
|
297
712
|
else {
|
|
@@ -299,7 +714,7 @@ function decodeMappings(mappings) {
|
|
|
299
714
|
acc = shift = 0
|
|
300
715
|
}
|
|
301
716
|
}
|
|
302
|
-
gCol += v[0]
|
|
717
|
+
gCol += v[0] || 0
|
|
303
718
|
if (v.length >= 4) {
|
|
304
719
|
src += v[1]
|
|
305
720
|
oLine += v[2]
|
|
@@ -312,7 +727,61 @@ function decodeMappings(mappings) {
|
|
|
312
727
|
return lines
|
|
313
728
|
}
|
|
314
729
|
|
|
315
|
-
|
|
730
|
+
// Where a served line/column (1-based) came from: { source, line } or null.
|
|
731
|
+
// Index maps (`sections`, Turbopack's) are read section by section.
|
|
732
|
+
const decodedMaps = new WeakMap()
|
|
733
|
+
function originalPosition(map, line, col) {
|
|
734
|
+
if (!map || !(line > 0)) return null
|
|
735
|
+
if (Array.isArray(map.sections)) {
|
|
736
|
+
const L = line - 1
|
|
737
|
+
const C = col > 0 ? col - 1 : null
|
|
738
|
+
let hit = null
|
|
739
|
+
for (const s of map.sections) {
|
|
740
|
+
const o = s.offset || { line: 0, column: 0 }
|
|
741
|
+
if (o.line < L || (o.line === L && (C == null || o.column <= C))) hit = s
|
|
742
|
+
else break
|
|
743
|
+
}
|
|
744
|
+
if (!hit || !hit.map) return null
|
|
745
|
+
const o = hit.offset || { line: 0, column: 0 }
|
|
746
|
+
return originalPosition(hit.map, L - o.line + 1, C == null ? null : (L === o.line ? C - o.column : C) + 1)
|
|
747
|
+
}
|
|
748
|
+
let lines = decodedMaps.get(map)
|
|
749
|
+
if (!lines) decodedMaps.set(map, (lines = decodeMappings(map.mappings)))
|
|
750
|
+
const segs = lines[line - 1]
|
|
751
|
+
if (!segs || !segs.length) return null
|
|
752
|
+
let best = segs[0]
|
|
753
|
+
if (col > 0) for (const s of segs) if (s[0] <= col - 1) best = s
|
|
754
|
+
let source = map.sources && map.sources[best[1]]
|
|
755
|
+
if (source && map.sourceRoot && !/^[a-z]+:/i.test(source) && !source.startsWith("/")) source = map.sourceRoot.replace(/\/?$/, "/") + source
|
|
756
|
+
return source ? { source, line: best[2] + 1 } : null
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// A map's source name as a path (webpack://app/./src/a.tsx → src/a.tsx,
|
|
760
|
+
// turbopack:///[project]/app/page.tsx → app/page.tsx, file:///… → /…),
|
|
761
|
+
// relative to the project when the map's node_modules sources show where it is.
|
|
762
|
+
function cleanSource(name, map) {
|
|
763
|
+
let p = String(name || "").replace(/[?#].*$/, "")
|
|
764
|
+
p = p.replace(/^webpack:\/\/[^/]*\//, "").replace(/^turbopack:\/\/\/?(\[project\]\/)?/, "").replace(/^\[project\]\//, "")
|
|
765
|
+
if (p.startsWith("file://")) {
|
|
766
|
+
try {
|
|
767
|
+
p = decodeURIComponent(new URL(p).pathname)
|
|
768
|
+
} catch {
|
|
769
|
+
p = p.slice(7)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
p = projectPath(p.replace(/^\.\//, ""))
|
|
773
|
+
if (p.startsWith("/") && map) {
|
|
774
|
+
const all = Array.isArray(map.sections) ? map.sections.flatMap((s) => (s.map && s.map.sources) || []) : map.sources || []
|
|
775
|
+
for (const s of all) {
|
|
776
|
+
const m = /^(?:file:\/\/)?(\/.*?)\/node_modules\//.exec(String(s || ""))
|
|
777
|
+
if (m && p.startsWith(m[1] + "/")) return p.slice(m[1].length + 1)
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return p
|
|
781
|
+
}
|
|
782
|
+
const libraryFile = (f) => /(^|\/)node_modules\/|\/\.vite\/deps\/|react-dom|jsx-dev-runtime|jsx-runtime|\[turbopack\]|turbopack\/|webpack\/(bootstrap|runtime)/.test(f)
|
|
783
|
+
|
|
784
|
+
const sourceMaps = new Map() // served url (without query) → Promise<map | null>
|
|
316
785
|
function loadMap(url) {
|
|
317
786
|
const key = url.replace(/\?.*$/, "")
|
|
318
787
|
if (!sourceMaps.has(key))
|
|
@@ -321,16 +790,22 @@ function loadMap(url) {
|
|
|
321
790
|
(async () => {
|
|
322
791
|
try {
|
|
323
792
|
if (!/^https?:\/\//.test(url)) return null
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
793
|
+
const res = await fetch(url)
|
|
794
|
+
if (!res.ok) return null
|
|
795
|
+
const code = await res.text()
|
|
796
|
+
const header = res.headers.get("sourcemap") || res.headers.get("x-sourcemap")
|
|
797
|
+
const refs = [...code.matchAll(/\/\/[#@] sourceMappingURL=(\S+)/g)]
|
|
798
|
+
const ref = header || (refs.length ? refs[refs.length - 1][1] : null)
|
|
327
799
|
let json
|
|
328
|
-
if (
|
|
329
|
-
const b64 =
|
|
330
|
-
json = /;base64/.test(
|
|
331
|
-
} else
|
|
332
|
-
|
|
333
|
-
|
|
800
|
+
if (ref && ref.startsWith("data:")) {
|
|
801
|
+
const b64 = ref.slice(ref.indexOf(",") + 1)
|
|
802
|
+
json = /;base64/.test(ref.slice(0, ref.indexOf(","))) ? new TextDecoder().decode(Uint8Array.from(atob(b64), (c) => c.charCodeAt(0))) : decodeURIComponent(b64)
|
|
803
|
+
} else {
|
|
804
|
+
const r = await fetch(ref ? new URL(ref, url) : key + ".map")
|
|
805
|
+
if (!r.ok) return null
|
|
806
|
+
json = await r.text()
|
|
807
|
+
}
|
|
808
|
+
return JSON.parse(json)
|
|
334
809
|
} catch {
|
|
335
810
|
return null
|
|
336
811
|
}
|
|
@@ -339,21 +814,37 @@ function loadMap(url) {
|
|
|
339
814
|
return sourceMaps.get(key)
|
|
340
815
|
}
|
|
341
816
|
|
|
342
|
-
// Rewrites src
|
|
343
|
-
// note that holds it is saved with the source line.
|
|
344
|
-
async function mapSource(src) {
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
817
|
+
// Rewrites src (file, line) in place from the first frame that maps into the
|
|
818
|
+
// app's own code, so the note that holds it is saved with the source line.
|
|
819
|
+
async function mapSource(src, frames = [src]) {
|
|
820
|
+
for (const fr of frames) {
|
|
821
|
+
if (!fr.url) {
|
|
822
|
+
Object.assign(src, { file: fr.file, line: null, mapped: true })
|
|
823
|
+
delete src.col
|
|
824
|
+
delete src.url
|
|
825
|
+
return
|
|
826
|
+
}
|
|
827
|
+
const map = await loadMap(fr.url)
|
|
828
|
+
if (!map) {
|
|
829
|
+
if (fr === frames[0]) break // nothing to read this one with: unmapped
|
|
830
|
+
continue
|
|
831
|
+
}
|
|
832
|
+
const pos = originalPosition(map, fr.line, fr.col)
|
|
833
|
+
if (!pos) continue
|
|
834
|
+
const file = cleanSource(pos.source, map)
|
|
835
|
+
if (libraryFile(file)) continue
|
|
836
|
+
// A module served as itself (Vite) keeps its served path; a bundle names the source.
|
|
837
|
+
const served = shortPath(fr.url)
|
|
838
|
+
src.file = file.split("/").pop() === served.split("/").pop() ? served : file
|
|
839
|
+
src.line = pos.line
|
|
840
|
+
src.mapped = true
|
|
841
|
+
delete src.col
|
|
842
|
+
delete src.url
|
|
843
|
+
delete src.compiled
|
|
844
|
+
return
|
|
353
845
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
delete src.url
|
|
846
|
+
// No map: a compiled bundle's line is no use to anyone; say so (the component and selector still are).
|
|
847
|
+
if (/\/_next\/static\/|\.vite\/deps|\._\.js$|[-_.](?=[0-9a-z]*\d)[0-9a-z]{6,}\.m?js$/i.test(src.file)) src.compiled = true
|
|
357
848
|
}
|
|
358
849
|
|
|
359
850
|
// The computed styles an agent would ask about first. Defaults are left out.
|
|
@@ -398,10 +889,14 @@ function describe(el) {
|
|
|
398
889
|
u.searchParams.delete("__wb")
|
|
399
890
|
page = u.pathname + u.search
|
|
400
891
|
} catch {}
|
|
892
|
+
const sel = selectorInfo(el)
|
|
401
893
|
return {
|
|
402
894
|
label: `<${tag}${id}${cls}>`,
|
|
403
895
|
text,
|
|
404
|
-
selector:
|
|
896
|
+
selector: sel.selector,
|
|
897
|
+
matches: sel.matches || 1,
|
|
898
|
+
shadowHost: sel.shadowHost || undefined,
|
|
899
|
+
hint: selectorHint(el, text),
|
|
405
900
|
components: reactComponents(el),
|
|
406
901
|
source: sourceOf(el),
|
|
407
902
|
classes: [...el.classList],
|
|
@@ -411,42 +906,83 @@ function describe(el) {
|
|
|
411
906
|
}
|
|
412
907
|
}
|
|
413
908
|
|
|
414
|
-
// What
|
|
415
|
-
//
|
|
416
|
-
|
|
909
|
+
// What grep finds the element by: its text, the CSS-module classes it has
|
|
910
|
+
// (`_title_18lyj_1` is `.title` in a *.module.css), its component.
|
|
911
|
+
function selectorHint(el, text) {
|
|
912
|
+
const parts = []
|
|
913
|
+
if (text) parts.push(`text "${text.length > 40 ? text.slice(0, 40) + "…" : text}"`)
|
|
914
|
+
const mods = [...el.classList].map((c) => [c, moduleClass(c)]).filter(([, m]) => m)
|
|
915
|
+
if (mods.length) parts.push(mods.map(([c, m]) => `${c} = .${m} in a CSS module`).join(", "))
|
|
916
|
+
const comp = reactComponents(el)[0]
|
|
917
|
+
if (comp) parts.push(`in <${comp}>`)
|
|
918
|
+
return parts.join("; ") || null
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// What was moving on the element at t, by the clip's exact element (never a
|
|
922
|
+
// selector match, F91): on it, else on one of its nearest ancestors, and then
|
|
923
|
+
// only if what that animates moves the element (a label inside a sliding
|
|
924
|
+
// button). A note on a still element says nothing is animating.
|
|
417
925
|
const CLIP_ANCESTORS = 4
|
|
926
|
+
const MOVES_IT = /^(all|transform|translate|scale|rotate|opacity|filter|clip-?path|top|left|right|bottom|inset|width|height|offset)/i
|
|
418
927
|
function clipFor(t, selector, el) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
928
|
+
if (!el) {
|
|
929
|
+
try {
|
|
930
|
+
el = findEl(selector, D.frame.contentDocument)
|
|
931
|
+
} catch {}
|
|
932
|
+
}
|
|
933
|
+
if (!el) return null
|
|
934
|
+
const at = (c) => c.start <= t + 0.5 && (c.end == null || t <= c.end + 0.5)
|
|
935
|
+
const latest = (list) => list.filter(at).sort((a, b) => b.start - a.start)[0] || null
|
|
936
|
+
let node = el
|
|
937
|
+
let c = latest(clipsOn(el).filter((x) => !x.pseudoElement))
|
|
938
|
+
for (let n = el.parentElement, i = 0; !c && n && n.tagName !== "BODY" && n.tagName !== "HTML" && i < CLIP_ANCESTORS; n = n.parentElement, i++) {
|
|
939
|
+
c = latest(clipsOn(n).filter((x) => !x.pseudoElement && clipMoves(x)))
|
|
940
|
+
node = n
|
|
941
|
+
}
|
|
942
|
+
if (!c) return null
|
|
423
943
|
const end = c.end == null ? null : c.end
|
|
424
|
-
return { id: c.id, offset: Math.round(
|
|
944
|
+
return { id: c.id, offset: Math.round(t - c.start), duration: end == null ? null : Math.round(end - c.start), start: Math.round(c.start), end: end == null ? null : Math.round(end), label: c.label || c.property || c.kind, kind: c.kind, property: c.property || null, selector: selectorInfo(node).selector || c.selector || null }
|
|
945
|
+
}
|
|
946
|
+
// The clips on the element itself: in its subtree (clipsFor, by recorded
|
|
947
|
+
// path), and not in any child's.
|
|
948
|
+
function clipsOn(el) {
|
|
949
|
+
const all = clipsOfElement(el)
|
|
950
|
+
if (!all.length) return []
|
|
951
|
+
const inKids = new Set()
|
|
952
|
+
for (const k of el.children) for (const c of clipsOfElement(k)) inKids.add(c)
|
|
953
|
+
return all.filter((c) => !inKids.has(c))
|
|
425
954
|
}
|
|
955
|
+
const clipMoves = (c) => [c.property, ...Object.keys(c.from || {}), ...Object.keys(c.to || {})].filter(Boolean).flatMap((p) => String(p).split(/,\s*/)).some((p) => MOVES_IT.test(p))
|
|
426
956
|
const clipPhrase = (c) =>
|
|
427
957
|
`${c.offset}ms into ${c.duration != null ? `a ${c.duration}ms` : "a running"} ${c.property ? `${c.property} ` : ""}${c.kind === "transition" ? "transition" : c.kind === "css-animation" ? `animation (${c.label})` : c.kind === "waapi" ? "animation" : c.label || "animation"}${c.selector ? ` on ${c.selector}` : ""}`
|
|
428
958
|
|
|
959
|
+
// Where the element was written, or why that isn't known.
|
|
960
|
+
const sourceText = (s) =>
|
|
961
|
+
s.compiled ? "not mapped (only a compiled bundle, no source map): find it by the component and selector" : `${s.file}${s.line ? `:${s.line}` : ""}`
|
|
962
|
+
|
|
429
963
|
// The note as a prompt for a coding agent: what to change, where it is in
|
|
430
|
-
// the code, and the
|
|
964
|
+
// the code, the moment and, for an animation, the exact point or range on its
|
|
965
|
+
// own clock. One formatter for this and `retake mcp` (src/note-text.js, NT).
|
|
431
966
|
function prompt(n) {
|
|
432
967
|
const b = branchById(n.branchId)
|
|
433
|
-
const start = D.last ? D.last.start : 0
|
|
434
968
|
const parent = b && branchById(b.parentId)
|
|
435
969
|
const el = n.el
|
|
436
|
-
const lines = [`## ${n.text}`, "", `Page: ${el.page}`, `Element: ${el.label}${el.text ? ` "${el.text}"` : ""}`, `Selector: ${el.selector}`]
|
|
437
|
-
if (el.components.length) lines.push(`Component: ${el.components.join(" < ")}`)
|
|
438
|
-
if (el.pseudo) lines.push(`Animation: ${pseudoSentence(el)}`)
|
|
439
|
-
if (el.cssSource) lines.push(`CSS: ${el.cssSource.file}${el.cssSource.line ? `:${el.cssSource.line}` : ""}${el.cssSource.keyframes && el.cssSource.keyframes.line ? ` (@keyframes ${el.cssSource.keyframes.name} at ${el.cssSource.keyframes.file}:${el.cssSource.keyframes.line})` : ""}`)
|
|
440
|
-
if (el.source) lines.push(`Source: ${el.source.file}${el.source.line ? `:${el.source.line}` : ""}`)
|
|
441
|
-
if (el.classes && el.classes.length) lines.push(`Classes: ${el.classes.join(" ")}`)
|
|
442
970
|
const st = el.styles || {}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
const
|
|
448
|
-
|
|
449
|
-
|
|
971
|
+
const extra = []
|
|
972
|
+
if (el.pseudo) extra.push(`Animation: ${pseudoSentence(el)}`)
|
|
973
|
+
// What an animation changes is given at the note's own point below; the
|
|
974
|
+
// styles read when the composer opened would only contradict it.
|
|
975
|
+
const moving = n.anims && n.anims.length ? /^(transform|opacity|animation-name|transition)$/ : null
|
|
976
|
+
const shown = Object.entries(st).filter(([k]) => !moving || !moving.test(k))
|
|
977
|
+
if (shown.length) extra.push(`Computed: ${shown.map(([k, v]) => `${k}: ${v}`).join("; ")}`)
|
|
978
|
+
return NT.noteText(noteForServer(n), {
|
|
979
|
+
start: D.last ? D.last.start : 0,
|
|
980
|
+
timeline: b ? b.name : "Timeline",
|
|
981
|
+
parent: parent ? parent.name : null,
|
|
982
|
+
forkAt: b ? b.forkAt : null,
|
|
983
|
+
sourceLine: el.source ? `Source: ${sourceText(el.source)}` : undefined,
|
|
984
|
+
computed: extra.join("\n") || undefined,
|
|
985
|
+
})
|
|
450
986
|
}
|
|
451
987
|
|
|
452
988
|
async function copy(text, btn) {
|
|
@@ -486,6 +1022,11 @@ function noteForServer(n) {
|
|
|
486
1022
|
status: n.status || "pending",
|
|
487
1023
|
replies: n.replies || [],
|
|
488
1024
|
el: n.el,
|
|
1025
|
+
range: n.range || null,
|
|
1026
|
+
target: n.target || null,
|
|
1027
|
+
anims: n.anims || null,
|
|
1028
|
+
inside: n.inside || [],
|
|
1029
|
+
asked: n.asked || null,
|
|
489
1030
|
}
|
|
490
1031
|
}
|
|
491
1032
|
|
|
@@ -500,7 +1041,7 @@ function noteFromServer(n) {
|
|
|
500
1041
|
classes: n.classes || [],
|
|
501
1042
|
source: n.source || null,
|
|
502
1043
|
}
|
|
503
|
-
return { id: n.id, branchId: n.branchId, t: n.t, text: n.text || "", el, clip: n.clip || null, status: n.status || "pending", replies: n.replies || [] }
|
|
1044
|
+
return { id: n.id, branchId: n.branchId, t: n.t, text: n.text || "", el, clip: n.clip || null, status: n.status || "pending", replies: n.replies || [], range: n.range || null, target: n.target || null, anims: n.anims || null, inside: n.inside || [], asked: n.asked || null }
|
|
504
1045
|
}
|
|
505
1046
|
|
|
506
1047
|
// A change from outside (an agent replied, or marked it resolved).
|
|
@@ -525,7 +1066,7 @@ function anchorOf(el) {
|
|
|
525
1066
|
const f = D.frame.getBoundingClientRect()
|
|
526
1067
|
let r = null
|
|
527
1068
|
try {
|
|
528
|
-
const node = el.selector
|
|
1069
|
+
const node = findEl(el.selector, D.frame.contentDocument)
|
|
529
1070
|
if (node) r = node.getBoundingClientRect()
|
|
530
1071
|
} catch {}
|
|
531
1072
|
if (!r || (!r.width && !r.height)) r = { x: el.rect.x, y: el.rect.y, width: el.rect.w, height: el.rect.h }
|
|
@@ -585,9 +1126,24 @@ function pseudoInfo(layer) {
|
|
|
585
1126
|
function pseudoClip(t, desc) {
|
|
586
1127
|
const want = (c) => (c.pseudoElement || null) === desc.pseudo.pseudoElement
|
|
587
1128
|
const clips = timeline().clips.filter((c) => want(c) && c.start <= t && (c.end == null || c.end >= t))
|
|
588
|
-
|
|
1129
|
+
let host = null
|
|
1130
|
+
try {
|
|
1131
|
+
host = findEl(desc.selector, D.frame.contentDocument)
|
|
1132
|
+
} catch {}
|
|
1133
|
+
const onHost = (c) => !!host && (c.selector === cssPath(host) + desc.pseudo.pseudoElement || nodeOfPseudoClip(c) === host)
|
|
1134
|
+
const c = clips.find((c) => c.selector === desc.selector + desc.pseudo.pseudoElement) || clips.find(onHost) || clips[0]
|
|
589
1135
|
if (!c) return null
|
|
590
|
-
return { id: c.id, offset: Math.round(t - c.start), duration: c.end == null ? null : Math.round(c.end - c.start), label: c.label || desc.pseudo.animationName, kind: c.kind, property: c.property || null, selector: c.selector || null }
|
|
1136
|
+
return { id: c.id, offset: Math.round(t - c.start), duration: c.end == null ? null : Math.round(c.end - c.start), start: Math.round(c.start), end: c.end == null ? null : Math.round(c.end), label: c.label || desc.pseudo.animationName, kind: c.kind, property: c.property || null, selector: c.selector || null }
|
|
1137
|
+
}
|
|
1138
|
+
// The element a ::before/::after clip is on (its selector without the pseudo-element).
|
|
1139
|
+
function nodeOfPseudoClip(c) {
|
|
1140
|
+
const pe = c.pseudoElement || ""
|
|
1141
|
+
const sel = c.selector && pe && c.selector.endsWith(pe) ? c.selector.slice(0, -pe.length) : c.selector
|
|
1142
|
+
try {
|
|
1143
|
+
return findEl(sel, D.frame.contentDocument)
|
|
1144
|
+
} catch {
|
|
1145
|
+
return null
|
|
1146
|
+
}
|
|
591
1147
|
}
|
|
592
1148
|
// Where the animation is written in the CSS (the runtime reads Vite's
|
|
593
1149
|
// source maps); filled in when it answers.
|
|
@@ -628,13 +1184,14 @@ function shortClip(c) {
|
|
|
628
1184
|
}
|
|
629
1185
|
|
|
630
1186
|
// Everything else about the element, folded away.
|
|
631
|
-
function details(el) {
|
|
1187
|
+
function details(el, geo = null) {
|
|
632
1188
|
const rows = [["Selector", el.selector]]
|
|
633
1189
|
if (el.components && el.components.length) rows.push(["Component", el.components.join(" < ")])
|
|
634
|
-
if (el.source) rows.push(["Source",
|
|
1190
|
+
if (el.source) rows.push(["Source", sourceText(el.source)])
|
|
635
1191
|
if (el.pseudo) rows.push(["Animation", pseudoSentence(el)])
|
|
636
1192
|
if (el.cssSource) rows.push(["CSS", `${el.cssSource.file}${el.cssSource.line ? ":" + el.cssSource.line : ""}`])
|
|
637
1193
|
if (el.classes && el.classes.length) rows.push(["Classes", el.classes.join(" ")])
|
|
1194
|
+
if (geo && geo.page) rows.push(["Box", `x ${Math.round(geo.page.x)} y ${Math.round(geo.page.y)} · ${Math.round(geo.page.w)}×${Math.round(geo.page.h)} (page)${geo.decomposed && (geo.decomposed.tx || geo.decomposed.ty) ? ` · translate ${geo.decomposed.tx}, ${geo.decomposed.ty}` : ""}${geo.opacity != null && geo.opacity !== 1 ? ` · opacity ${+Number(geo.opacity).toFixed(2)}` : ""}`])
|
|
638
1195
|
return `<details class="details"><summary>Details</summary><dl>${rows.map(([k, v]) => `<dt>${k}</dt><dd>${esc(v)}</dd>`).join("")}</dl></details>`
|
|
639
1196
|
}
|
|
640
1197
|
|
|
@@ -653,27 +1210,48 @@ function openComposer(el, point, layer) {
|
|
|
653
1210
|
lookUpCss(desc, layer.anim)
|
|
654
1211
|
}
|
|
655
1212
|
const clip = desc.pseudo ? pseudoClip(t, desc) : clipFor(t, desc.selector, el)
|
|
656
|
-
draft = { el: desc, t, clip }
|
|
1213
|
+
draft = { el: desc, t, clip, picked: layer && layer.picked ? layer.picked : null, asked: null, askedReading: null }
|
|
657
1214
|
openNote = null
|
|
658
|
-
|
|
1215
|
+
// The element becomes the dock's focus: its own animations get a row on the track (32-anims.js).
|
|
1216
|
+
focusOn(el, layer, t)
|
|
1217
|
+
card.innerHTML = `${meta(t, draft.el)}<div class="note-at" hidden></div><textarea rows="3" placeholder="What should change here?"></textarea>
|
|
1218
|
+
<button class="note-asked" data-note-a="asked" hidden></button>
|
|
659
1219
|
<div class="note-actions"><button data-note-a="cancel">Cancel</button><button data-note-a="save" class="primary">Add note</button></div>`
|
|
660
1220
|
card.hidden = false
|
|
1221
|
+
refreshComposer()
|
|
661
1222
|
placeCard(anchorOf(draft.el))
|
|
662
1223
|
const ta = card.querySelector("textarea")
|
|
663
1224
|
setTimeout(() => ta.focus())
|
|
1225
|
+
ta.addEventListener("input", updateAsked)
|
|
664
1226
|
// Enter saves and folds the note down to its pin; Shift+Enter is a new line.
|
|
665
1227
|
ta.addEventListener("keydown", (e) => {
|
|
666
1228
|
if (e.key === "Enter" && !e.shiftKey) {
|
|
667
1229
|
e.preventDefault()
|
|
668
1230
|
saveDraft()
|
|
669
1231
|
}
|
|
670
|
-
if (e.key === "Escape")
|
|
1232
|
+
if (e.key === "Escape") {
|
|
1233
|
+
e.stopPropagation()
|
|
1234
|
+
if (!closeFocusClip()) closeCard()
|
|
1235
|
+
}
|
|
671
1236
|
})
|
|
672
1237
|
}
|
|
673
1238
|
|
|
674
1239
|
function saveDraft() {
|
|
675
1240
|
const text = card.querySelector("textarea").value.trim()
|
|
676
|
-
if (draft && text)
|
|
1241
|
+
if (draft && text) {
|
|
1242
|
+
updateAsked()
|
|
1243
|
+
let extra = { t: draft.t, range: null, target: null, anims: null, inside: [], asked: null }
|
|
1244
|
+
try {
|
|
1245
|
+
extra = notePayload(draft)
|
|
1246
|
+
} catch (err) {
|
|
1247
|
+
console.error("[retake] couldn't describe the note's animations", err)
|
|
1248
|
+
}
|
|
1249
|
+
// A note moved to another moment on the element's row: its clip summary follows.
|
|
1250
|
+
const clip = extra.t !== draft.t && !draft.el.pseudo ? clipFor(extra.t, draft.el.selector, D.focus && D.focus.el) : draft.clip
|
|
1251
|
+
// A dragged range wins over numbers typed in the note.
|
|
1252
|
+
const asked = extra.range && D.focus && D.focus.open ? null : extra.asked
|
|
1253
|
+
D.notes.push({ id: newNoteId(), t: extra.t, branchId: D.activeId, text, el: draft.el, clip, status: "pending", replies: [], range: extra.range, target: extra.target, anims: extra.anims, inside: extra.inside, asked })
|
|
1254
|
+
}
|
|
677
1255
|
closeCard()
|
|
678
1256
|
// Back to the Hand so the next click is on the prototype, not another note.
|
|
679
1257
|
setPicking(null)
|
|
@@ -688,9 +1266,11 @@ function showNote(n) {
|
|
|
688
1266
|
.map((r) => `<div class="reply ${r.from === "agent" ? "agent" : "user"}"><span class="who">${r.from === "agent" ? "Agent" : "You"}</span>${esc(r.text)}</div>`)
|
|
689
1267
|
.join("")
|
|
690
1268
|
const status = n.status || "pending"
|
|
1269
|
+
const at = n.anims ? NT.atPhrase(noteForServer(n)) : null
|
|
1270
|
+
const clipLine = at || (n.clip ? shortClip(n.clip) : "")
|
|
691
1271
|
card.innerHTML = `${meta(n.t, n.el, n.branchId)}<p class="note-text">${esc(n.text)}</p>
|
|
692
|
-
${
|
|
693
|
-
${details(n.el)}
|
|
1272
|
+
${clipLine ? `<div class="note-clip">${esc(clipLine)}</div>` : ""}
|
|
1273
|
+
${details(n.el, n.target && n.target.geometry)}
|
|
694
1274
|
${replies ? `<div class="replies">${replies}</div>` : ""}
|
|
695
1275
|
<div class="note-actions"><span class="status s-${esc(status)}">${esc(status[0].toUpperCase() + status.slice(1))}</span>
|
|
696
1276
|
<button data-note-a="delete" class="quiet">Delete</button>
|
|
@@ -699,6 +1279,11 @@ function showNote(n) {
|
|
|
699
1279
|
if (keepOpen) card.querySelector(".details").open = true
|
|
700
1280
|
card.hidden = false
|
|
701
1281
|
placeCard(anchorOf(n.el))
|
|
1282
|
+
// Its element row, opened clip and range come back with it.
|
|
1283
|
+
if (n.anims && n.anims.length && (!D.focus || D.focus.note !== n)) {
|
|
1284
|
+
focusFromNote(n)
|
|
1285
|
+
if (D.focus) D.focus.note = n
|
|
1286
|
+
}
|
|
702
1287
|
}
|
|
703
1288
|
|
|
704
1289
|
// Tell the server about a status change (the session save carries it too).
|
|
@@ -711,6 +1296,7 @@ function closeCard() {
|
|
|
711
1296
|
card.hidden = true
|
|
712
1297
|
draft = null
|
|
713
1298
|
openNote = null
|
|
1299
|
+
clearFocus()
|
|
714
1300
|
}
|
|
715
1301
|
|
|
716
1302
|
// Returns true when the click was one of ours.
|
|
@@ -718,6 +1304,10 @@ function handleNoteClick(b) {
|
|
|
718
1304
|
const act = b.dataset.noteA
|
|
719
1305
|
if (act === "save") saveDraft()
|
|
720
1306
|
if (act === "cancel") closeCard()
|
|
1307
|
+
if (act === "asked" && draft) {
|
|
1308
|
+
draft.askedReading = draft.askedReading === "recording" ? null : "recording"
|
|
1309
|
+
updateAsked()
|
|
1310
|
+
}
|
|
721
1311
|
if (act === "copy" && openNote) {
|
|
722
1312
|
copy(prompt(openNote), b)
|
|
723
1313
|
// Copied: fold the note back to its pin.
|
|
@@ -805,6 +1395,10 @@ function renderExtras(s) {
|
|
|
805
1395
|
t.classList.toggle("on", t.dataset.tool === tool)
|
|
806
1396
|
t.setAttribute("aria-selected", String(t.dataset.tool === tool))
|
|
807
1397
|
}
|
|
1398
|
+
syncEmbeds()
|
|
1399
|
+
watchClasses(D.frame)
|
|
1400
|
+
if (D.building) watchClasses(D.building.frame)
|
|
1401
|
+
document.body.classList.toggle("picking-notes", mode() === "comment")
|
|
808
1402
|
|
|
809
1403
|
updateLens()
|
|
810
1404
|
renderLayers()
|