react-detective 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +135 -2
- package/dist/chunk-5TDNB3DH.js +2686 -0
- package/dist/chunk-6EJCTAJS.js +25 -0
- package/dist/chunk-E23QXCCY.js +43 -0
- package/dist/chunk-Z2Y65YOY.cjs +7 -0
- package/dist/cli.js +35 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.js +2028 -0
- package/dist/init-JUCWNJPS.js +630 -0
- package/dist/loader.cjs +105 -0
- package/dist/loader.js +101 -0
- package/dist/next.cjs +58 -0
- package/dist/next.d.cts +24 -0
- package/dist/next.d.ts +22 -0
- package/dist/next.js +54 -0
- package/dist/react.d.ts +11 -0
- package/dist/react.js +26 -0
- package/dist/server.d.ts +292 -0
- package/dist/server.js +15 -0
- package/dist/vite.d.ts +17 -0
- package/dist/vite.js +132 -0
- package/package.json +84 -4
package/dist/client.js
ADDED
|
@@ -0,0 +1,2028 @@
|
|
|
1
|
+
// src/client/fiber.ts
|
|
2
|
+
function readLiveData(el, componentName) {
|
|
3
|
+
const fiberKey = Object.keys(el).find((k) => k.startsWith("__reactFiber$"));
|
|
4
|
+
let fiber = fiberKey ? el[fiberKey] : null;
|
|
5
|
+
while (fiber) {
|
|
6
|
+
if (typeof fiber.type !== "string" && fiberName(fiber) === componentName) {
|
|
7
|
+
const current = currentFiber(fiber);
|
|
8
|
+
const owner = current._debugOwner && typeof current._debugOwner.type !== "string" ? fiberName(current._debugOwner) : void 0;
|
|
9
|
+
return { props: serialize(stripChildren(current.memoizedProps)), state: readHookState(current), contexts: readContexts(current), hooks: readHooks(current), owner };
|
|
10
|
+
}
|
|
11
|
+
fiber = fiber.return;
|
|
12
|
+
}
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
function currentFiber(fiber) {
|
|
16
|
+
const alt = fiber.alternate;
|
|
17
|
+
if (!alt) return fiber;
|
|
18
|
+
let top = fiber;
|
|
19
|
+
while (top.return) top = top.return;
|
|
20
|
+
const root = top.stateNode?.current;
|
|
21
|
+
if (!root) return fiber;
|
|
22
|
+
const stack = [root];
|
|
23
|
+
while (stack.length) {
|
|
24
|
+
const node = stack.pop();
|
|
25
|
+
if (node === fiber) return fiber;
|
|
26
|
+
if (node === alt) return alt;
|
|
27
|
+
if (node.sibling) stack.push(node.sibling);
|
|
28
|
+
if (node.child) stack.push(node.child);
|
|
29
|
+
}
|
|
30
|
+
return fiber;
|
|
31
|
+
}
|
|
32
|
+
function fiberName(fiber) {
|
|
33
|
+
const t = fiber.type;
|
|
34
|
+
if (!t) return;
|
|
35
|
+
if (typeof t === "function") return t.displayName || t.name;
|
|
36
|
+
return t.displayName || t.render?.displayName || t.render?.name || t.type?.displayName || t.type?.name;
|
|
37
|
+
}
|
|
38
|
+
function stripChildren(props2) {
|
|
39
|
+
if (!props2 || typeof props2 !== "object") return props2;
|
|
40
|
+
const { children, ...rest } = props2;
|
|
41
|
+
return children === void 0 ? rest : { ...rest, children: "[children]" };
|
|
42
|
+
}
|
|
43
|
+
function readContexts(fiber) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const seen = /* @__PURE__ */ new Set();
|
|
46
|
+
let dep = fiber.dependencies?.firstContext;
|
|
47
|
+
let guard = 0;
|
|
48
|
+
while (dep && guard++ < 50) {
|
|
49
|
+
if (!seen.has(dep.context)) {
|
|
50
|
+
seen.add(dep.context);
|
|
51
|
+
out.push(serialize(dep.memoizedValue));
|
|
52
|
+
}
|
|
53
|
+
dep = dep.next;
|
|
54
|
+
}
|
|
55
|
+
return out.length ? out : void 0;
|
|
56
|
+
}
|
|
57
|
+
function readHooks(fiber) {
|
|
58
|
+
if (fiber.tag === 1) return void 0;
|
|
59
|
+
const out = [];
|
|
60
|
+
let hook = fiber.memoizedState;
|
|
61
|
+
let guard = 0;
|
|
62
|
+
while (hook && guard++ < 100) {
|
|
63
|
+
out.push({ isState: !!hook.queue && "lastRenderedState" in hook.queue, stored: hook.memoizedState });
|
|
64
|
+
hook = hook.next;
|
|
65
|
+
}
|
|
66
|
+
return out.length ? out : void 0;
|
|
67
|
+
}
|
|
68
|
+
function readHookState(fiber) {
|
|
69
|
+
if (fiber.tag === 1) return fiber.memoizedState ? [serialize(fiber.memoizedState)] : void 0;
|
|
70
|
+
const out = [];
|
|
71
|
+
let hook = fiber.memoizedState;
|
|
72
|
+
let guard = 0;
|
|
73
|
+
while (hook && guard++ < 50) {
|
|
74
|
+
if (hook.queue && "lastRenderedState" in hook.queue) out.push(serialize(hook.memoizedState));
|
|
75
|
+
hook = hook.next;
|
|
76
|
+
}
|
|
77
|
+
return out.length ? out : void 0;
|
|
78
|
+
}
|
|
79
|
+
function serialize(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
|
|
80
|
+
if (value === null || value === void 0) return value;
|
|
81
|
+
const type = typeof value;
|
|
82
|
+
if (type === "string") return value.length > 300 ? value.slice(0, 300) + "\u2026" : value;
|
|
83
|
+
if (type === "number" || type === "boolean") return value;
|
|
84
|
+
if (type === "bigint") return `${value}n`;
|
|
85
|
+
if (type === "symbol") return String(value);
|
|
86
|
+
if (type === "function") return `[Function ${value.name || "anonymous"}]`;
|
|
87
|
+
const obj = value;
|
|
88
|
+
if (obj.$$typeof) return `[React element${obj.type ? ` <${typeof obj.type === "string" ? obj.type : obj.type.displayName || obj.type.name || "?"}>` : ""}]`;
|
|
89
|
+
if (typeof Element !== "undefined" && obj instanceof Element) return `[DOM <${obj.tagName.toLowerCase()}>]`;
|
|
90
|
+
if (obj instanceof Date) return obj.toISOString();
|
|
91
|
+
if (seen.has(obj)) return "[Circular]";
|
|
92
|
+
if (depth >= 5) return Array.isArray(obj) ? `[Array(${obj.length})]` : "[Object]";
|
|
93
|
+
seen.add(obj);
|
|
94
|
+
if (Array.isArray(obj)) {
|
|
95
|
+
const items = obj.slice(0, 10).map((v) => serialize(v, depth + 1, seen));
|
|
96
|
+
if (obj.length > 10) items.push(`\u2026 ${obj.length - 10} more`);
|
|
97
|
+
return items;
|
|
98
|
+
}
|
|
99
|
+
if (obj instanceof Map) return { "[Map]": serialize(Object.fromEntries([...obj.entries()].slice(0, 10)), depth + 1, seen) };
|
|
100
|
+
if (obj instanceof Set) return { "[Set]": serialize([...obj].slice(0, 10), depth + 1, seen) };
|
|
101
|
+
const out = {};
|
|
102
|
+
const keys = Object.keys(obj);
|
|
103
|
+
for (const k of keys.slice(0, 25)) out[k] = serialize(obj[k], depth + 1, seen);
|
|
104
|
+
if (keys.length > 25) out["\u2026"] = `${keys.length - 25} more keys`;
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/client/markdown.ts
|
|
109
|
+
var FILE_REF = /^([\w@.\-/[\]()]+\.(?:[cm]?[jt]sx?|css|scss|json|md))(?::(\d+))?(?::(\d+))?$/;
|
|
110
|
+
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
111
|
+
function inline(text) {
|
|
112
|
+
const codes = [];
|
|
113
|
+
const withSlots = text.replace(/`([^`]+)`/g, (_, code2) => {
|
|
114
|
+
const m = code2.match(FILE_REF);
|
|
115
|
+
codes.push(
|
|
116
|
+
m ? `<a class="src" data-file="${esc(m[1])}" data-line="${m[2] ?? 1}" data-col="${m[3] ?? 1}"><code>${esc(code2)}</code></a>` : `<code>${esc(code2)}</code>`
|
|
117
|
+
);
|
|
118
|
+
return `\0${codes.length - 1}\0`;
|
|
119
|
+
});
|
|
120
|
+
return esc(withSlots).replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/(^|[^*\w])\*(?!\s)(.+?)\*(?!\w)/g, "$1<em>$2</em>").replace(/\[([^\]]+)\]\((https?:[^)\s]+)\)/g, '<a href="$2" target="_blank" rel="noreferrer">$1</a>').replace(/\u0000(\d+)\u0000/g, (_, i) => codes[Number(i)]);
|
|
121
|
+
}
|
|
122
|
+
function renderMarkdown(md) {
|
|
123
|
+
const lines = md.replace(/\r\n/g, "\n").split("\n");
|
|
124
|
+
const out = [];
|
|
125
|
+
let list = null;
|
|
126
|
+
let para = [];
|
|
127
|
+
const flushPara = () => {
|
|
128
|
+
if (para.length) out.push(`<p>${inline(para.join(" "))}</p>`);
|
|
129
|
+
para = [];
|
|
130
|
+
};
|
|
131
|
+
const closeList = () => {
|
|
132
|
+
if (list) out.push(`</${list}>`);
|
|
133
|
+
list = null;
|
|
134
|
+
};
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
const line = lines[i];
|
|
137
|
+
const fence = line.match(/^```(\w*)/);
|
|
138
|
+
if (fence) {
|
|
139
|
+
flushPara();
|
|
140
|
+
closeList();
|
|
141
|
+
const body = [];
|
|
142
|
+
while (++i < lines.length && !lines[i].startsWith("```")) body.push(lines[i]);
|
|
143
|
+
out.push(`<pre><code>${esc(body.join("\n"))}</code></pre>`);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
const heading = line.match(/^(#{1,4})\s+(.*)/);
|
|
147
|
+
if (heading) {
|
|
148
|
+
flushPara();
|
|
149
|
+
closeList();
|
|
150
|
+
const level = Math.min(heading[1].length + 1, 5);
|
|
151
|
+
out.push(`<h${level}>${inline(heading[2])}</h${level}>`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const item = line.match(/^\s*(?:([-*+])|(\d+)[.)])\s+(.*)/);
|
|
155
|
+
if (item) {
|
|
156
|
+
flushPara();
|
|
157
|
+
const kind = item[1] ? "ul" : "ol";
|
|
158
|
+
if (list !== kind) {
|
|
159
|
+
closeList();
|
|
160
|
+
out.push(`<${kind}>`);
|
|
161
|
+
list = kind;
|
|
162
|
+
}
|
|
163
|
+
out.push(`<li>${inline(item[3])}</li>`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (!line.trim()) {
|
|
167
|
+
flushPara();
|
|
168
|
+
closeList();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (list && /^\s{2,}/.test(line)) {
|
|
172
|
+
out[out.length - 1] = out[out.length - 1].replace(/<\/li>$/, ` ${inline(line.trim())}</li>`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
closeList();
|
|
176
|
+
para.push(line.trim());
|
|
177
|
+
}
|
|
178
|
+
flushPara();
|
|
179
|
+
closeList();
|
|
180
|
+
return out.join("\n");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/client/evaluate.ts
|
|
184
|
+
var code = (text) => `[Code ${text.trim()}]`;
|
|
185
|
+
var FUNCTION = "[Function anonymous]";
|
|
186
|
+
function evaluate(text, known) {
|
|
187
|
+
const src = text.trim();
|
|
188
|
+
let i = 0;
|
|
189
|
+
let resolved = false;
|
|
190
|
+
class Bail extends Error {
|
|
191
|
+
}
|
|
192
|
+
const bail = () => {
|
|
193
|
+
throw new Bail();
|
|
194
|
+
};
|
|
195
|
+
const ws = () => {
|
|
196
|
+
while (i < src.length && /\s/.test(src[i])) i++;
|
|
197
|
+
};
|
|
198
|
+
const peek = () => src[i];
|
|
199
|
+
const eat = (c) => {
|
|
200
|
+
ws();
|
|
201
|
+
if (src[i] !== c) bail();
|
|
202
|
+
i++;
|
|
203
|
+
};
|
|
204
|
+
const skipString = () => {
|
|
205
|
+
const quote = src[i++];
|
|
206
|
+
while (i < src.length && src[i] !== quote) {
|
|
207
|
+
if (src[i] === "\\") i += 2;
|
|
208
|
+
else if (quote === "`" && src[i] === "$" && src[i + 1] === "{") {
|
|
209
|
+
i += 2;
|
|
210
|
+
skipTo("}");
|
|
211
|
+
i++;
|
|
212
|
+
} else i++;
|
|
213
|
+
}
|
|
214
|
+
i++;
|
|
215
|
+
};
|
|
216
|
+
const skipTo = (stop = "") => {
|
|
217
|
+
let depth = 0;
|
|
218
|
+
while (i < src.length) {
|
|
219
|
+
const c = src[i];
|
|
220
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
221
|
+
skipString();
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (depth === 0 && (c === stop || !stop && ",)]}".includes(c))) return;
|
|
225
|
+
if ("([{".includes(c)) depth++;
|
|
226
|
+
else if (")]}".includes(c)) depth--;
|
|
227
|
+
i++;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
const expression = () => {
|
|
231
|
+
ws();
|
|
232
|
+
const start = i;
|
|
233
|
+
if (/^(async\s+)?(\([^()]*\)|[\w$]+)\s*=>|^(async\s+)?function\b/.test(src.slice(i))) {
|
|
234
|
+
skipTo();
|
|
235
|
+
return FUNCTION;
|
|
236
|
+
}
|
|
237
|
+
const resolvedBefore = resolved;
|
|
238
|
+
try {
|
|
239
|
+
const value2 = primary();
|
|
240
|
+
ws();
|
|
241
|
+
if (i < src.length && !",)]}".includes(peek())) bail();
|
|
242
|
+
return value2;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
if (!(e instanceof Bail)) throw e;
|
|
245
|
+
resolved = resolvedBefore;
|
|
246
|
+
i = start;
|
|
247
|
+
skipTo();
|
|
248
|
+
return code(src.slice(start, i));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
const primary = () => {
|
|
252
|
+
ws();
|
|
253
|
+
const c = peek();
|
|
254
|
+
if (c === '"' || c === "'") return stringLiteral();
|
|
255
|
+
if (c === "`") return template();
|
|
256
|
+
if (c === "[") return array();
|
|
257
|
+
if (c === "{") return object();
|
|
258
|
+
if (c === "(") {
|
|
259
|
+
i++;
|
|
260
|
+
const inner = expression();
|
|
261
|
+
eat(")");
|
|
262
|
+
return postfix(inner);
|
|
263
|
+
}
|
|
264
|
+
const num = src.slice(i).match(/^-?(\d[\d_]*\.?\d*(e[+-]?\d+)?|\.\d+)/i);
|
|
265
|
+
if (num) {
|
|
266
|
+
i += num[0].length;
|
|
267
|
+
resolved = true;
|
|
268
|
+
return Number(num[0].replace(/_/g, ""));
|
|
269
|
+
}
|
|
270
|
+
const id = src.slice(i).match(/^[A-Za-z_$][\w$]*/);
|
|
271
|
+
if (!id) return bail();
|
|
272
|
+
i += id[0].length;
|
|
273
|
+
const literals = { true: true, false: false, null: null, undefined: void 0 };
|
|
274
|
+
if (id[0] in literals) {
|
|
275
|
+
resolved = true;
|
|
276
|
+
return literals[id[0]];
|
|
277
|
+
}
|
|
278
|
+
if (!known.has(id[0])) return bail();
|
|
279
|
+
resolved = true;
|
|
280
|
+
return postfix(known.get(id[0]));
|
|
281
|
+
};
|
|
282
|
+
const postfix = (value2) => {
|
|
283
|
+
for (; ; ) {
|
|
284
|
+
ws();
|
|
285
|
+
const optional = src.startsWith("?.", i);
|
|
286
|
+
if (optional) i += 2;
|
|
287
|
+
if (peek() === "[") {
|
|
288
|
+
i++;
|
|
289
|
+
const key = expression();
|
|
290
|
+
eat("]");
|
|
291
|
+
if (typeof key !== "string" && typeof key !== "number") return bail();
|
|
292
|
+
value2 = member(value2, key, optional);
|
|
293
|
+
} else if (peek() === "(") return bail();
|
|
294
|
+
else if (optional || peek() === "." && src[i + 1] !== ".") {
|
|
295
|
+
if (!optional) i++;
|
|
296
|
+
ws();
|
|
297
|
+
const name = src.slice(i).match(/^[A-Za-z_$][\w$]*/);
|
|
298
|
+
if (!name) return bail();
|
|
299
|
+
i += name[0].length;
|
|
300
|
+
value2 = member(value2, name[0], optional);
|
|
301
|
+
} else return value2;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const member = (value2, key, optional) => {
|
|
305
|
+
if (value2 === null || value2 === void 0) return optional ? void 0 : bail();
|
|
306
|
+
if (Array.isArray(value2) && key === "length") return value2.length;
|
|
307
|
+
if (typeof value2 === "string" && key === "length") return value2.length;
|
|
308
|
+
if (typeof value2 !== "object" || !(key in value2)) return bail();
|
|
309
|
+
return value2[key];
|
|
310
|
+
};
|
|
311
|
+
const stringLiteral = () => {
|
|
312
|
+
const quote = src[i++];
|
|
313
|
+
let out = "";
|
|
314
|
+
while (i < src.length && src[i] !== quote) {
|
|
315
|
+
if (src[i] === "\\") {
|
|
316
|
+
const next = src[i + 1];
|
|
317
|
+
out += next === "n" ? "\n" : next === "t" ? " " : next;
|
|
318
|
+
i += 2;
|
|
319
|
+
} else out += src[i++];
|
|
320
|
+
}
|
|
321
|
+
i++;
|
|
322
|
+
resolved = true;
|
|
323
|
+
return out;
|
|
324
|
+
};
|
|
325
|
+
const template = () => {
|
|
326
|
+
i++;
|
|
327
|
+
let out = "";
|
|
328
|
+
while (i < src.length && src[i] !== "`") {
|
|
329
|
+
if (src[i] === "\\") {
|
|
330
|
+
out += src[i + 1];
|
|
331
|
+
i += 2;
|
|
332
|
+
} else if (src[i] === "$" && src[i + 1] === "{") {
|
|
333
|
+
i += 2;
|
|
334
|
+
const part = expression();
|
|
335
|
+
if (typeof part === "string" && part.startsWith("[Code ")) bail();
|
|
336
|
+
out += typeof part === "object" && part !== null ? JSON.stringify(part) : String(part);
|
|
337
|
+
eat("}");
|
|
338
|
+
} else out += src[i++];
|
|
339
|
+
}
|
|
340
|
+
i++;
|
|
341
|
+
resolved = true;
|
|
342
|
+
return out;
|
|
343
|
+
};
|
|
344
|
+
const array = () => {
|
|
345
|
+
i++;
|
|
346
|
+
const out = [];
|
|
347
|
+
for (; ; ) {
|
|
348
|
+
ws();
|
|
349
|
+
if (peek() === "]") break;
|
|
350
|
+
if (src.startsWith("...", i)) {
|
|
351
|
+
i += 3;
|
|
352
|
+
const spread = expression();
|
|
353
|
+
if (Array.isArray(spread)) out.push(...spread);
|
|
354
|
+
else out.push(typeof spread === "string" && spread.startsWith("[Code ") ? code(`...${spread.slice(6, -1)}`) : bail());
|
|
355
|
+
} else out.push(expression());
|
|
356
|
+
ws();
|
|
357
|
+
if (peek() === ",") i++;
|
|
358
|
+
else break;
|
|
359
|
+
}
|
|
360
|
+
eat("]");
|
|
361
|
+
return out;
|
|
362
|
+
};
|
|
363
|
+
const object = () => {
|
|
364
|
+
i++;
|
|
365
|
+
const out = {};
|
|
366
|
+
for (; ; ) {
|
|
367
|
+
ws();
|
|
368
|
+
if (peek() === "}") break;
|
|
369
|
+
if (src.startsWith("...", i)) {
|
|
370
|
+
i += 3;
|
|
371
|
+
const spread = expression();
|
|
372
|
+
if (spread && typeof spread === "object" && !Array.isArray(spread)) Object.assign(out, spread);
|
|
373
|
+
else out["\u2026"] = typeof spread === "string" && spread.startsWith("[Code ") ? code(`...${spread.slice(6, -1)}`) : spread;
|
|
374
|
+
} else {
|
|
375
|
+
const keyStart = i;
|
|
376
|
+
let key;
|
|
377
|
+
if (peek() === '"' || peek() === "'") key = stringLiteral();
|
|
378
|
+
else if (peek() === "[") {
|
|
379
|
+
skipTo(":");
|
|
380
|
+
key = src.slice(keyStart, i).trim();
|
|
381
|
+
} else {
|
|
382
|
+
const name = src.slice(i).match(/^[A-Za-z_$][\w$]*|^\d+/);
|
|
383
|
+
if (!name) return bail();
|
|
384
|
+
key = name[0];
|
|
385
|
+
i += key.length;
|
|
386
|
+
}
|
|
387
|
+
ws();
|
|
388
|
+
if (peek() === ":") {
|
|
389
|
+
i++;
|
|
390
|
+
out[key] = expression();
|
|
391
|
+
} else if (peek() === "(") {
|
|
392
|
+
skipTo();
|
|
393
|
+
out[key] = FUNCTION;
|
|
394
|
+
} else {
|
|
395
|
+
out[key] = known.has(key) ? (resolved = true, known.get(key)) : code(key);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
ws();
|
|
399
|
+
if (peek() === ",") i++;
|
|
400
|
+
else break;
|
|
401
|
+
}
|
|
402
|
+
eat("}");
|
|
403
|
+
return out;
|
|
404
|
+
};
|
|
405
|
+
const value = expression();
|
|
406
|
+
return { value, resolved };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// src/client/value.ts
|
|
410
|
+
var span = (cls, text) => `<span class="${cls}">${esc(text)}</span>`;
|
|
411
|
+
function special(s) {
|
|
412
|
+
let m = s.match(/^\[Code (.*)\]$/s);
|
|
413
|
+
if (m) return span("v-code", m[1].length > 40 ? `${m[1].slice(0, 39)}\u2026` : m[1]);
|
|
414
|
+
m = s.match(/^\[Function (.*)\]$/);
|
|
415
|
+
if (m) return span("v-fn", `\u0192 ${m[1] === "anonymous" ? "" : m[1]}()`);
|
|
416
|
+
m = s.match(/^\[React element <(.+)>\]$/);
|
|
417
|
+
if (m) return span("v-el", `<${m[1]} />`);
|
|
418
|
+
if (/^\[React element\]$/.test(s)) return span("v-el", "<element />");
|
|
419
|
+
if (/^\[(children|Circular|Object|Array\(\d+\)|DOM <[^>]+>)\]$/.test(s)) return span("v-meta", s.slice(1, -1));
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function preview(v, depth = 0) {
|
|
423
|
+
if (v === null) return span("v-null", "null");
|
|
424
|
+
if (v === void 0) return span("v-null", "undefined");
|
|
425
|
+
if (typeof v === "boolean") return span("v-bool", String(v));
|
|
426
|
+
if (typeof v === "number") return span("v-num", String(v));
|
|
427
|
+
if (typeof v === "string") {
|
|
428
|
+
const s = special(v);
|
|
429
|
+
if (s) return s;
|
|
430
|
+
return span("v-str", `"${v.length > 40 ? v.slice(0, 39) + "\u2026" : v}"`);
|
|
431
|
+
}
|
|
432
|
+
if (Array.isArray(v)) {
|
|
433
|
+
if (depth > 0) return span("v-meta", `Array(${v.length})`);
|
|
434
|
+
const shown = v.slice(0, 3).map((x) => preview(x, depth + 1));
|
|
435
|
+
return `<span class="v-meta">(${v.length})</span> [${shown.join(", ")}${v.length > 3 ? ", \u2026" : ""}]`;
|
|
436
|
+
}
|
|
437
|
+
if (typeof v === "object") {
|
|
438
|
+
const entries = Object.entries(v);
|
|
439
|
+
if (depth > 0) return span("v-meta", entries.length ? "{\u2026}" : "{}");
|
|
440
|
+
const shown = entries.slice(0, 3).map(([k, x]) => `${span("v-key", k)}: ${preview(x, depth + 1)}`);
|
|
441
|
+
return `{ ${shown.join(", ")}${entries.length > 3 ? `, ${span("v-meta", `+${entries.length - 3}`)}` : ""} }`;
|
|
442
|
+
}
|
|
443
|
+
return span("v-meta", String(v));
|
|
444
|
+
}
|
|
445
|
+
function isExpandable(v) {
|
|
446
|
+
return v !== null && typeof v === "object" && Object.keys(v).length > 0;
|
|
447
|
+
}
|
|
448
|
+
function tree(v, depth = 0) {
|
|
449
|
+
const entries = Array.isArray(v) ? v.map((x, i) => [String(i), x]) : Object.entries(v);
|
|
450
|
+
return `<div class="vt">${entries.map(([k, x]) => {
|
|
451
|
+
const row = `<span class="vt-tw"></span><span class="v-key">${esc(k)}</span><span class="vt-val">${preview(x)}</span>`;
|
|
452
|
+
return isExpandable(x) && depth < 8 ? `<details class="vt-node"><summary class="vt-row">${row}</summary>${tree(x, depth + 1)}</details>` : `<div class="vt-row">${row}</div>`;
|
|
453
|
+
}).join("")}</div>`;
|
|
454
|
+
}
|
|
455
|
+
function valueBox(v, label) {
|
|
456
|
+
const expandable = isExpandable(v);
|
|
457
|
+
return `<div class="val ${expandable ? "expandable" : ""}" ${expandable ? 'title="Click to expand"' : ""}>
|
|
458
|
+
<div class="val-line">
|
|
459
|
+
${label ? `<span class="val-label">${esc(label)}</span>` : ""}
|
|
460
|
+
<span class="val-preview">${preview(v)}</span>
|
|
461
|
+
${expandable ? '<span class="val-chevron">\u25B8</span>' : ""}
|
|
462
|
+
</div>
|
|
463
|
+
${expandable ? `<div class="val-tree">${tree(v)}</div>` : ""}
|
|
464
|
+
</div>`;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/client/card.ts
|
|
468
|
+
var svg = (body) => `<svg viewBox="0 0 24 24" aria-hidden="true">${body}</svg>`;
|
|
469
|
+
var ICONS = {
|
|
470
|
+
// Braces: a value the component holds.
|
|
471
|
+
state: svg(`<path d="M8 3H7a2 2 0 0 0-2 2v4a2 2 0 0 1-2 2 2 2 0 0 1 2 2v4a2 2 0 0 0 2 2h1M16 3h1a2 2 0 0 1 2 2v4a2 2 0 0 0 2 2 2 2 0 0 0-2 2v4a2 2 0 0 1-2 2h-1"/>`),
|
|
472
|
+
// A chain link: a reference that's kept between renders.
|
|
473
|
+
ref: svg(`<path d="M10 13.5a4.5 4.5 0 0 0 6.4.4l3-3a4.5 4.5 0 0 0-6.4-6.4l-1.2 1.2"/><path d="M14 10.5a4.5 4.5 0 0 0-6.4-.4l-3 3a4.5 4.5 0 0 0 6.4 6.4l1.2-1.2"/>`),
|
|
474
|
+
// Broadcast: provided from somewhere above.
|
|
475
|
+
context: svg(`<circle cx="12" cy="12" r="2" fill="currentColor"/><path d="M8 8a5.7 5.7 0 0 0 0 8M16 8a5.7 5.7 0 0 1 0 8M5 5a10 10 0 0 0 0 14M19 5a10 10 0 0 1 0 14"/>`),
|
|
476
|
+
// A cube: a packaged, reusable piece of logic.
|
|
477
|
+
hook: svg(`<path d="M12 2.5 3.5 7v10l8.5 4.5 8.5-4.5V7z"/><path d="M3.5 7 12 11.5 20.5 7M12 11.5v10"/>`),
|
|
478
|
+
derived: "x",
|
|
479
|
+
function: "\u0192",
|
|
480
|
+
// A bolt, drawn rather than the ⚡ emoji, which renders in its own colours.
|
|
481
|
+
effect: svg(`<path d="M13 2.5 4.5 13.5H12l-1 8 8.5-11H12z"/>`),
|
|
482
|
+
render: "<>",
|
|
483
|
+
// A padlock: fixed, defined outside the component.
|
|
484
|
+
constant: svg(`<rect x="4.5" y="11" width="15" height="10" rx="2"/><path d="M8 11V7.5a4 4 0 0 1 8 0V11"/>`),
|
|
485
|
+
other: "\u2022"
|
|
486
|
+
};
|
|
487
|
+
var KINDS = {
|
|
488
|
+
state: { icon: ICONS.state, label: "state (useState)" },
|
|
489
|
+
ref: { icon: ICONS.ref, label: "ref (useRef)" },
|
|
490
|
+
context: { icon: ICONS.context, label: "context (useContext)" },
|
|
491
|
+
store: { icon: ICONS.context, label: "external store (useSyncExternalStore)", tag: "store" },
|
|
492
|
+
hook: { icon: ICONS.hook, label: "custom hook" },
|
|
493
|
+
derived: { icon: ICONS.derived, label: "computed during render" },
|
|
494
|
+
memo: { icon: ICONS.derived, label: "memoized value (useMemo)", tag: "useMemo" },
|
|
495
|
+
callback: { icon: ICONS.function, label: "memoized callback (useCallback)", tag: "useCallback" },
|
|
496
|
+
function: { icon: ICONS.function, label: "function" },
|
|
497
|
+
effect: { icon: ICONS.effect, label: "effect (useEffect)" },
|
|
498
|
+
render: { icon: ICONS.render, label: "rendered output" },
|
|
499
|
+
constant: { icon: ICONS.constant, label: "constant" }
|
|
500
|
+
};
|
|
501
|
+
var USUAL_HOOK = { state: "useState", ref: "useRef", context: "useContext", store: "useSyncExternalStore", effect: "useEffect" };
|
|
502
|
+
var HOOK_LABELS = {
|
|
503
|
+
useReducer: "state updated by a reducer (useReducer)",
|
|
504
|
+
useTransition: "whether a transition is pending (useTransition)",
|
|
505
|
+
useOptimistic: "state shown optimistically while an action runs (useOptimistic)",
|
|
506
|
+
useActionState: "state of a form action (useActionState)",
|
|
507
|
+
useFormStatus: "status of the enclosing form (useFormStatus)",
|
|
508
|
+
useDeferredValue: "a value that lags behind during urgent updates (useDeferredValue)",
|
|
509
|
+
useId: "a unique id, stable across renders (useId)",
|
|
510
|
+
use: "context (use)",
|
|
511
|
+
useLayoutEffect: "effect that runs before paint (useLayoutEffect)",
|
|
512
|
+
useInsertionEffect: "effect that runs before layout effects (useInsertionEffect)"
|
|
513
|
+
};
|
|
514
|
+
var GROUPS = [
|
|
515
|
+
{ kinds: ["state"], title: "State", hint: "type", about: "useState, plus other hooks that hold a value (tagged): useReducer, useTransition, useOptimistic, useActionState", cls: "state" },
|
|
516
|
+
{ kinds: ["ref"], title: "Refs", hint: "type", about: "useRef, and useImperativeHandle: what a parent's ref to this component sees", cls: "ref" },
|
|
517
|
+
{ kinds: ["context", "store"], title: "Subscriptions", hint: "returns", about: "Values read from outside the component that re-render it when they change: context from a provider above it (useContext, or use, tagged), or an external store (useSyncExternalStore, tagged store)", cls: "context" },
|
|
518
|
+
{ kinds: ["hook"], title: "Hooks", hint: "signature", about: "Custom hooks it calls; ones from a package are tagged with it", cls: "hook" },
|
|
519
|
+
{ kinds: ["derived", "memo"], title: "Computed values", hint: "type", about: "Worked out on each render, or kept by a hook (tagged): useMemo, useDeferredValue, useId", cls: "derived" },
|
|
520
|
+
{ kinds: ["function", "callback"], title: "Functions", hint: "signature", about: "Handlers and helpers (useCallback ones are tagged)", cls: "function" },
|
|
521
|
+
{ kinds: ["effect"], title: "Effects", hint: "timing", about: "useEffect (useLayoutEffect and useInsertionEffect are tagged), each with when it runs: only on mount, after every render, or on mount and when its dependencies change", cls: "effect" },
|
|
522
|
+
{ kinds: ["render"], title: "Renders", hint: "shown when", about: "Parts of the returned JSX", cls: "render" },
|
|
523
|
+
{ kinds: ["constant"], title: "Constants", hint: "type", about: "Defined outside the component", cls: "constant" }
|
|
524
|
+
];
|
|
525
|
+
function renderCard(card, ctx) {
|
|
526
|
+
card = { ...card, outline: flattenSelf(card.outline, ctx.component) };
|
|
527
|
+
const values = liveStateValues(card, ctx.live, ctx.slots);
|
|
528
|
+
return [overview(card), props(card, ctx.live), outline(card, ctx, values), usedIn(card, ctx.live), history(card)].join("");
|
|
529
|
+
}
|
|
530
|
+
function settled(v) {
|
|
531
|
+
if (v === null || typeof v !== "object" || !("status" in v) || !("then" in v)) return v;
|
|
532
|
+
const p = v;
|
|
533
|
+
return p.status === "fulfilled" ? p.value : p.status === "rejected" ? p.reason : v;
|
|
534
|
+
}
|
|
535
|
+
function liveStateValues(card, live, slots) {
|
|
536
|
+
const map = /* @__PURE__ */ new Map();
|
|
537
|
+
const add = (item, v) => {
|
|
538
|
+
if (!item) return;
|
|
539
|
+
if (!map.has(item)) map.set(item, []);
|
|
540
|
+
map.get(item).push(v);
|
|
541
|
+
};
|
|
542
|
+
const base = (name) => name.replace(/\(.*$/, "").trim();
|
|
543
|
+
const hookItem = (hook) => card.outline.find((o) => (o.kind === "hook" || o.kind === "context") && base(o.name) === hook);
|
|
544
|
+
const spread = (parent, key, name, value) => {
|
|
545
|
+
const given = (parent?.children ?? []).filter((c) => c.reads !== void 0);
|
|
546
|
+
const own = (child2, v) => {
|
|
547
|
+
if (child2.kind !== "function") add(child2, { value: v, name: child2.name, fromHook: false });
|
|
548
|
+
};
|
|
549
|
+
const whole = given.find((c) => c.reads === "");
|
|
550
|
+
if (key === "" && whole) return void own(whole, value);
|
|
551
|
+
if (key === "" && given.length && value !== null && typeof value === "object") {
|
|
552
|
+
for (const child2 of given) {
|
|
553
|
+
if (child2.reads in value) own(child2, value[child2.reads]);
|
|
554
|
+
}
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
if (key && whole) return void add(whole, { value, key, name, fromHook: false, part: true });
|
|
558
|
+
const child = key ? given.find((c) => c.reads === key || c.name === key || `[${c.reads}]` === key) : void 0;
|
|
559
|
+
if (child) return void own(child, value);
|
|
560
|
+
if (parent?.kind === "hook") return;
|
|
561
|
+
add(parent, { value, key, name, fromHook: true });
|
|
562
|
+
};
|
|
563
|
+
const addOwned = (s, value) => spread(hookItem(s.owner), s.key, s.name, value);
|
|
564
|
+
const addOwn = (item, v) => !v.dep && item?.children.some((c) => c.reads !== void 0) ? spread(item, "", v.name, v.value) : add(item, v);
|
|
565
|
+
const states = live.state ?? [];
|
|
566
|
+
const contexts = live.contexts ?? [];
|
|
567
|
+
if (slots && slots.states.length === states.length) {
|
|
568
|
+
slots.states.forEach((s, i) => {
|
|
569
|
+
const value = settled(states[i]);
|
|
570
|
+
if (s.owner) return addOwned(s, value);
|
|
571
|
+
add(card.outline.find((o) => o.kind === "state" && base(o.name) === s.name), { value, key: s.key, name: s.name, fromHook: false });
|
|
572
|
+
});
|
|
573
|
+
} else {
|
|
574
|
+
stateNames(card, live).forEach((label, i) => {
|
|
575
|
+
const item = card.outline.find((o) => o.kind === "state" && o.name === label || o.kind === "hook" && label.startsWith(base(o.name)));
|
|
576
|
+
add(item, { value: states[i], key: item?.kind === "state" ? void 0 : label, name: label, fromHook: item?.kind === "hook" });
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
matchOtherHooks(card, live, slots, addOwn, addOwned);
|
|
580
|
+
const distinct = [...new Set(slots?.contexts.map((s) => s.name) ?? [])];
|
|
581
|
+
if (slots && distinct.length === contexts.length) {
|
|
582
|
+
slots.contexts.forEach((s) => {
|
|
583
|
+
const i = distinct.indexOf(s.name);
|
|
584
|
+
if (s.owner) return addOwned(s, contexts[i]);
|
|
585
|
+
const item = card.outline.find((o) => o.kind === "context" && (base(o.name) === s.name || o.name.includes(s.name) || o.from === s.name || (o.type ?? "").includes(s.name)));
|
|
586
|
+
addOwn(item, { value: contexts[i], key: s.key, name: s.name, fromHook: false });
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
const passed = live.props && typeof live.props === "object" ? live.props : {};
|
|
590
|
+
for (const handle of card.outline.filter((o) => o.hook === "useImperativeHandle")) {
|
|
591
|
+
const current = passed[handleRef(handle)]?.current;
|
|
592
|
+
if (current !== void 0) spread(handle, "", handle.name, current);
|
|
593
|
+
}
|
|
594
|
+
const known = new Map(Object.entries(live.props && typeof live.props === "object" ? live.props : {}));
|
|
595
|
+
for (const vals of map.values()) for (const v of vals) if (!v.fromHook && !v.dep) known.set(v.name, v.value);
|
|
596
|
+
map.known = known;
|
|
597
|
+
return map;
|
|
598
|
+
}
|
|
599
|
+
var handleRef = (item) => item.name.match(/^useImperativeHandle\(([\w$]+)\)/)?.[1] ?? "";
|
|
600
|
+
var STATE_HOOKS = /* @__PURE__ */ new Set(["useState", "useReducer", "useOptimistic", "useTransition", "useActionState"]);
|
|
601
|
+
var VALUE_HOOKS = /* @__PURE__ */ new Set(["useSyncExternalStore", "useDeferredValue", "useId"]);
|
|
602
|
+
var EFFECT_HOOKS = /* @__PURE__ */ new Set(["useEffect", "useLayoutEffect", "useInsertionEffect", "useImperativeHandle"]);
|
|
603
|
+
function matchOtherHooks(card, live, slots, add, addOwned) {
|
|
604
|
+
const entries = live.hooks ?? [];
|
|
605
|
+
const list = slots?.hooks ?? [];
|
|
606
|
+
const base = (name) => name.replace(/\(.*$/, "").trim();
|
|
607
|
+
const effects = card.outline.filter((o) => o.kind === "effect");
|
|
608
|
+
let effectIndex = 0;
|
|
609
|
+
const memos = /* @__PURE__ */ new Set();
|
|
610
|
+
const handles = card.outline.filter((o) => o.hook === "useImperativeHandle");
|
|
611
|
+
let handleIndex = 0;
|
|
612
|
+
let slot;
|
|
613
|
+
const addDeps = (item, deps) => {
|
|
614
|
+
if (!item || !slot.deps?.length || !Array.isArray(deps)) return;
|
|
615
|
+
slot.deps.forEach((name, d) => add(item, { value: serialize(deps[d]), name, fromHook: false, dep: true }));
|
|
616
|
+
};
|
|
617
|
+
for (let i = 0; i < list.length && i < entries.length; i++) {
|
|
618
|
+
slot = list[i];
|
|
619
|
+
const { isState, stored } = entries[i];
|
|
620
|
+
const isObj = stored !== null && typeof stored === "object";
|
|
621
|
+
const fits = STATE_HOOKS.has(slot.hook) ? slot.name.endsWith("(internal)") || isState : slot.hook === "useRef" ? isObj && "current" in stored : slot.hook === "useMemo" || slot.hook === "useCallback" ? Array.isArray(stored) && stored.length === 2 : EFFECT_HOOKS.has(slot.hook) ? isObj && ("create" in stored || "deps" in stored) : true;
|
|
622
|
+
if (!fits) return;
|
|
623
|
+
const value = slot.hook === "useRef" ? serialize(stored.current) : slot.hook === "useMemo" ? serialize(stored[0]) : VALUE_HOOKS.has(slot.hook) && !slot.name.endsWith("(internal)") ? serialize(stored) : MISSING;
|
|
624
|
+
if (slot.owner) {
|
|
625
|
+
if (value !== MISSING) addOwned(slot, value);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (VALUE_HOOKS.has(slot.hook) && value !== MISSING) {
|
|
629
|
+
const item = card.outline.find((o) => (o.kind === "store" || o.kind === "state" || o.kind === "derived") && base(o.name) === slot.name);
|
|
630
|
+
add(item, { value, name: slot.name, fromHook: false });
|
|
631
|
+
} else if (slot.hook === "useRef") {
|
|
632
|
+
const item = card.outline.find((o) => o.kind === "ref" && base(o.name) === slot.name);
|
|
633
|
+
add(item, { value: serialize(stored.current), name: slot.name, fromHook: false });
|
|
634
|
+
} else if (slot.hook === "useMemo") {
|
|
635
|
+
const item = card.outline.find((o) => (o.kind === "memo" || o.kind === "derived") && base(o.name) === slot.name && !memos.has(o));
|
|
636
|
+
if (item) memos.add(item);
|
|
637
|
+
add(item, { value: serialize(stored[0]), name: slot.name, fromHook: false });
|
|
638
|
+
addDeps(item, stored[1]);
|
|
639
|
+
} else if (slot.hook === "useCallback") {
|
|
640
|
+
addDeps(card.outline.find((o) => o.kind === "callback" && base(o.name) === slot.name), stored[1]);
|
|
641
|
+
} else if (slot.hook === "useImperativeHandle") {
|
|
642
|
+
const item = handles[handleIndex++];
|
|
643
|
+
addDeps(item, stored.deps);
|
|
644
|
+
} else if (EFFECT_HOOKS.has(slot.hook)) {
|
|
645
|
+
const item = effects[effectIndex++];
|
|
646
|
+
addDeps(item, stored.deps);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function presentValues(all) {
|
|
651
|
+
const deps = all.filter((v) => v.dep);
|
|
652
|
+
const depsBox = deps.length ? valueBox(Object.fromEntries(deps.map((v) => [v.name, v.value])), "Current dependencies") : "";
|
|
653
|
+
return ownValues(all.filter((v) => !v.dep)) + depsBox;
|
|
654
|
+
}
|
|
655
|
+
function ownValues(vals) {
|
|
656
|
+
if (!vals.length) return "";
|
|
657
|
+
if (vals[0].part) return valueBox(Object.fromEntries(vals.map((v) => [v.key, v.value])), "Current");
|
|
658
|
+
if (!vals[0].fromHook) return valueBox(vals[0].value, "Current");
|
|
659
|
+
const returned = vals.filter((v) => v.key !== void 0);
|
|
660
|
+
const internal = vals.filter((v) => v.key === void 0);
|
|
661
|
+
const whole = returned.find((v) => v.key === "");
|
|
662
|
+
const shown = whole ? whole.value : returned.length ? Object.fromEntries(returned.map((v) => [v.key, v.value])) : void 0;
|
|
663
|
+
const blocks = [
|
|
664
|
+
// Read alongside the hook's signature: "Current arguments" → "Current return".
|
|
665
|
+
shown !== void 0 ? valueBox(shown, "Current return") : "",
|
|
666
|
+
...internal.map((v) => valueBox(v.value, `Internal \xB7 ${v.name}`))
|
|
667
|
+
];
|
|
668
|
+
return blocks.join("");
|
|
669
|
+
}
|
|
670
|
+
var MISSING = /* @__PURE__ */ Symbol("missing");
|
|
671
|
+
function hookArguments(item, known) {
|
|
672
|
+
const call = item.name.match(/^[\w$]+\((.*)\)$/s);
|
|
673
|
+
const args = call ? splitTopLevel(call[1]) : [];
|
|
674
|
+
if (!args.length) return "";
|
|
675
|
+
const params = parameterNames(item.type ?? "");
|
|
676
|
+
const results = args.map((arg) => evaluate(arg, known));
|
|
677
|
+
if (!results.some((r) => r.resolved)) return "";
|
|
678
|
+
if (args.length === 1 && !params[0]) return valueBox(results[0].value, "Current arguments");
|
|
679
|
+
const out = {};
|
|
680
|
+
results.forEach((r, i) => {
|
|
681
|
+
out[params[i] ?? (/^[\w$]+$/.test(args[i]) ? args[i] : `argument ${i + 1}`)] = r.value;
|
|
682
|
+
});
|
|
683
|
+
return valueBox(out, "Current arguments");
|
|
684
|
+
}
|
|
685
|
+
function parameterNames(signature) {
|
|
686
|
+
if (!signature.startsWith("(")) return [];
|
|
687
|
+
let depth = 0;
|
|
688
|
+
for (let i = 0; i < signature.length; i++) {
|
|
689
|
+
const c = signature[i];
|
|
690
|
+
if ("([{<".includes(c)) depth++;
|
|
691
|
+
else if (")]}>".includes(c) && signature[i - 1] !== "=") depth--;
|
|
692
|
+
if (depth === 0) return splitTopLevel(signature.slice(1, i)).map((p) => p.replace(/^\.\.\./, "").match(/^[A-Za-z_$][\w$]*/)?.[0]);
|
|
693
|
+
}
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
function compactArg(arg) {
|
|
697
|
+
const a = arg.trim();
|
|
698
|
+
if (a.startsWith("{") && a.endsWith("}")) {
|
|
699
|
+
const keys = splitTopLevel(a.slice(1, -1)).map((p) => p.match(/^(\.\.\.)?\s*["']?[\w$]+/)?.[0].replace(/["']/g, "") ?? "\u2026");
|
|
700
|
+
return keys.length ? `{ ${keys.join(", ")} }` : "{}";
|
|
701
|
+
}
|
|
702
|
+
const fn = a.match(/^(async\s+)?(\([^)]*\)|[\w$]+)\s*=>/);
|
|
703
|
+
if (fn) return `${fn[1] ?? ""}${fn[2]} => \u2026`;
|
|
704
|
+
if (/^(async\s+)?function\b/.test(a)) return "function \u2026";
|
|
705
|
+
return a.length > 40 ? `${a.slice(0, 39)}\u2026` : a;
|
|
706
|
+
}
|
|
707
|
+
function splitTopLevel(text) {
|
|
708
|
+
const parts = [];
|
|
709
|
+
let depth = 0;
|
|
710
|
+
let quote = "";
|
|
711
|
+
let start = 0;
|
|
712
|
+
for (let i = 0; i < text.length; i++) {
|
|
713
|
+
const c = text[i];
|
|
714
|
+
if (quote) {
|
|
715
|
+
if (c === quote && text[i - 1] !== "\\") quote = "";
|
|
716
|
+
} else if (c === '"' || c === "'" || c === "`") quote = c;
|
|
717
|
+
else if ("([{<".includes(c)) depth++;
|
|
718
|
+
else if (")]}>".includes(c) && text[i - 1] !== "=") depth--;
|
|
719
|
+
else if (c === "," && depth === 0) {
|
|
720
|
+
parts.push(text.slice(start, i).trim());
|
|
721
|
+
start = i + 1;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const last = text.slice(start).trim();
|
|
725
|
+
if (last) parts.push(last);
|
|
726
|
+
return parts;
|
|
727
|
+
}
|
|
728
|
+
function flattenSelf(outline2, component) {
|
|
729
|
+
const [only] = outline2;
|
|
730
|
+
if (outline2.length !== 1 || !only.children.length || !only.name.includes(component)) return outline2;
|
|
731
|
+
return only.children;
|
|
732
|
+
}
|
|
733
|
+
function stateNames(card, live) {
|
|
734
|
+
const count = live.state?.length ?? 0;
|
|
735
|
+
const names = [];
|
|
736
|
+
const order = card.outline.filter((i) => i.kind === "state" || i.kind === "hook");
|
|
737
|
+
for (const [i, item] of order.entries()) {
|
|
738
|
+
if (item.kind === "state") {
|
|
739
|
+
names.push(item.name);
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (i === order.length - 1) {
|
|
743
|
+
const hook = item.name.replace(/\(.*$/, "");
|
|
744
|
+
const rest = count - names.length;
|
|
745
|
+
for (let n = 1; n <= rest; n++) names.push(rest > 1 ? `${hook} \xB7 ${n}` : hook);
|
|
746
|
+
}
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
return names.length === count ? names : [];
|
|
750
|
+
}
|
|
751
|
+
function section(id, title, body, opts = {}) {
|
|
752
|
+
return `<details class="sec" data-sec="${id}" ${opts.open === false ? "" : "open"}>
|
|
753
|
+
<summary><span class="sec-title">${title}</span>${opts.count !== void 0 ? `<span class="count">${opts.count}</span>` : ""}${opts.extra ?? ""}</summary>
|
|
754
|
+
<div class="sec-body">${body}</div>
|
|
755
|
+
</details>`;
|
|
756
|
+
}
|
|
757
|
+
function overview(card) {
|
|
758
|
+
if (!card.summary) {
|
|
759
|
+
return section("overview", "Overview", `<p class="summary pending">The AI is writing the overview and explanations. Meanwhile, everything below is read straight from the code.</p>`);
|
|
760
|
+
}
|
|
761
|
+
return section("overview", "Overview", `<p class="summary">${inline(card.summary)}</p>`);
|
|
762
|
+
}
|
|
763
|
+
function propRow(name, type, description, value, hasValue, note = "") {
|
|
764
|
+
return `<div class="prop" data-def="${esc(name.replace(/\?$/, ""))}">
|
|
765
|
+
<div class="prop-head">
|
|
766
|
+
<code class="prop-name">${esc(name)}</code>
|
|
767
|
+
${type ? `<code class="prop-type" title="${esc(type)}">${esc(type)}</code>` : ""}
|
|
768
|
+
</div>
|
|
769
|
+
<div class="prop-body">
|
|
770
|
+
${description ? `<div class="prop-desc">${inline(description)}</div>` : ""}
|
|
771
|
+
${note}
|
|
772
|
+
${hasValue ? valueBox(value, "Current") : ""}
|
|
773
|
+
</div>
|
|
774
|
+
</div>`;
|
|
775
|
+
}
|
|
776
|
+
function props(card, live) {
|
|
777
|
+
const liveProps = live.props && typeof live.props === "object" ? live.props : {};
|
|
778
|
+
const exposes = /* @__PURE__ */ new Map();
|
|
779
|
+
for (const o of card.outline) if (o.hook === "useImperativeHandle") exposes.set(handleRef(o), o.children.map((c) => c.name));
|
|
780
|
+
const rows = card.props.map((p) => {
|
|
781
|
+
const names = exposes.get(p.name);
|
|
782
|
+
const note = names?.length ? `<div class="prop-desc">Exposes ${names.map((n) => `<code>${esc(n)}</code>`).join(", ")} to the parent through useImperativeHandle.</div>` : "";
|
|
783
|
+
return propRow(p.name + (p.optional ? "?" : ""), p.type, p.description, liveProps[p.name], p.name in liveProps && !names, note);
|
|
784
|
+
});
|
|
785
|
+
const described = new Set(card.props.map((p) => p.name));
|
|
786
|
+
for (const [k, v] of Object.entries(liveProps)) {
|
|
787
|
+
if (!described.has(k)) rows.push(propRow(k, "", "", v, true));
|
|
788
|
+
}
|
|
789
|
+
if (!rows.length) return "";
|
|
790
|
+
return section("props", "Props", rows.join(""), { count: rows.length });
|
|
791
|
+
}
|
|
792
|
+
function outline(card, ctx, values) {
|
|
793
|
+
if (!card.outline.length) return "";
|
|
794
|
+
const count = (items) => items.reduce((n, i) => n + 1 + count(i.children ?? []), 0);
|
|
795
|
+
const total = count(card.outline);
|
|
796
|
+
const known = new Set(GROUPS.flatMap((g) => g.kinds));
|
|
797
|
+
const groups = [...GROUPS, { kinds: [], title: "Other", hint: "", about: "", cls: "other" }].map((g) => ({
|
|
798
|
+
...g,
|
|
799
|
+
items: card.outline.filter((i) => g.cls === "other" ? !known.has(i.kind) : g.kinds.includes(i.kind))
|
|
800
|
+
}));
|
|
801
|
+
const body = groups.filter((g) => g.items.length).map(
|
|
802
|
+
(g) => `<details class="ol-group" open>
|
|
803
|
+
<summary class="ol-group-head" title="${esc(g.about)}">
|
|
804
|
+
<span class="ol-icon k-${g.cls}">${ICONS[g.cls]}</span>
|
|
805
|
+
<span class="ol-group-title">${g.title}</span>
|
|
806
|
+
<span class="ol-group-hint">${g.hint ? `<span class="ol-hint-label">${esc(g.hint)}</span>` : ""}</span>
|
|
807
|
+
<span class="count">${g.items.length}</span>
|
|
808
|
+
</summary>
|
|
809
|
+
<ul class="outline">${g.items.map((item) => outlineNode(item, ctx, values)).join("")}</ul>
|
|
810
|
+
</details>`
|
|
811
|
+
).join("");
|
|
812
|
+
return section("outline", "Outline", body, {
|
|
813
|
+
count: total,
|
|
814
|
+
extra: `<button class="link expand-all" data-expand-all title="Expand or collapse every explanation">expand all</button>`
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
function fact(label, cls, html) {
|
|
818
|
+
return `<div class="fact fact-${cls}"><span class="val-label">${label}</span><span class="fact-val">${html}</span></div>`;
|
|
819
|
+
}
|
|
820
|
+
function effectDeps(when) {
|
|
821
|
+
const list = when.trim().match(/^\[(.*)\]$/s);
|
|
822
|
+
return list ? splitTopLevel(list[1]) : null;
|
|
823
|
+
}
|
|
824
|
+
function effectTiming(when, hook = "") {
|
|
825
|
+
const w = when.trim();
|
|
826
|
+
const deps = effectDeps(w);
|
|
827
|
+
const timing = deps?.length === 0 || /^(on |only on )?mount|once|mount only/i.test(w) ? "only on mount" : /every render|no dep/i.test(w) ? "after every render" : deps?.length || !w ? "on mount and when dependencies change" : w;
|
|
828
|
+
const phase = hook === "useLayoutEffect" ? "before paint" : hook === "useInsertionEffect" ? "before layout" : "";
|
|
829
|
+
return [timing, phase].filter(Boolean).join(" \xB7 ");
|
|
830
|
+
}
|
|
831
|
+
function callParts(name) {
|
|
832
|
+
const call = name.match(/^(use[\w$]*)\((.*)\)$/s);
|
|
833
|
+
return call ? { shown: call[1], args: call[2].trim() } : { shown: name, args: "" };
|
|
834
|
+
}
|
|
835
|
+
function outlineNode(item, ctx, values, parent) {
|
|
836
|
+
const { shown, args } = callParts(item.name);
|
|
837
|
+
const fromCall = item.reads !== void 0 && parent ? `${callParts(parent.name).shown}${parent.kind === "state" ? "" : "()"}` : "";
|
|
838
|
+
const base = KINDS[item.kind] ?? { icon: ICONS.other, label: item.kind };
|
|
839
|
+
const kind = fromCall ? parent?.kind === "state" ? { ...base, label: `updates ${parent.name}` } : { ...base, label: `${item.kind === "derived" ? "value" : "function"} from ${fromCall}`, tag: void 0 } : item.hook === "useImperativeHandle" ? { ...base, label: "what a parent's ref sees (useImperativeHandle)" } : item.hook && HOOK_LABELS[item.hook] ? { ...base, label: HOOK_LABELS[item.hook] } : item.kind === "hook" && item.from ? { ...base, label: `hook from ${item.from}` } : base;
|
|
840
|
+
const tagText = kind.tag ?? (item.hook && item.hook !== USUAL_HOOK[item.kind] ? item.hook : "");
|
|
841
|
+
const tag = tagText && !shown.startsWith(tagText) ? tagText : "";
|
|
842
|
+
const children = item.children ?? [];
|
|
843
|
+
const facts = { type: item.type ?? "", initial: item.initial ?? "", when: item.when ?? "" };
|
|
844
|
+
const liveArgs = item.kind === "hook" ? hookArguments(item, values.known ?? /* @__PURE__ */ new Map()) : "";
|
|
845
|
+
const block = liveArgs + presentValues(values.get(item) ?? []);
|
|
846
|
+
const typePill = facts.type ? `<span class="ol-type" title="${esc(facts.type)}">${esc(facts.type)}</span>` : "";
|
|
847
|
+
const timed = item.kind === "effect" || item.kind === "memo" || item.kind === "callback" || item.hook === "useImperativeHandle";
|
|
848
|
+
const when = !timed ? facts.when : item.kind === "effect" ? effectTiming(facts.when, item.hook) : effectTiming(facts.when).replace("after every render", "on every render");
|
|
849
|
+
const whenChip = when ? `<span class="ol-when" title="${esc(facts.when)}">${esc(when)}</span>` : "";
|
|
850
|
+
const constant = item.kind === "constant" ? ctx.slots?.constants?.[item.name] : void 0;
|
|
851
|
+
const returns = item.returns;
|
|
852
|
+
const factRows = [
|
|
853
|
+
facts.initial && fact("Initial", "initial", `<code>${esc(facts.initial)}</code>`),
|
|
854
|
+
constant && fact("Value", "value", `<code>${esc(constant)}</code>`),
|
|
855
|
+
returns && (item.kind === "effect" ? fact("Cleanup", "cleanup", inline(returns)) : fact("Returns", "returns", inline(returns))),
|
|
856
|
+
// An effect's dependencies, when there are no live ones to show them with.
|
|
857
|
+
timed && !block && effectDeps(facts.when)?.length && fact("Dependencies", "deps", effectDeps(facts.when).map((d) => `<code>${esc(d)}</code>`).join(", ")),
|
|
858
|
+
// A hook's arguments as written, when there are no live ones to show.
|
|
859
|
+
item.kind === "hook" && args && !liveArgs && fact("Arguments", "args", `<code title="${esc(args)}">${esc(splitTopLevel(args).map(compactArg).join(", "))}</code>`)
|
|
860
|
+
].filter(Boolean);
|
|
861
|
+
const transient = item.kind === "derived" && !fromCall && !block ? `<div class="ol-note">Worked out on every render and not stored by React, so there's no live value to show.</div>` : "";
|
|
862
|
+
return `<li class="ol-node kc-${item.kind}" data-def="${esc(shown)}">
|
|
863
|
+
<div class="ol-row" title="Click to explain">
|
|
864
|
+
<span class="ol-icon k-${item.kind}" title="${esc(kind.label)}">${kind.icon}</span>
|
|
865
|
+
<span class="ol-name" title="${esc(item.name)}">${esc(shown)}</span>
|
|
866
|
+
${tag ? `<span class="ol-tag">${esc(tag)}</span>` : ""}
|
|
867
|
+
${item.from && !shown.includes(item.from) ? `<span class="ol-tag ol-from" title="${item.kind === "hook" ? "From the package" : "Reads"} ${esc(item.from)}">${esc(item.from)}</span>` : ""}
|
|
868
|
+
${typePill}${whenChip}
|
|
869
|
+
<a class="src ol-goto" data-file="${esc(ctx.file)}" data-line="${item.line}" data-col="1" title="Open line ${item.line} in editor">${item.line}</a>
|
|
870
|
+
</div>
|
|
871
|
+
<div class="ol-expl">
|
|
872
|
+
${inline(item.explanation)}
|
|
873
|
+
${factRows.length ? `<div class="ol-facts">${factRows.join("")}</div>` : ""}
|
|
874
|
+
${block ? `<div class="ol-now">${block}</div>` : ""}${transient}
|
|
875
|
+
</div>
|
|
876
|
+
${children.length ? `<ul class="outline">${children.map((c) => outlineNode(c, ctx, values, item)).join("")}</ul>` : ""}
|
|
877
|
+
</li>`;
|
|
878
|
+
}
|
|
879
|
+
function usedIn(card, live) {
|
|
880
|
+
if (!card.usedIn.length) return "";
|
|
881
|
+
const current = thisCallSite(card, live);
|
|
882
|
+
const body = card.usedIn.map((u) => {
|
|
883
|
+
const file = u.ref.replace(/:\d+(:\d+)?$/, "");
|
|
884
|
+
const line = u.ref.match(/:(\d+)/)?.[1] ?? "1";
|
|
885
|
+
const props2 = (u.props ?? []).map((p) => {
|
|
886
|
+
const shown = p.name === "children" && p.value.startsWith("<") ? `${p.value.match(/^<[\w.]+/)?.[0] ?? "<\u2026"} \u2026>` : compactArg(p.value);
|
|
887
|
+
const value = p.line ? `<a class="src prop-type used-var" data-file="${esc(file)}" data-line="${p.line}" data-col="1" title="${esc(p.value)}: declared on line ${p.line}">${esc(shown)} \u2197</a>` : `<code class="prop-type" title="${esc(p.value)}">${esc(shown)}</code>`;
|
|
888
|
+
return `<code class="prop-name">${esc(p.name === "..." ? "{...}" : p.name)}</code><span class="used-val">${value}</span>`;
|
|
889
|
+
});
|
|
890
|
+
return `<div class="used${u === current ? " current" : ""}">
|
|
891
|
+
<div class="used-head">
|
|
892
|
+
<a class="src ref" data-file="${esc(file)}" data-line="${line}" data-col="1" title="Open ${esc(u.ref)} in editor">${esc(file.split("/").pop())}:${line}</a>
|
|
893
|
+
${u.parent ? `<span class="used-in">in <code><${esc(u.parent)}></code></span>` : ""}
|
|
894
|
+
${u === current ? `<span class="used-this" title="The instance you're inspecting was rendered here">this one</span>` : ""}
|
|
895
|
+
</div>
|
|
896
|
+
${props2.length ? `<div class="used-props">${props2.join("")}</div>` : ""}
|
|
897
|
+
</div>`;
|
|
898
|
+
}).join("");
|
|
899
|
+
return section("used", "Used in", `<div class="used-list">${body}</div>`, { count: card.usedIn.length });
|
|
900
|
+
}
|
|
901
|
+
function thisCallSite(card, live) {
|
|
902
|
+
if (!live.owner) return void 0;
|
|
903
|
+
const candidates = card.usedIn.filter((u) => u.parent === live.owner);
|
|
904
|
+
if (candidates.length <= 1) return candidates[0];
|
|
905
|
+
const passed = live.props && typeof live.props === "object" ? live.props : {};
|
|
906
|
+
const matches = candidates.filter(
|
|
907
|
+
(u) => (u.props ?? []).every((p) => {
|
|
908
|
+
if (p.name === "..." || p.name === "children" || p.name === "key" || p.name === "ref") return true;
|
|
909
|
+
const r = evaluate(p.value, /* @__PURE__ */ new Map());
|
|
910
|
+
if (!r.resolved || typeof r.value === "string" && r.value.startsWith("[Code ")) return true;
|
|
911
|
+
return JSON.stringify(r.value) === JSON.stringify(passed[p.name]);
|
|
912
|
+
})
|
|
913
|
+
);
|
|
914
|
+
return matches.length === 1 ? matches[0] : void 0;
|
|
915
|
+
}
|
|
916
|
+
function history(card) {
|
|
917
|
+
if (!card.history.length) return "";
|
|
918
|
+
const body = `<ol class="timeline">${card.history.map(
|
|
919
|
+
(h) => `<li>
|
|
920
|
+
<div class="tl-meta"><code>${esc(h.commit)}</code>${h.date ? ` \xB7 ${esc(h.date)}` : ""}${h.author ? ` \xB7 ${esc(h.author)}` : ""}</div>
|
|
921
|
+
<div class="tl-why">${inline(h.why)}</div>
|
|
922
|
+
</li>`
|
|
923
|
+
).join("")}</ol>`;
|
|
924
|
+
return section("history", "History", body, { count: card.history.length, open: false });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// src/client/types.ts
|
|
928
|
+
var TARGETS = ".prop-type, .ol-type, code";
|
|
929
|
+
function linkTypes(root, types) {
|
|
930
|
+
if (!types.size) return;
|
|
931
|
+
const re = new RegExp(`\\b(${[...types.keys()].map((n) => n.replace(/[$]/g, "\\$")).join("|")})\\b`, "g");
|
|
932
|
+
for (const el of root.querySelectorAll(TARGETS)) {
|
|
933
|
+
if (el.closest("a, pre, .tp-code")) continue;
|
|
934
|
+
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
|
|
935
|
+
const nodes = [];
|
|
936
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) if (!n.parentElement?.closest(".type-ref")) nodes.push(n);
|
|
937
|
+
for (const node of nodes) {
|
|
938
|
+
const text = node.data;
|
|
939
|
+
re.lastIndex = 0;
|
|
940
|
+
if (!re.test(text)) continue;
|
|
941
|
+
const span2 = document.createElement("span");
|
|
942
|
+
span2.innerHTML = esc(text).replace(re, (name) => typeLink(types.get(name)));
|
|
943
|
+
node.replaceWith(...span2.childNodes);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
function typeLink(t) {
|
|
948
|
+
return `<a class="src type-ref" data-type="${esc(t.name)}" data-file="${esc(t.file)}" data-line="${t.line}" data-col="1">${esc(t.name)}</a>`;
|
|
949
|
+
}
|
|
950
|
+
function typePopover(t, types) {
|
|
951
|
+
const re = new RegExp(`\\b(${[...types.keys()].join("|")})\\b`, "g");
|
|
952
|
+
const code2 = esc(t.code).replace(re, (name) => name === t.name ? name : typeLink(types.get(name)));
|
|
953
|
+
return `<div class="tp-head">
|
|
954
|
+
<span class="tp-kind">${esc(t.kind)}</span><span class="tp-name">${esc(t.name)}</span>
|
|
955
|
+
<a class="src tp-file" data-file="${esc(t.file)}" data-line="${t.line}" data-col="1" title="Open in editor">${esc(t.file.split("/").pop())}:${t.line} \u2197</a>
|
|
956
|
+
</div>
|
|
957
|
+
<pre class="tp-code">${code2}</pre>`;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// src/client/symbols.ts
|
|
961
|
+
var REF = /^([A-Za-z_$][\w$]*)(?:\.[\w$]+)*(?:\(.*\))?$/s;
|
|
962
|
+
function definitions(root) {
|
|
963
|
+
const defs = /* @__PURE__ */ new Map();
|
|
964
|
+
for (const el of root.querySelectorAll("[data-def]")) {
|
|
965
|
+
if (!defs.has(el.dataset.def)) defs.set(el.dataset.def, el);
|
|
966
|
+
}
|
|
967
|
+
return defs;
|
|
968
|
+
}
|
|
969
|
+
function findDefinition(root, name) {
|
|
970
|
+
return definitions(root).get(name) ?? null;
|
|
971
|
+
}
|
|
972
|
+
var SKIP = "a, pre, .ol-row, .prop-head, .tp-head, .val, .sym-ref, .used-list";
|
|
973
|
+
var CODE_LIKE = /^(?:[a-z][a-z0-9]*[A-Z][\w$]*|[A-Za-z$][\w$]*[_$][\w$]*)$/;
|
|
974
|
+
var kindOf = (def) => [...def.classList].find((c) => c.startsWith("kc-")) ?? "kc-prop";
|
|
975
|
+
function linkSymbols(root) {
|
|
976
|
+
const defs = definitions(root);
|
|
977
|
+
if (!defs.size) return;
|
|
978
|
+
for (const code2 of root.querySelectorAll("code:not(.sym-ref)")) {
|
|
979
|
+
if (code2.closest(SKIP) || code2.querySelector("a")) continue;
|
|
980
|
+
const name = code2.textContent.trim().match(REF)?.[1];
|
|
981
|
+
const def = name ? defs.get(name) : void 0;
|
|
982
|
+
if (!def || code2.closest("[data-def]") === def) continue;
|
|
983
|
+
code2.classList.add("sym-ref", kindOf(def));
|
|
984
|
+
code2.dataset.sym = name;
|
|
985
|
+
}
|
|
986
|
+
const prose = [...defs.keys()].filter((n) => CODE_LIKE.test(n));
|
|
987
|
+
if (!prose.length) return;
|
|
988
|
+
const re = new RegExp(`(?<![\\w$.])(${prose.map((n) => n.replace(/\$/g, "\\$")).join("|")})(?![\\w$])`, "g");
|
|
989
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
990
|
+
const nodes = [];
|
|
991
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
992
|
+
const parent = n.parentElement;
|
|
993
|
+
if (parent && !parent.closest(`${SKIP}, code, summary, button, .steps`)) nodes.push(n);
|
|
994
|
+
}
|
|
995
|
+
for (const node of nodes) {
|
|
996
|
+
re.lastIndex = 0;
|
|
997
|
+
if (!re.test(node.data)) continue;
|
|
998
|
+
const own = node.parentElement.closest("[data-def]");
|
|
999
|
+
const span2 = document.createElement("span");
|
|
1000
|
+
span2.innerHTML = esc(node.data).replace(re, (name) => {
|
|
1001
|
+
const def = defs.get(name);
|
|
1002
|
+
return def === own ? name : `<span class="sym-ref ${kindOf(def)}" data-sym="${esc(name)}">${name}</span>`;
|
|
1003
|
+
});
|
|
1004
|
+
node.replaceWith(...span2.childNodes);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function symbolPopover(def) {
|
|
1008
|
+
if (def.classList.contains("prop")) {
|
|
1009
|
+
const type = def.querySelector(".prop-type")?.textContent ?? "";
|
|
1010
|
+
const desc = def.querySelector(".prop-desc")?.innerHTML ?? "";
|
|
1011
|
+
const value = def.querySelector(".prop-body > .val")?.outerHTML ?? "";
|
|
1012
|
+
return `<div class="tp-head">
|
|
1013
|
+
<span class="tp-kind">prop</span><span class="tp-name">${esc(def.dataset.def)}</span>
|
|
1014
|
+
${type ? `<span class="ol-type sp-type">${esc(type)}</span>` : ""}
|
|
1015
|
+
</div>
|
|
1016
|
+
<div class="sp-body">${desc}${value}</div>`;
|
|
1017
|
+
}
|
|
1018
|
+
const row = def.querySelector(":scope > .ol-row");
|
|
1019
|
+
const icon = row.querySelector(".ol-icon")?.outerHTML ?? "";
|
|
1020
|
+
const pill = row.querySelector(".ol-type, .ol-when");
|
|
1021
|
+
const goto = row.querySelector(".ol-goto");
|
|
1022
|
+
const expl = def.querySelector(":scope > .ol-expl").cloneNode(true);
|
|
1023
|
+
expl.querySelector(".ol-note")?.remove();
|
|
1024
|
+
const boxes = [...expl.querySelectorAll(".ol-facts, .ol-now")].map((b) => (b.remove(), b.outerHTML)).join("");
|
|
1025
|
+
return `<div class="sp ${[...def.classList].filter((c) => c.startsWith("kc-")).join(" ")}">
|
|
1026
|
+
<div class="tp-head">
|
|
1027
|
+
${icon}<span class="tp-name">${esc(def.dataset.def)}</span>
|
|
1028
|
+
${pill ? `<span class="ol-type sp-type">${esc(pill.textContent)}</span>` : ""}
|
|
1029
|
+
${goto ? `<a class="src tp-file" data-file="${esc(goto.dataset.file)}" data-line="${goto.dataset.line}" data-col="1" title="Open in editor">L${goto.dataset.line} \u2197</a>` : ""}
|
|
1030
|
+
</div>
|
|
1031
|
+
<div class="sp-body">${expl.innerHTML.trim()}${boxes}</div>
|
|
1032
|
+
</div>`;
|
|
1033
|
+
}
|
|
1034
|
+
function revealDefinition(def) {
|
|
1035
|
+
for (let d = def.closest("details"); d; d = d.parentElement.closest("details")) d.open = true;
|
|
1036
|
+
if (def.classList.contains("ol-node")) def.classList.add("open");
|
|
1037
|
+
def.scrollIntoView({ block: "center", behavior: matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" });
|
|
1038
|
+
def.classList.remove("flash");
|
|
1039
|
+
void def.offsetWidth;
|
|
1040
|
+
def.classList.add("flash");
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// src/client/styles.ts
|
|
1044
|
+
var STYLES = (
|
|
1045
|
+
/* css */
|
|
1046
|
+
`
|
|
1047
|
+
:host { all: initial; }
|
|
1048
|
+
* { box-sizing: border-box; }
|
|
1049
|
+
.box, .tip, .launcher, .panel, .type-pop {
|
|
1050
|
+
/* Monotone: neutral greys with a muted violet-grey accent, so the outline's kind colours are the only real colour. */
|
|
1051
|
+
--accent: #5d5873;
|
|
1052
|
+
--bg: #ffffff; --fg: #33303d; --muted: #6f6c7c; --line: #e6e6e9; --soft: #f5f5f6; --code: #efeff1;
|
|
1053
|
+
font: 13px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
1054
|
+
color: var(--fg);
|
|
1055
|
+
}
|
|
1056
|
+
@media (prefers-color-scheme: dark) {
|
|
1057
|
+
.box, .tip, .launcher, .panel, .type-pop {
|
|
1058
|
+
--accent: #b1adc4;
|
|
1059
|
+
--bg: #18181b; --fg: #dddbe6; --muted: #9a97a8; --line: #2d2d31; --soft: #212124; --code: #28282c;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
[hidden] { display: none !important; }
|
|
1063
|
+
|
|
1064
|
+
.box {
|
|
1065
|
+
position: fixed; top: 0; left: 0;
|
|
1066
|
+
border: 2px solid var(--accent); border-radius: 4px;
|
|
1067
|
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
|
|
1068
|
+
transition: transform 60ms ease-out, width 60ms ease-out, height 60ms ease-out;
|
|
1069
|
+
}
|
|
1070
|
+
.box.selected { background: none; border-style: dashed; }
|
|
1071
|
+
|
|
1072
|
+
.tip {
|
|
1073
|
+
position: fixed; top: 0; left: 0; max-width: min(560px, calc(100vw - 8px));
|
|
1074
|
+
display: flex; gap: 8px; align-items: baseline;
|
|
1075
|
+
padding: 4px 8px; border-radius: 6px;
|
|
1076
|
+
background: var(--fg); color: var(--bg); white-space: nowrap; overflow: hidden;
|
|
1077
|
+
box-shadow: 0 4px 16px rgb(0 0 0 / .25);
|
|
1078
|
+
}
|
|
1079
|
+
.tip .name { font: 600 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1080
|
+
.tip .path { font-size: 11px; opacity: .75; overflow: hidden; text-overflow: ellipsis; }
|
|
1081
|
+
|
|
1082
|
+
.launcher {
|
|
1083
|
+
pointer-events: auto; position: fixed; touch-action: none; user-select: none;
|
|
1084
|
+
display: flex; align-items: center; gap: 6px;
|
|
1085
|
+
padding: 4px 8px; border: 1px solid var(--line); border-radius: 999px;
|
|
1086
|
+
background: var(--bg); color: var(--muted); cursor: pointer;
|
|
1087
|
+
box-shadow: 0 2px 10px rgb(0 0 0 / .12); opacity: .75; transition: opacity .15s;
|
|
1088
|
+
}
|
|
1089
|
+
/* Drag it anywhere; it snaps to the nearest corner (remembered per browser). */
|
|
1090
|
+
.launcher[data-corner="br"] { right: 12px; bottom: 12px; }
|
|
1091
|
+
.launcher[data-corner="bl"] { left: 12px; bottom: 12px; }
|
|
1092
|
+
.launcher[data-corner="tr"] { right: 12px; top: 12px; }
|
|
1093
|
+
.launcher[data-corner="tl"] { left: 12px; top: 12px; }
|
|
1094
|
+
.launcher.dragging { cursor: grabbing; opacity: 1; box-shadow: 0 8px 24px rgb(0 0 0 / .25); }
|
|
1095
|
+
.launcher:hover, .launcher.active { opacity: 1; color: var(--fg); }
|
|
1096
|
+
.launcher .hat { width: 18px; height: 18px; flex-shrink: 0; transform-origin: 30% 80%; transition: transform .25s cubic-bezier(.3, 1.6, .5, 1); }
|
|
1097
|
+
.launcher .hat .band { fill: var(--bg); }
|
|
1098
|
+
.launcher:hover .hat { transform: rotate(-14deg) translateY(-1px); } /* tips the hat */
|
|
1099
|
+
.launcher.active .hat { color: var(--accent); animation: sleuth 2.2s ease-in-out infinite; }
|
|
1100
|
+
@keyframes sleuth { 0%, 100% { transform: rotate(0); } 30% { transform: rotate(-8deg) translateX(-1px); } 60% { transform: rotate(6deg) translateX(1px); } }
|
|
1101
|
+
@media (prefers-reduced-motion: reduce) { .launcher .hat { transition: none; animation: none !important; } }
|
|
1102
|
+
/* While inspecting, the label turns into a prompt and the key hint shows how to stop. */
|
|
1103
|
+
.launcher .on, .launcher.active .off { display: none; }
|
|
1104
|
+
.launcher.active .on { display: inline; }
|
|
1105
|
+
.launcher kbd { font: 11px ui-monospace, monospace; padding: 0 5px; border: 1px solid var(--line); border-radius: 4px; background: var(--soft); }
|
|
1106
|
+
|
|
1107
|
+
.panel {
|
|
1108
|
+
pointer-events: auto; position: fixed; top: 0; right: 0; bottom: 0;
|
|
1109
|
+
width: min(460px, 100vw); display: flex; flex-direction: column;
|
|
1110
|
+
background: var(--bg); border-left: 1px solid var(--line);
|
|
1111
|
+
box-shadow: -8px 0 32px rgb(0 0 0 / .12);
|
|
1112
|
+
}
|
|
1113
|
+
header { position: relative; padding: 12px 44px 12px 16px; border-bottom: 1px solid var(--line); }
|
|
1114
|
+
.title { display: flex; align-items: center; gap: 4px; min-width: 0; }
|
|
1115
|
+
.title-link { display: inline-flex; align-items: center; gap: 7px; min-width: 0; padding: 3px 10px; border-radius: 7px; border: 1px solid var(--line); background: var(--soft); text-decoration: none; cursor: pointer; transition: background .12s, border-color .12s; }
|
|
1116
|
+
.title-link:hover { background: var(--code); border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); }
|
|
1117
|
+
.title-goto { font-size: 12px; color: var(--muted); }
|
|
1118
|
+
.title-link:hover .title-goto { color: var(--accent); }
|
|
1119
|
+
header .close { position: absolute; top: 8px; right: 8px; }
|
|
1120
|
+
h1 { margin: 0; font: 600 16px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: var(--fg); overflow-wrap: anywhere; }
|
|
1121
|
+
button { font: inherit; color: inherit; }
|
|
1122
|
+
.icon { width: 28px; height: 28px; border: 0; border-radius: 6px; background: none; color: var(--muted); cursor: pointer; font-size: 15px; }
|
|
1123
|
+
.icon:hover { background: var(--soft); color: var(--fg); }
|
|
1124
|
+
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 1px; margin: -2px 0 4px -4px; font: 11px ui-monospace, monospace; color: var(--muted); }
|
|
1125
|
+
.crumbs button { border: 0; background: none; padding: 1px 4px; border-radius: 4px; cursor: pointer; color: var(--muted); font: inherit; }
|
|
1126
|
+
.crumbs button:hover { background: var(--soft); color: var(--fg); }
|
|
1127
|
+
.crumbs .sep { opacity: .45; }
|
|
1128
|
+
|
|
1129
|
+
.meta-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 0; margin-top: 4px; font-size: 11.5px; color: var(--muted); }
|
|
1130
|
+
.meta-row:not(:has(> :not([hidden]))) { display: none; }
|
|
1131
|
+
.meta-row > :not([hidden]) ~ :not([hidden])::before { content: "\xB7"; margin: 0 7px; opacity: .6; }
|
|
1132
|
+
.status, .gitinfo { cursor: default; white-space: nowrap; }
|
|
1133
|
+
.gitinfo .tag { margin-left: 4px; }
|
|
1134
|
+
|
|
1135
|
+
.body { flex: 1; overflow-y: auto; padding: 4px 16px 16px; }
|
|
1136
|
+
.progress { display: flex; align-items: center; gap: 10px; margin: 14px 0 6px; color: var(--muted); }
|
|
1137
|
+
/* Busy indicator: a magnifying glass searching about. */
|
|
1138
|
+
.spinner { position: relative; flex-shrink: 0; width: 16px; height: 16px; animation: search 1.6s ease-in-out infinite; }
|
|
1139
|
+
.spinner::before { content: ""; position: absolute; left: 0; top: 0; width: 11px; height: 11px; border: 2px solid var(--accent); border-radius: 50%; }
|
|
1140
|
+
.spinner::after { content: ""; position: absolute; left: 10px; top: 9px; width: 2.5px; height: 7px; border-radius: 1px; background: var(--accent); transform: rotate(-45deg); transform-origin: top center; }
|
|
1141
|
+
@keyframes search { 0%, 100% { transform: translate(0, 0); } 25% { transform: translate(3px, -2px); } 50% { transform: translate(0, -4px); } 75% { transform: translate(-3px, -1px); } }
|
|
1142
|
+
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
|
|
1143
|
+
.steps { list-style: none; margin: 0; padding: 0 0 0 26px; font: 12px ui-monospace, monospace; color: var(--muted); }
|
|
1144
|
+
.steps li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; opacity: .55; }
|
|
1145
|
+
.steps li:last-child { opacity: 1; }
|
|
1146
|
+
|
|
1147
|
+
.md h2, .md h3, .md h4 { margin: 18px 0 6px; font-size: 13px; font-weight: 650; text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
|
|
1148
|
+
.md h2:first-child { margin-top: 14px; }
|
|
1149
|
+
.md p, .md ul, .md ol { margin: 0 0 8px; }
|
|
1150
|
+
.md ul, .md ol { padding-left: 20px; }
|
|
1151
|
+
.md li { margin: 2px 0; }
|
|
1152
|
+
.md code, .md pre { font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1153
|
+
.md code { padding: 1px 4px; border-radius: 4px; background: var(--code); overflow-wrap: anywhere; }
|
|
1154
|
+
.md pre { padding: 10px; border-radius: 6px; background: var(--code); overflow-x: auto; }
|
|
1155
|
+
.md pre code { padding: 0; background: none; }
|
|
1156
|
+
/* Web links anywhere in the panel; without this they'd get the browser's dark blue, unreadable in dark mode. */
|
|
1157
|
+
a[href] { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
|
|
1158
|
+
a[href]:hover { color: var(--fg); }
|
|
1159
|
+
.banner a[href], .error a[href] { color: inherit; font-weight: 600; }
|
|
1160
|
+
.md a { color: var(--accent); }
|
|
1161
|
+
.md a.src { cursor: pointer; text-decoration: none; }
|
|
1162
|
+
.md a.src code { color: var(--accent); }
|
|
1163
|
+
.md a.src:hover code { text-decoration: underline; }
|
|
1164
|
+
.meta { margin-top: 12px; font-size: 11px; color: var(--muted); }
|
|
1165
|
+
.link { border: 0; padding: 0; background: none; color: var(--accent); cursor: pointer; font-size: inherit; }
|
|
1166
|
+
.error { margin: 14px 0; padding: 10px 12px; border-radius: 6px; background: color-mix(in srgb, #e5484d 12%, var(--bg)); color: #c62a2f; overflow-wrap: anywhere; }
|
|
1167
|
+
@media (prefers-color-scheme: dark) {
|
|
1168
|
+
.error { color: #ff9592; }
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
/* ---------- explanation card ---------- */
|
|
1172
|
+
.card code { font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; padding: 0 3px; border-radius: 3px; background: var(--code); }
|
|
1173
|
+
a.ref { font: 11px ui-monospace, monospace; color: var(--muted); text-decoration: none; cursor: pointer; white-space: nowrap; }
|
|
1174
|
+
a.ref:hover { color: var(--accent); text-decoration: underline; }
|
|
1175
|
+
|
|
1176
|
+
.sec { border-top: 1px solid var(--line); }
|
|
1177
|
+
.sec:first-child { border-top: 0; }
|
|
1178
|
+
.sec > summary { display: flex; align-items: center; gap: 6px; padding: 12px 0 8px; cursor: pointer; list-style: none; user-select: none; }
|
|
1179
|
+
.sec > summary::-webkit-details-marker { display: none; }
|
|
1180
|
+
.sec > summary::before { content: "\u25B8"; font-size: 10px; color: var(--muted); transition: transform .15s; }
|
|
1181
|
+
.sec[open] > summary::before { transform: rotate(90deg); }
|
|
1182
|
+
.sec-title { font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
1183
|
+
.count { min-width: 18px; padding: 0 5px; border-radius: 999px; background: var(--soft); font-size: 11px; text-align: center; color: var(--muted); }
|
|
1184
|
+
.expand-all { margin-left: auto; font-size: 11px; }
|
|
1185
|
+
.sec-body { padding-bottom: 10px; }
|
|
1186
|
+
|
|
1187
|
+
.summary { margin: 0; font-size: 14px; line-height: 1.55; }
|
|
1188
|
+
.summary.pending { font-size: 12.5px; color: var(--muted); font-style: italic; }
|
|
1189
|
+
/* Read-from-source outline while the AI writes: explanations are still empty, so items open to just their facts. */
|
|
1190
|
+
.card.skeleton .ol-expl:not(:has(.ol-facts, .ol-now, .ol-note)) { display: none !important; }
|
|
1191
|
+
|
|
1192
|
+
/* live values, devtools-style */
|
|
1193
|
+
.v-str { color: #c2410c; }
|
|
1194
|
+
.v-num { color: #2563eb; }
|
|
1195
|
+
.v-bool { color: #9333ea; }
|
|
1196
|
+
.v-null { color: var(--muted); font-style: italic; }
|
|
1197
|
+
.v-fn { color: #a24d9c; font-style: italic; }
|
|
1198
|
+
.v-el { color: #0d8a7c; }
|
|
1199
|
+
.v-meta { color: var(--muted); }
|
|
1200
|
+
/* Code that couldn't be worked out to a value: as written, muted and italic so it doesn't read as a string. */
|
|
1201
|
+
.v-code { color: var(--muted); font-style: italic; }
|
|
1202
|
+
.v-key { color: var(--muted); }
|
|
1203
|
+
@media (prefers-color-scheme: dark) {
|
|
1204
|
+
.v-str { color: #f0a46b; }
|
|
1205
|
+
.v-num { color: #8ab4ff; }
|
|
1206
|
+
.v-bool { color: #d2a8ff; }
|
|
1207
|
+
.v-fn { color: #d7a0d3; }
|
|
1208
|
+
.v-el { color: #6ad3c2; }
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
.val { margin-top: 6px; border-radius: 6px; background: var(--soft); border: 1px solid var(--line); font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1212
|
+
.val.expandable { cursor: pointer; }
|
|
1213
|
+
.val.expandable:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); }
|
|
1214
|
+
.val-line { display: flex; align-items: baseline; gap: 8px; padding: 3px 8px; min-width: 0; }
|
|
1215
|
+
/* Labels take the colour of what they belong to (--kc: an outline item's group colour, else the accent).
|
|
1216
|
+
Live values ("Current") get a filled, gently pulsing dot. */
|
|
1217
|
+
.val-label { flex-shrink: 0; font: 700 9.5px ui-sans-serif, system-ui, sans-serif; text-transform: uppercase; letter-spacing: .06em; color: var(--kc, var(--accent)); }
|
|
1218
|
+
.val-label::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 5px; border-radius: 50%; background: currentColor; vertical-align: 0; animation: live 2.4s ease-in-out infinite; }
|
|
1219
|
+
.fact .val-label::before { content: none; }
|
|
1220
|
+
@keyframes live { 50% { opacity: .35; } }
|
|
1221
|
+
@media (prefers-reduced-motion: reduce) { .val-label::before { animation: none; } }
|
|
1222
|
+
.val-preview { flex: 1; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1223
|
+
.val-chevron { flex-shrink: 0; font-size: 10px; color: var(--muted); transition: transform .15s; }
|
|
1224
|
+
.val.open .val-chevron { transform: rotate(90deg); }
|
|
1225
|
+
.val-tree { display: none; padding: 2px 8px 6px; border-top: 1px solid var(--line); cursor: text; }
|
|
1226
|
+
.val.open .val-tree { display: block; }
|
|
1227
|
+
.vt .vt { padding-left: 14px; }
|
|
1228
|
+
.vt-row { display: flex; align-items: baseline; gap: 4px; padding: 1px 0; min-width: 0; list-style: none; }
|
|
1229
|
+
.vt-row::-webkit-details-marker { display: none; }
|
|
1230
|
+
summary.vt-row { cursor: pointer; border-radius: 3px; }
|
|
1231
|
+
summary.vt-row:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
|
|
1232
|
+
.vt-tw { flex-shrink: 0; width: 10px; font-size: 9px; color: var(--muted); }
|
|
1233
|
+
summary.vt-row .vt-tw::before { content: "\u25B8"; display: inline-block; transition: transform .12s; }
|
|
1234
|
+
.vt-node[open] > summary.vt-row .vt-tw::before { transform: rotate(90deg); }
|
|
1235
|
+
.vt-row > .v-key { flex-shrink: 0; }
|
|
1236
|
+
.vt-row > .v-key::after { content: ":"; margin-right: 4px; }
|
|
1237
|
+
.vt-val { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1238
|
+
|
|
1239
|
+
/* props: docs-style rows */
|
|
1240
|
+
.prop { display: grid; grid-template-columns: minmax(90px, 30%) 1fr; gap: 12px; padding: 10px 0; border-top: 1px solid var(--line); }
|
|
1241
|
+
.prop:first-child { border-top: 0; padding-top: 2px; }
|
|
1242
|
+
.prop-head { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; min-width: 0; }
|
|
1243
|
+
.card .prop-name { padding: 0; background: none; font-weight: 650; color: var(--fg); overflow-wrap: anywhere; }
|
|
1244
|
+
.card .prop-type { max-width: 100%; padding: 1px 6px; border-radius: 4px; font-size: 11px; color: var(--muted); background: var(--code); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1245
|
+
.prop-body { min-width: 0; }
|
|
1246
|
+
.prop-desc { font-size: 12.5px; }
|
|
1247
|
+
.prop-desc + .val { margin-top: 6px; }
|
|
1248
|
+
.prop-body > .val:first-child { margin-top: 0; }
|
|
1249
|
+
|
|
1250
|
+
/* outline tree, in the spirit of VS Code's outline view */
|
|
1251
|
+
.ol-group + .ol-group { margin-top: 10px; }
|
|
1252
|
+
.ol-group-head { display: flex; align-items: center; gap: 7px; padding: 4px 6px 3px; border-radius: 4px; cursor: pointer; list-style: none; user-select: none; }
|
|
1253
|
+
.ol-group-head::-webkit-details-marker { display: none; }
|
|
1254
|
+
.ol-group-head:hover { background: var(--soft); }
|
|
1255
|
+
.ol-group-head::before { content: "\u25B8"; width: 8px; font-size: 9px; color: var(--muted); transition: transform .12s; }
|
|
1256
|
+
.ol-group[open] > .ol-group-head::before { transform: rotate(90deg); }
|
|
1257
|
+
.ol-group-title { font-size: 12px; font-weight: 650; }
|
|
1258
|
+
.ol-group-hint { flex: 1; }
|
|
1259
|
+
.ol-hint-label { font: 10.5px ui-monospace, monospace; color: var(--muted); padding: 0 5px; border-radius: 3px; border: 1px dashed var(--line); }
|
|
1260
|
+
|
|
1261
|
+
/* custom types: linked in code-like text, definition in a hover popover */
|
|
1262
|
+
.type-ref { color: inherit; text-decoration: underline dotted; text-decoration-color: color-mix(in srgb, var(--accent) 70%, transparent); text-underline-offset: 2px; cursor: pointer; }
|
|
1263
|
+
.type-ref:hover { color: var(--accent); text-decoration-style: solid; }
|
|
1264
|
+
.type-pop { pointer-events: auto; position: fixed; z-index: 2; max-width: min(420px, calc(100vw - 16px)); border-radius: 8px; border: 1px solid var(--line); background: var(--bg); box-shadow: 0 8px 28px rgb(0 0 0 / .28); overflow: hidden; }
|
|
1265
|
+
.tp-head { display: flex; align-items: baseline; gap: 7px; padding: 7px 10px; border-bottom: 1px solid var(--line); background: var(--soft); }
|
|
1266
|
+
.tp-kind { font: 10px ui-monospace, monospace; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
1267
|
+
.tp-name { flex: 1; font: 600 12.5px ui-monospace, monospace; color: var(--fg); }
|
|
1268
|
+
.tp-file { font: 11px ui-monospace, monospace; color: var(--muted); text-decoration: none; cursor: pointer; }
|
|
1269
|
+
.tp-file:hover { color: var(--accent); text-decoration: underline; }
|
|
1270
|
+
.tp-code { margin: 0; padding: 8px 10px; max-height: 320px; overflow: auto; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: pre; }
|
|
1271
|
+
/* Symbol references: underlined in their outline kind colour; hover previews the definition, click jumps to it. */
|
|
1272
|
+
.kc-prop { --kc: var(--accent); }
|
|
1273
|
+
/* Symbol references read as tinted chips in their outline kind colour, so names stand out when skimming.
|
|
1274
|
+
(Text is mixed toward the foreground so lighter kind colours stay readable.) */
|
|
1275
|
+
.panel .sym-ref, .type-pop .sym-ref {
|
|
1276
|
+
cursor: pointer; padding: 0 4px; border-radius: 4px; white-space: nowrap;
|
|
1277
|
+
font: 500 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1278
|
+
color: color-mix(in srgb, var(--kc, var(--accent)) 78%, var(--fg));
|
|
1279
|
+
background: color-mix(in srgb, var(--kc, var(--accent)) 15%, transparent);
|
|
1280
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--kc, var(--accent)) 22%, transparent);
|
|
1281
|
+
transition: background .12s, box-shadow .12s;
|
|
1282
|
+
}
|
|
1283
|
+
.panel .sym-ref:hover, .type-pop .sym-ref:hover {
|
|
1284
|
+
background: color-mix(in srgb, var(--kc, var(--accent)) 26%, transparent);
|
|
1285
|
+
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--kc, var(--accent)) 55%, transparent);
|
|
1286
|
+
}
|
|
1287
|
+
.sp-body { max-height: 340px; overflow: auto; padding: 8px 10px 10px; font-size: 12.5px; }
|
|
1288
|
+
.sp-body .ol-facts { margin-top: 8px; }
|
|
1289
|
+
.sp-body .val { background: var(--bg); }
|
|
1290
|
+
.sp-body .val-label { min-width: 64px; }
|
|
1291
|
+
.sp-type { max-width: 50%; }
|
|
1292
|
+
.tp-head .ol-icon { align-self: center; }
|
|
1293
|
+
.tp-head .tp-file { margin-left: auto; }
|
|
1294
|
+
/* Go to definition: briefly highlight where we landed. */
|
|
1295
|
+
.ol-node.flash > .ol-row, .prop.flash { animation: flash 1.4s ease-out; }
|
|
1296
|
+
@keyframes flash { 0%, 30% { background: color-mix(in srgb, var(--kc, var(--accent)) 26%, transparent); } }
|
|
1297
|
+
@media (prefers-reduced-motion: reduce) { .ol-node.flash > .ol-row, .prop.flash { animation-duration: .01s; } }
|
|
1298
|
+
.ol-group .outline { margin-left: 15px; padding-left: 8px; border-left: 1px solid var(--line); }
|
|
1299
|
+
.ol-tag { flex-shrink: 0; font: 10px ui-monospace, monospace; padding: 0 4px; border-radius: 3px; border: 1px solid var(--line); color: var(--muted); }
|
|
1300
|
+
.ol-now { margin-top: 6px; }
|
|
1301
|
+
.outline { list-style: none; margin: 0; padding: 0; }
|
|
1302
|
+
.outline .outline { margin-left: 11px; padding-left: 10px; border-left: 1px solid var(--line); }
|
|
1303
|
+
.ol-row { display: flex; align-items: center; gap: 7px; padding: 3px 6px; border-radius: 4px; cursor: pointer; min-width: 0; }
|
|
1304
|
+
.ol-row:hover { background: var(--soft); }
|
|
1305
|
+
.ol-node.open > .ol-row { background: var(--soft); }
|
|
1306
|
+
.ol-icon { flex-shrink: 0; width: 18px; height: 18px; display: grid; place-items: center; border-radius: 4px; font: 700 10px ui-monospace, monospace; }
|
|
1307
|
+
.ol-icon svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round; stroke-linejoin: round; }
|
|
1308
|
+
.ol-name { flex-shrink: 0; font: 12.5px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1309
|
+
.ol-type { flex-shrink: 1; min-width: 0; max-width: 45%; padding: 0 6px; border-radius: 4px; background: var(--code); font: 11px/1.6 ui-monospace, monospace; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1310
|
+
.ol-when { flex-shrink: 1; min-width: 0; padding: 0 6px; border-radius: 4px; border: 1px dashed var(--line); font: 11px/1.6 ui-monospace, monospace; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1311
|
+
.ol-goto { margin-left: auto; }
|
|
1312
|
+
/* Long types are cut off in the row; opening the item shows the whole thing in place. */
|
|
1313
|
+
.ol-node.open > .ol-row { align-items: flex-start; flex-wrap: wrap; }
|
|
1314
|
+
.ol-node.open > .ol-row .ol-type, .ol-node.open > .ol-row .ol-when { max-width: none; white-space: normal; overflow-wrap: anywhere; }
|
|
1315
|
+
/* Secondary facts (initial value, returns, cleanup\u2026) as labelled boxes, like "Current". */
|
|
1316
|
+
.ol-facts { display: grid; gap: 5px; margin-top: 10px; }
|
|
1317
|
+
.ol-facts + .ol-now .val:first-child { margin-top: 5px; }
|
|
1318
|
+
.fact { display: flex; align-items: baseline; gap: 8px; padding: 4px 8px; border-radius: 6px; background: var(--bg); border: 1px solid var(--line); }
|
|
1319
|
+
.fact-val { min-width: 0; font-size: 12px; overflow-wrap: anywhere; }
|
|
1320
|
+
.fact-val code { font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; padding: 0; background: none; }
|
|
1321
|
+
.ol-expl .val-label { min-width: 64px; }
|
|
1322
|
+
.ol-expl .val { background: var(--bg); }
|
|
1323
|
+
.ol-note { margin-top: 6px; font-size: 11.5px; color: var(--muted); font-style: italic; }
|
|
1324
|
+
.ol-goto { flex-shrink: 0; font: 10.5px ui-monospace, monospace; color: var(--muted); text-decoration: none; opacity: 0; padding: 0 2px; }
|
|
1325
|
+
.ol-row:hover .ol-goto, .ol-node.open > .ol-row .ol-goto { opacity: 1; }
|
|
1326
|
+
.ol-goto:hover { color: var(--accent); text-decoration: underline; }
|
|
1327
|
+
.ol-goto::before { content: "L"; opacity: .6; }
|
|
1328
|
+
.ol-expl { display: none; margin: 2px 0 6px 31px; padding: 6px 10px; border-left: 2px solid var(--kc, var(--accent)); font-size: 12.5px; background: var(--soft); border-radius: 0 4px 4px 0; }
|
|
1329
|
+
.ol-node.open > .ol-expl { display: block; }
|
|
1330
|
+
/* One colour per kind, spread around the colour wheel so no two groups look alike.
|
|
1331
|
+
It drives the group icon, the item's labels and the expanded panel's edge. */
|
|
1332
|
+
.kc-state, .k-state { --kc: #9d5fc9; }
|
|
1333
|
+
.kc-callback, .kc-function, .k-callback, .k-function { --kc: #c2418f; }
|
|
1334
|
+
.kc-render, .k-render { --kc: #d0453f; }
|
|
1335
|
+
.kc-effect, .k-effect { --kc: #d27a12; }
|
|
1336
|
+
.kc-hook, .k-hook { --kc: #a68f0b; }
|
|
1337
|
+
.kc-ref, .k-ref { --kc: #2e9a52; }
|
|
1338
|
+
.kc-context, .kc-store, .k-context, .k-store { --kc: #12918e; }
|
|
1339
|
+
.kc-derived, .kc-memo, .k-derived, .k-memo { --kc: #3a7fc4; }
|
|
1340
|
+
.kc-constant, .kc-other, .k-constant, .k-other { --kc: var(--muted); }
|
|
1341
|
+
.ol-icon[class*="k-"] { background: color-mix(in srgb, var(--kc) 20%, transparent); color: var(--kc); }
|
|
1342
|
+
/* Brighter in dark mode so the kinds stay vivid against the dark panel. */
|
|
1343
|
+
@media (prefers-color-scheme: dark) {
|
|
1344
|
+
.kc-state, .k-state { --kc: #c495f2; }
|
|
1345
|
+
.kc-callback, .kc-function, .k-callback, .k-function { --kc: #f07cc0; }
|
|
1346
|
+
.kc-render, .k-render { --kc: #ff7b72; }
|
|
1347
|
+
.kc-effect, .k-effect { --kc: #ffab4a; }
|
|
1348
|
+
.kc-hook, .k-hook { --kc: #e8cd45; }
|
|
1349
|
+
.kc-ref, .k-ref { --kc: #5fd987; }
|
|
1350
|
+
.kc-context, .kc-store, .k-context, .k-store { --kc: #44d4cd; }
|
|
1351
|
+
.kc-derived, .kc-memo, .k-derived, .k-memo { --kc: #74b4ff; }
|
|
1352
|
+
.ol-icon[class*="k-"] { background: color-mix(in srgb, var(--kc) 24%, transparent); }
|
|
1353
|
+
}
|
|
1354
|
+
.k-render { font-size: 9px; }
|
|
1355
|
+
|
|
1356
|
+
/* Used in: like the props rows (these are props, as a parent passes them), one block per place. */
|
|
1357
|
+
.used { padding: 10px 0; border-top: 1px solid var(--line); }
|
|
1358
|
+
.used:first-child { border-top: 0; padding-top: 2px; }
|
|
1359
|
+
.used-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; font-size: 12.5px; }
|
|
1360
|
+
.used-head a.ref { color: var(--fg); font: 650 12px ui-monospace, monospace; padding: 1px 6px; border: 1px solid var(--line); border-radius: 4px; }
|
|
1361
|
+
.used-head a.ref:hover { color: var(--accent); text-decoration: none; border-color: color-mix(in srgb, var(--accent) 50%, var(--line)); }
|
|
1362
|
+
.used-in { color: var(--muted); }
|
|
1363
|
+
.used-in code { background: none; padding: 0; }
|
|
1364
|
+
.used-this { margin-left: auto; font-size: 11px; color: var(--accent); }
|
|
1365
|
+
.used-this::before { content: "\u25CF "; font-size: 8px; vertical-align: 1px; }
|
|
1366
|
+
.used-props { display: grid; grid-template-columns: minmax(90px, 30%) 1fr; gap: 6px 12px; margin-top: 8px; align-items: baseline; }
|
|
1367
|
+
.used-props .prop-name { font-weight: normal; }
|
|
1368
|
+
.used-val { min-width: 0; display: flex; }
|
|
1369
|
+
.card a.used-var { text-decoration: none; cursor: pointer; }
|
|
1370
|
+
.card a.used-var:hover { color: var(--accent); }
|
|
1371
|
+
|
|
1372
|
+
.timeline { list-style: none; margin: 4px 0 0 5px; padding: 0 0 0 14px; border-left: 2px solid var(--line); }
|
|
1373
|
+
.timeline li { position: relative; margin: 0 0 10px; }
|
|
1374
|
+
.timeline li::before { content: ""; position: absolute; left: -21px; top: 4px; width: 8px; height: 8px; border-radius: 50%; background: var(--bg); border: 2px solid var(--accent); }
|
|
1375
|
+
.tl-meta { font-size: 11px; color: var(--muted); }
|
|
1376
|
+
.tl-why { font-size: 12.5px; }
|
|
1377
|
+
|
|
1378
|
+
.banner { margin: 14px 0 0; padding: 8px 10px; border-radius: 6px; font-size: 12px; background: color-mix(in srgb, #f5a524 16%, var(--bg)); color: var(--fg); overflow-wrap: anywhere; }
|
|
1379
|
+
.banner code { font: 11px ui-monospace, monospace; }
|
|
1380
|
+
.stale { opacity: .45; pointer-events: none; }
|
|
1381
|
+
.diffs { margin-top: 6px; }
|
|
1382
|
+
.diffs summary { cursor: pointer; color: var(--accent); }
|
|
1383
|
+
.difffile { margin: 8px 0 2px; font: 600 11px ui-monospace, monospace; }
|
|
1384
|
+
pre.diff { margin: 0; padding: 6px 0; max-height: 240px; overflow: auto; border-radius: 4px; background: var(--bg); font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
|
1385
|
+
pre.diff span { display: block; padding: 0 8px; white-space: pre; }
|
|
1386
|
+
pre.diff .add { background: color-mix(in srgb, #30a46c 18%, transparent); }
|
|
1387
|
+
pre.diff .del { background: color-mix(in srgb, #e5484d 18%, transparent); }
|
|
1388
|
+
pre.diff .hunk { color: var(--muted); }
|
|
1389
|
+
|
|
1390
|
+
.meta code { font: 11px ui-monospace, monospace; }
|
|
1391
|
+
|
|
1392
|
+
.history { margin-top: 4px; padding-top: 12px; border-top: 1px solid var(--line); }
|
|
1393
|
+
.history h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 6px; font-size: 11.5px; font-weight: 650; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
|
|
1394
|
+
.qa { border-bottom: 1px solid var(--line); }
|
|
1395
|
+
.qa > summary { padding: 7px 0; cursor: pointer; font-weight: 550; }
|
|
1396
|
+
.qa[open] > summary { color: var(--accent); }
|
|
1397
|
+
.qa-body { padding: 0 0 8px 14px; }
|
|
1398
|
+
.qa-body .progress { margin-top: 2px; }
|
|
1399
|
+
.tag { font-size: 10px; font-weight: 600; text-transform: uppercase; padding: 1px 5px; border-radius: 4px; background: color-mix(in srgb, #f5a524 22%, var(--bg)); }
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
.ask { display: flex; gap: 6px; padding: 10px 12px; border-top: 1px solid var(--line); background: var(--soft); }
|
|
1403
|
+
.ask input { flex: 1; min-width: 0; padding: 7px 10px; border: 1px solid var(--line); border-radius: 6px; background: var(--bg); color: var(--fg); font: inherit; outline: none; }
|
|
1404
|
+
.ask input:focus { border-color: var(--accent); }
|
|
1405
|
+
.ask button { padding: 0 12px; border: 0; border-radius: 6px; background: var(--fg); color: var(--bg); font-weight: 600; cursor: pointer; }
|
|
1406
|
+
.ask button:hover { background: var(--accent); }
|
|
1407
|
+
`
|
|
1408
|
+
);
|
|
1409
|
+
|
|
1410
|
+
// src/client/index.ts
|
|
1411
|
+
var LOC_ATTR = "data-rd-loc";
|
|
1412
|
+
var COMP_ATTR = "data-rd-comp";
|
|
1413
|
+
function parseLoc(el) {
|
|
1414
|
+
const raw = el.getAttribute(LOC_ATTR);
|
|
1415
|
+
const m = raw?.match(/^(.*):(\d+):(\d+)$/);
|
|
1416
|
+
return m ? { file: m[1], line: +m[2], column: +m[3] } : null;
|
|
1417
|
+
}
|
|
1418
|
+
function resolveTarget(start) {
|
|
1419
|
+
const el = start?.closest(`[${COMP_ATTR}]`);
|
|
1420
|
+
if (!el) return null;
|
|
1421
|
+
const comp = el.getAttribute(COMP_ATTR);
|
|
1422
|
+
const file = parseLoc(el)?.file;
|
|
1423
|
+
let root = el;
|
|
1424
|
+
for (let p = el.parentElement; p; p = p.parentElement) {
|
|
1425
|
+
if (p.getAttribute(COMP_ATTR) !== comp || parseLoc(p)?.file !== file) break;
|
|
1426
|
+
root = p;
|
|
1427
|
+
}
|
|
1428
|
+
const loc = parseLoc(root);
|
|
1429
|
+
if (!loc) return null;
|
|
1430
|
+
return { comp, ...loc, root, el: start };
|
|
1431
|
+
}
|
|
1432
|
+
function componentChain(t) {
|
|
1433
|
+
const chain = [t];
|
|
1434
|
+
let cur = t.root.parentElement;
|
|
1435
|
+
while (cur) {
|
|
1436
|
+
const parent = resolveTarget(cur);
|
|
1437
|
+
if (!parent) break;
|
|
1438
|
+
chain.unshift(parent);
|
|
1439
|
+
cur = parent.root.parentElement;
|
|
1440
|
+
}
|
|
1441
|
+
return chain;
|
|
1442
|
+
}
|
|
1443
|
+
function modelName(id) {
|
|
1444
|
+
const m = id.match(/^claude-([a-z]+)-(\d+)(?:-(\d+))?/);
|
|
1445
|
+
return m ? `${m[1][0].toUpperCase()}${m[1].slice(1)} ${m[2]}${m[3] && m[3].length < 3 ? `.${m[3]}` : ""}` : id;
|
|
1446
|
+
}
|
|
1447
|
+
function init(options) {
|
|
1448
|
+
const w = window;
|
|
1449
|
+
if (w.__reactDetective) return w.__reactDetective;
|
|
1450
|
+
const endpoint = options.endpoint.replace(/\/$/, "");
|
|
1451
|
+
const host = document.createElement("react-detective");
|
|
1452
|
+
host.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483647;";
|
|
1453
|
+
const shadow = host.attachShadow({ mode: "open" });
|
|
1454
|
+
shadow.innerHTML = `<style>${STYLES}</style>
|
|
1455
|
+
<div class="box" hidden></div>
|
|
1456
|
+
<div class="box selected" hidden></div>
|
|
1457
|
+
<div class="tip" hidden></div>
|
|
1458
|
+
<button class="launcher" title="React Detective: press Alt (or click here) to inspect, then click any component. Esc to stop.">
|
|
1459
|
+
<svg class="hat" viewBox="0 0 24 24" aria-hidden="true">
|
|
1460
|
+
<path fill="currentColor" d="M1.5 16.6c2.7 1.8 6.4 2.6 10.5 2.6s7.8-.8 10.5-2.6c-1.5-.5-3-.8-4.4-.9l-1.3-7.4c-.3-1.7-1.4-2.5-2.6-2.1L12 7l-2.2-.8c-1.2-.4-2.3.4-2.6 2.1l-1.3 7.4c-1.4.1-2.9.4-4.4.9z"/>
|
|
1461
|
+
<path class="band" d="M6.1 13.2c3.9.9 7.9.9 11.8 0l.35 2.1c-4.1 1-8.4 1-12.5 0z"/>
|
|
1462
|
+
</svg>
|
|
1463
|
+
<span class="label"><span class="off">Detective</span><span class="on">Select a component to inspect</span></span><kbd class="off">Alt</kbd><kbd class="on" title="Press Esc to stop inspecting">Esc</kbd>
|
|
1464
|
+
</button>
|
|
1465
|
+
<aside class="panel" hidden></aside>
|
|
1466
|
+
<div class="type-pop" hidden></div>`;
|
|
1467
|
+
document.documentElement.appendChild(host);
|
|
1468
|
+
const $ = (sel) => shadow.querySelector(sel);
|
|
1469
|
+
const hoverBox = $(".box:not(.selected)");
|
|
1470
|
+
const selectedBox = $(".box.selected");
|
|
1471
|
+
const tip = $(".tip");
|
|
1472
|
+
const launcher = $(".launcher");
|
|
1473
|
+
const panel = $(".panel");
|
|
1474
|
+
const pop = $(".type-pop");
|
|
1475
|
+
let currentTypes = /* @__PURE__ */ new Map();
|
|
1476
|
+
if (options.showLauncher === false) launcher.hidden = true;
|
|
1477
|
+
let altDown = false;
|
|
1478
|
+
let sticky = false;
|
|
1479
|
+
let hovered = null;
|
|
1480
|
+
let selected = null;
|
|
1481
|
+
let pointer = { x: -1, y: -1 };
|
|
1482
|
+
let request = null;
|
|
1483
|
+
let raf = 0;
|
|
1484
|
+
const inspecting = () => altDown || sticky;
|
|
1485
|
+
function setInspecting() {
|
|
1486
|
+
const on = inspecting();
|
|
1487
|
+
launcher.classList.toggle("active", on);
|
|
1488
|
+
document.documentElement.style.cursor = on ? "crosshair" : "";
|
|
1489
|
+
if (on) updateHover();
|
|
1490
|
+
else hideHover();
|
|
1491
|
+
}
|
|
1492
|
+
function place(box, el) {
|
|
1493
|
+
const r = el.getBoundingClientRect();
|
|
1494
|
+
box.hidden = false;
|
|
1495
|
+
box.style.transform = `translate(${r.left}px, ${r.top}px)`;
|
|
1496
|
+
box.style.width = `${r.width}px`;
|
|
1497
|
+
box.style.height = `${r.height}px`;
|
|
1498
|
+
return r;
|
|
1499
|
+
}
|
|
1500
|
+
function hideHover() {
|
|
1501
|
+
hovered = null;
|
|
1502
|
+
hoverBox.hidden = true;
|
|
1503
|
+
tip.hidden = true;
|
|
1504
|
+
}
|
|
1505
|
+
function updateHover() {
|
|
1506
|
+
if (!inspecting() || pointer.x < 0) return;
|
|
1507
|
+
const el = document.elementFromPoint(pointer.x, pointer.y);
|
|
1508
|
+
const t = el && !host.contains(el) ? resolveTarget(el) : null;
|
|
1509
|
+
if (!t) return hideHover();
|
|
1510
|
+
hovered = t;
|
|
1511
|
+
const r = place(hoverBox, t.root);
|
|
1512
|
+
tip.hidden = false;
|
|
1513
|
+
tip.innerHTML = `<span class="name"><${esc(t.comp)}></span><span class="path">${esc(t.file)}:${t.line}</span>`;
|
|
1514
|
+
const tr = tip.getBoundingClientRect();
|
|
1515
|
+
const top = r.top - tr.height - 6 >= 0 ? r.top - tr.height - 6 : Math.min(r.bottom + 6, innerHeight - tr.height - 4);
|
|
1516
|
+
const left = Math.max(4, Math.min(r.left, innerWidth - tr.width - 4));
|
|
1517
|
+
tip.style.transform = `translate(${left}px, ${top}px)`;
|
|
1518
|
+
}
|
|
1519
|
+
function trackSelected() {
|
|
1520
|
+
cancelAnimationFrame(raf);
|
|
1521
|
+
const tick = () => {
|
|
1522
|
+
if (!selected || !selected.root.isConnected) {
|
|
1523
|
+
selectedBox.hidden = true;
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
place(selectedBox, selected.root);
|
|
1527
|
+
raf = requestAnimationFrame(tick);
|
|
1528
|
+
};
|
|
1529
|
+
tick();
|
|
1530
|
+
}
|
|
1531
|
+
function select(t) {
|
|
1532
|
+
selected = t;
|
|
1533
|
+
sticky = false;
|
|
1534
|
+
setInspecting();
|
|
1535
|
+
trackSelected();
|
|
1536
|
+
const chain = componentChain(t);
|
|
1537
|
+
const live = readLiveData(t.root, t.comp);
|
|
1538
|
+
currentTypes = /* @__PURE__ */ new Map();
|
|
1539
|
+
pop.hidden = true;
|
|
1540
|
+
panel.hidden = false;
|
|
1541
|
+
panel.innerHTML = `
|
|
1542
|
+
<header>
|
|
1543
|
+
${chain.length > 1 ? `<nav class="crumbs" title="Rendered inside these components. Click one to inspect it.">${chain.slice(0, -1).map((c, i) => `<button data-crumb="${i}">${esc(c.comp)}</button>`).join('<span class="sep">\u203A</span>')}<span class="sep">\u203A</span></nav>` : ""}
|
|
1544
|
+
<button class="icon close" title="Close (Esc)">\u2715</button>
|
|
1545
|
+
<div class="title">
|
|
1546
|
+
<a class="src title-link" data-file="${esc(t.file)}" data-line="${t.line}" data-col="${t.column}" title="Open ${esc(t.file)}:${t.line} in your editor">
|
|
1547
|
+
<h1><${esc(t.comp)}></h1><span class="title-goto">\u2197</span>
|
|
1548
|
+
</a>
|
|
1549
|
+
<button class="icon refresh" title="Regenerate the explanation">\u21BB</button>
|
|
1550
|
+
</div>
|
|
1551
|
+
<div class="meta-row">
|
|
1552
|
+
<span class="status" hidden></span>
|
|
1553
|
+
<span class="gitinfo" hidden></span>
|
|
1554
|
+
</div>
|
|
1555
|
+
</header>
|
|
1556
|
+
<div class="body">
|
|
1557
|
+
<section class="explanation"></section>
|
|
1558
|
+
<section class="history" hidden></section>
|
|
1559
|
+
</div>
|
|
1560
|
+
<form class="ask">
|
|
1561
|
+
<input name="q" placeholder="Ask about <${esc(t.comp)}>\u2026 (e.g. when does it re-render?)" autocomplete="off" />
|
|
1562
|
+
<button type="submit">Ask</button>
|
|
1563
|
+
</form>`;
|
|
1564
|
+
panel.querySelector(".close").addEventListener("click", closePanel);
|
|
1565
|
+
panel.querySelector(".refresh").addEventListener("click", () => run(true));
|
|
1566
|
+
panel.querySelectorAll("[data-crumb]").forEach(
|
|
1567
|
+
(b) => b.addEventListener("click", () => select(chain[Number(b.dataset.crumb)]))
|
|
1568
|
+
);
|
|
1569
|
+
panel.querySelector("form").addEventListener("submit", (e) => {
|
|
1570
|
+
e.preventDefault();
|
|
1571
|
+
const input = e.target.q;
|
|
1572
|
+
const q = input.value.trim();
|
|
1573
|
+
if (!q) return;
|
|
1574
|
+
input.value = "";
|
|
1575
|
+
ask(q);
|
|
1576
|
+
});
|
|
1577
|
+
function ask(q) {
|
|
1578
|
+
const list = questionList();
|
|
1579
|
+
const key = normalizeQuestion(q);
|
|
1580
|
+
let entry = [...list.querySelectorAll(".qa")].find((d) => d.dataset.key === key);
|
|
1581
|
+
if (!entry) {
|
|
1582
|
+
entry = document.createElement("details");
|
|
1583
|
+
entry.className = "qa";
|
|
1584
|
+
entry.dataset.key = key;
|
|
1585
|
+
entry.innerHTML = `<summary>${esc(q)}</summary><div class="qa-body"></div>`;
|
|
1586
|
+
}
|
|
1587
|
+
entry.querySelector(".tag")?.remove();
|
|
1588
|
+
list.append(entry);
|
|
1589
|
+
entry.open = true;
|
|
1590
|
+
updateQuestionCount();
|
|
1591
|
+
entry.scrollIntoView({ behavior: "smooth", block: "start" });
|
|
1592
|
+
stream({ question: q }, entry.querySelector(".qa-body"), false);
|
|
1593
|
+
}
|
|
1594
|
+
function questionList() {
|
|
1595
|
+
const section2 = panel.querySelector(".history");
|
|
1596
|
+
if (!section2.querySelector(".qa-list")) {
|
|
1597
|
+
section2.innerHTML = `<h3>Questions asked <span class="count">0</span></h3><div class="qa-list"></div>`;
|
|
1598
|
+
}
|
|
1599
|
+
section2.hidden = false;
|
|
1600
|
+
return section2.querySelector(".qa-list");
|
|
1601
|
+
}
|
|
1602
|
+
function updateQuestionCount() {
|
|
1603
|
+
const section2 = panel.querySelector(".history");
|
|
1604
|
+
section2.querySelector(".count").textContent = String(section2.querySelectorAll(".qa").length);
|
|
1605
|
+
}
|
|
1606
|
+
function setStatus(text, detail = "") {
|
|
1607
|
+
const el = panel.querySelector(".status");
|
|
1608
|
+
el.textContent = text;
|
|
1609
|
+
el.title = detail;
|
|
1610
|
+
el.hidden = !text;
|
|
1611
|
+
}
|
|
1612
|
+
function renderGitLine(last, uncommitted) {
|
|
1613
|
+
const el = panel.querySelector(".gitinfo");
|
|
1614
|
+
if (!last) return;
|
|
1615
|
+
el.innerHTML = `${esc(last.author)}, ${esc(shortDate(last.date))}${uncommitted ? ' <span class="tag">uncommitted</span>' : ""}`;
|
|
1616
|
+
el.title = `Last changed in ${last.sha}: ${last.subject}${uncommitted ? "\nThe file also has uncommitted changes." : ""}`;
|
|
1617
|
+
el.hidden = false;
|
|
1618
|
+
}
|
|
1619
|
+
let historyRendered = false;
|
|
1620
|
+
function renderHistory(questions) {
|
|
1621
|
+
if (historyRendered || !questions.length) return;
|
|
1622
|
+
historyRendered = true;
|
|
1623
|
+
const list = questionList();
|
|
1624
|
+
list.innerHTML = questions.map(
|
|
1625
|
+
(q) => `<details class="qa" data-key="${esc(normalizeQuestion(q.question))}">
|
|
1626
|
+
<summary>${esc(q.question)}${q.stale ? ' <span class="tag">outdated</span>' : ""}</summary>
|
|
1627
|
+
<div class="qa-body">
|
|
1628
|
+
<div class="md">${renderMarkdown(q.answer)}</div>
|
|
1629
|
+
<div class="meta">${timeAgo(q.createdAt)}${q.stale ? ` \xB7 code changed since \xB7 <button class="link" data-reask>ask again</button>` : ""}</div>
|
|
1630
|
+
</div>
|
|
1631
|
+
</details>`
|
|
1632
|
+
).join("");
|
|
1633
|
+
updateQuestionCount();
|
|
1634
|
+
list.querySelectorAll("[data-reask]").forEach(
|
|
1635
|
+
(b) => b.addEventListener("click", () => ask(b.closest(".qa").querySelector("summary").textContent.trim()))
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
const run = (refresh) => stream({ refresh }, panel.querySelector(".explanation"), true);
|
|
1639
|
+
run(false);
|
|
1640
|
+
function stream(extra, target, primary) {
|
|
1641
|
+
if (primary) request?.abort();
|
|
1642
|
+
const ctrl = new AbortController();
|
|
1643
|
+
if (primary) request = ctrl;
|
|
1644
|
+
const progressHtml = (label) => `<div class="progress"><div class="spinner"></div><span>${label}</span></div><ul class="steps"></ul>`;
|
|
1645
|
+
const current = primary ? target.querySelector(".card")?.outerHTML ?? "" : "";
|
|
1646
|
+
target.innerHTML = progressHtml(current ? "Regenerating\u2026" : "Exploring the codebase\u2026") + (current ? `<div class="stale">${current}</div>` : "");
|
|
1647
|
+
if (primary) setStatus("");
|
|
1648
|
+
else target.closest("details")?.setAttribute("open", "");
|
|
1649
|
+
let steps = target.querySelector(".steps");
|
|
1650
|
+
const content = (ev) => primary && ev.card ? `<div class="card">${renderCard(ev.card, { component: t.comp, file: t.file, live, slots: ev.slots })}</div>` : `<div class="md">${renderMarkdown(ev.markdown)}</div>`;
|
|
1651
|
+
const body = {
|
|
1652
|
+
component: t.comp,
|
|
1653
|
+
file: t.file,
|
|
1654
|
+
line: t.line,
|
|
1655
|
+
chain: chain.map((c) => c.comp),
|
|
1656
|
+
props: live.props,
|
|
1657
|
+
state: live.state,
|
|
1658
|
+
elementText: t.el.innerText?.trim().slice(0, 200) || void 0,
|
|
1659
|
+
...extra
|
|
1660
|
+
};
|
|
1661
|
+
readStream(`${endpoint}/explain`, body, ctrl.signal, (ev) => {
|
|
1662
|
+
for (const def of ev.types ?? []) currentTypes.set(def.name, def);
|
|
1663
|
+
handle(ev);
|
|
1664
|
+
if (ev.type === "result" || ev.type === "stale" || ev.type === "history" || ev.type === "skeleton") {
|
|
1665
|
+
const body2 = panel.querySelector(".body");
|
|
1666
|
+
linkTypes(body2, currentTypes);
|
|
1667
|
+
linkSymbols(body2);
|
|
1668
|
+
}
|
|
1669
|
+
}).catch((err) => {
|
|
1670
|
+
if (ctrl.signal.aborted) return;
|
|
1671
|
+
target.innerHTML = `<div class="error">Could not reach the react-detective server at <code>${esc(endpoint)}</code>.<br>${esc(String(err?.message ?? err))}</div>`;
|
|
1672
|
+
});
|
|
1673
|
+
function handle(ev) {
|
|
1674
|
+
if (ev.type === "progress") {
|
|
1675
|
+
const li = document.createElement("li");
|
|
1676
|
+
li.textContent = ev.text;
|
|
1677
|
+
steps.appendChild(li);
|
|
1678
|
+
while (steps.children.length > 6) steps.firstElementChild.remove();
|
|
1679
|
+
} else if (ev.type === "skeleton") {
|
|
1680
|
+
if (!primary || current) return;
|
|
1681
|
+
const done = steps.innerHTML;
|
|
1682
|
+
target.innerHTML = progressHtml("Writing the explanations\u2026") + `<div class="card skeleton">${renderCard(ev.card, { component: t.comp, file: t.file, live, slots: ev.slots })}</div>`;
|
|
1683
|
+
steps = target.querySelector(".steps");
|
|
1684
|
+
steps.innerHTML = done;
|
|
1685
|
+
} else if (ev.type === "context") {
|
|
1686
|
+
renderGitLine(ev.lastChange, ev.uncommitted);
|
|
1687
|
+
} else if (ev.type === "history") {
|
|
1688
|
+
renderHistory(ev.questions);
|
|
1689
|
+
} else if (ev.type === "stale") {
|
|
1690
|
+
const changes = ev.changes;
|
|
1691
|
+
const files = changes.map((c) => `<code>${esc(c.file)}</code>${c.deleted ? " (deleted)" : ""}`).join(", ");
|
|
1692
|
+
const diffs = changes.filter((c) => c.diff);
|
|
1693
|
+
target.innerHTML = `<div class="banner">Code changed since this was written${ev.git ? ` at <code>${esc(ev.git.commit)}</code>` : ""} (${files}).
|
|
1694
|
+
If the changes were significant, you might need to <button class="link" data-regenerate>generate a new explanation</button>.
|
|
1695
|
+
${diffs.length ? `<details class="diffs"><summary>Show what changed</summary>${diffs.map((c) => renderDiff(c.diff)).join("")}</details>` : ""}
|
|
1696
|
+
</div>
|
|
1697
|
+
${progressHtml("Re-reading what changed\u2026")}
|
|
1698
|
+
<div class="stale">${content(ev)}</div>`;
|
|
1699
|
+
steps = target.querySelector(".steps");
|
|
1700
|
+
target.querySelector("[data-regenerate]")?.addEventListener("click", () => run(true));
|
|
1701
|
+
} else if (ev.type === "result") {
|
|
1702
|
+
let meta = "";
|
|
1703
|
+
const at = ev.git ? ` at <code>${esc(ev.git.commit)}</code>${ev.git.branch && ev.git.branch !== "HEAD" ? ` (${esc(ev.git.branch)})` : ""}${ev.git.dirty ? " with uncommitted changes" : ""}` : "";
|
|
1704
|
+
let short = "";
|
|
1705
|
+
if (ev.cached) {
|
|
1706
|
+
meta = primary ? `Saved ${timeAgo(ev.createdAt)}${at} \xB7 code unchanged` : `Answered before (${timeAgo(ev.createdAt)}${at}) \xB7 code unchanged`;
|
|
1707
|
+
short = `Saved ${timeAgo(ev.createdAt)}`;
|
|
1708
|
+
} else {
|
|
1709
|
+
short = `Generated just now${ev.costUsd ? ` \xB7 $${ev.costUsd.toFixed(2)}` : ""}`;
|
|
1710
|
+
const bits = [`Explored live${at}`];
|
|
1711
|
+
if (ev.model) bits.push(modelName(ev.model));
|
|
1712
|
+
if (ev.turns) bits.push(`${ev.turns} step${ev.turns === 1 ? "" : "s"}`);
|
|
1713
|
+
if (ev.filesRead !== void 0) bits.push(`read ${ev.filesRead} file${ev.filesRead === 1 ? "" : "s"}`);
|
|
1714
|
+
const reused = [
|
|
1715
|
+
ev.notesReused && `${ev.notesReused} note${ev.notesReused === 1 ? "" : "s"}`,
|
|
1716
|
+
ev.exportsReused && `${ev.exportsReused} import${ev.exportsReused === 1 ? "" : "s"}`,
|
|
1717
|
+
ev.answersReused && `${ev.answersReused} past answer${ev.answersReused === 1 ? "" : "s"}`
|
|
1718
|
+
].filter(Boolean);
|
|
1719
|
+
if (reused.length) bits.push(`reused ${reused.join(" + ")}`);
|
|
1720
|
+
if (ev.costUsd) bits.push(`~$${ev.costUsd.toFixed(3)}`);
|
|
1721
|
+
meta = bits.join(" \xB7 ");
|
|
1722
|
+
}
|
|
1723
|
+
if (primary) {
|
|
1724
|
+
setStatus(short, meta.replace(/<[^>]+>/g, ""));
|
|
1725
|
+
target.innerHTML = content(ev);
|
|
1726
|
+
} else {
|
|
1727
|
+
target.innerHTML = `${content(ev)}<div class="meta">${meta}</div>`;
|
|
1728
|
+
target.closest("details")?.setAttribute("open", "");
|
|
1729
|
+
}
|
|
1730
|
+
} else if (ev.type === "error") {
|
|
1731
|
+
if (primary) setStatus("");
|
|
1732
|
+
const message = esc(ev.message).replace(/^(ANTHROPIC_API_KEY=.*)$/m, "<code>$1</code>").replace(/(https:\/\/\S+?)(\.?)(?=\s|$)/g, '<a href="$1" target="_blank" rel="noopener">$1</a>$2').replace(/\n/g, "<br>");
|
|
1733
|
+
const box = `<div class="${ev.code === "no-api-key" ? "banner" : "error"}">${message}</div>`;
|
|
1734
|
+
const skeleton = target.querySelector(".card.skeleton");
|
|
1735
|
+
const dimmed = target.querySelector(":scope > .stale");
|
|
1736
|
+
if (!dimmed && !skeleton) return void (target.innerHTML = box);
|
|
1737
|
+
target.querySelectorAll(":scope > .progress, :scope > .steps").forEach((el) => el.remove());
|
|
1738
|
+
dimmed?.replaceWith(...dimmed.childNodes);
|
|
1739
|
+
const pending = skeleton?.querySelector(".summary.pending");
|
|
1740
|
+
if (pending) pending.textContent = "No explanation yet. The outline below is read straight from the code.";
|
|
1741
|
+
target.insertAdjacentHTML("afterbegin", box);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
function closePanel() {
|
|
1747
|
+
request?.abort();
|
|
1748
|
+
selected = null;
|
|
1749
|
+
panel.hidden = true;
|
|
1750
|
+
pop.hidden = true;
|
|
1751
|
+
selectedBox.hidden = true;
|
|
1752
|
+
cancelAnimationFrame(raf);
|
|
1753
|
+
}
|
|
1754
|
+
let hideTimer = 0;
|
|
1755
|
+
let shownFor = null;
|
|
1756
|
+
function showRef(ref) {
|
|
1757
|
+
clearTimeout(hideTimer);
|
|
1758
|
+
if (ref === shownFor && !pop.hidden) return;
|
|
1759
|
+
let html = "";
|
|
1760
|
+
if (ref.dataset.type) {
|
|
1761
|
+
const def = currentTypes.get(ref.dataset.type);
|
|
1762
|
+
if (def) html = typePopover(def, currentTypes);
|
|
1763
|
+
} else {
|
|
1764
|
+
const def = findDefinition(panel, ref.dataset.sym);
|
|
1765
|
+
if (def) html = symbolPopover(def);
|
|
1766
|
+
}
|
|
1767
|
+
if (!html) return;
|
|
1768
|
+
const inPop = pop.contains(ref);
|
|
1769
|
+
pop.innerHTML = html;
|
|
1770
|
+
pop.hidden = false;
|
|
1771
|
+
shownFor = inPop ? null : ref;
|
|
1772
|
+
if (inPop) return;
|
|
1773
|
+
pop.style.left = "0px";
|
|
1774
|
+
pop.style.top = "0px";
|
|
1775
|
+
const r = ref.getBoundingClientRect();
|
|
1776
|
+
const p = pop.getBoundingClientRect();
|
|
1777
|
+
const left = Math.max(8, Math.min(r.left, innerWidth - p.width - 8));
|
|
1778
|
+
const top = r.bottom + 6 + p.height < innerHeight - 8 ? r.bottom + 6 : Math.max(8, r.top - p.height - 6);
|
|
1779
|
+
pop.style.left = `${left}px`;
|
|
1780
|
+
pop.style.top = `${top}px`;
|
|
1781
|
+
}
|
|
1782
|
+
const hidePopSoon = () => {
|
|
1783
|
+
clearTimeout(hideTimer);
|
|
1784
|
+
hideTimer = window.setTimeout(() => pop.hidden = true, 200);
|
|
1785
|
+
};
|
|
1786
|
+
for (const el of [panel, pop]) {
|
|
1787
|
+
el.addEventListener("mouseover", (e) => {
|
|
1788
|
+
const ref = e.target.closest(".type-ref, .sym-ref");
|
|
1789
|
+
if (ref && !(el === pop && ref.classList.contains("sym-ref"))) showRef(ref);
|
|
1790
|
+
else if (el === pop) clearTimeout(hideTimer);
|
|
1791
|
+
});
|
|
1792
|
+
el.addEventListener("mouseout", (e) => {
|
|
1793
|
+
if (el === pop || e.target.closest(".type-ref, .sym-ref")) hidePopSoon();
|
|
1794
|
+
});
|
|
1795
|
+
}
|
|
1796
|
+
pop.addEventListener("click", (e) => onPanelClick(e));
|
|
1797
|
+
panel.addEventListener("click", (e) => onPanelClick(e));
|
|
1798
|
+
function onPanelClick(e) {
|
|
1799
|
+
const target = e.target;
|
|
1800
|
+
const a = target.closest("a.src");
|
|
1801
|
+
if (a) {
|
|
1802
|
+
e.preventDefault();
|
|
1803
|
+
fetch(`${endpoint}/open`, {
|
|
1804
|
+
method: "POST",
|
|
1805
|
+
headers: { "content-type": "application/json" },
|
|
1806
|
+
body: JSON.stringify({ file: a.dataset.file, line: a.dataset.line, column: a.dataset.col })
|
|
1807
|
+
}).catch(() => {
|
|
1808
|
+
});
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const sym = target.closest(".sym-ref");
|
|
1812
|
+
if (sym) {
|
|
1813
|
+
const def = findDefinition(panel, sym.dataset.sym);
|
|
1814
|
+
if (def) {
|
|
1815
|
+
e.preventDefault();
|
|
1816
|
+
pop.hidden = true;
|
|
1817
|
+
revealDefinition(def);
|
|
1818
|
+
}
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
const all = target.closest("[data-expand-all]");
|
|
1822
|
+
if (all) {
|
|
1823
|
+
e.preventDefault();
|
|
1824
|
+
const sec = all.closest(".sec");
|
|
1825
|
+
const nodes = [...sec?.querySelectorAll(".ol-node") ?? []];
|
|
1826
|
+
const groups = [...sec?.querySelectorAll("details.ol-group") ?? []];
|
|
1827
|
+
const open = nodes.some((n) => !n.classList.contains("open")) || groups.some((g) => !g.open);
|
|
1828
|
+
nodes.forEach((n) => n.classList.toggle("open", open));
|
|
1829
|
+
if (open) groups.forEach((g) => g.open = true);
|
|
1830
|
+
all.textContent = open ? "collapse all" : "expand all";
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
const row = target.closest(".ol-row");
|
|
1834
|
+
if (row) return row.parentElement.classList.toggle("open");
|
|
1835
|
+
const value = target.closest(".expandable");
|
|
1836
|
+
if (value && !target.closest(".val-tree")) value.classList.toggle("open");
|
|
1837
|
+
}
|
|
1838
|
+
launcher.addEventListener("click", (e) => {
|
|
1839
|
+
if (justDragged) {
|
|
1840
|
+
justDragged = false;
|
|
1841
|
+
return e.preventDefault();
|
|
1842
|
+
}
|
|
1843
|
+
sticky = !sticky;
|
|
1844
|
+
setInspecting();
|
|
1845
|
+
});
|
|
1846
|
+
const CORNER_KEY = "react-detective:launcher-corner";
|
|
1847
|
+
let justDragged = false;
|
|
1848
|
+
function setCorner(c) {
|
|
1849
|
+
launcher.dataset.corner = c;
|
|
1850
|
+
}
|
|
1851
|
+
try {
|
|
1852
|
+
const saved = localStorage.getItem(CORNER_KEY);
|
|
1853
|
+
setCorner(saved === "tl" || saved === "tr" || saved === "bl" ? saved : "br");
|
|
1854
|
+
} catch {
|
|
1855
|
+
setCorner("br");
|
|
1856
|
+
}
|
|
1857
|
+
launcher.addEventListener("pointerdown", (down) => {
|
|
1858
|
+
if (down.button !== 0) return;
|
|
1859
|
+
const start = launcher.getBoundingClientRect();
|
|
1860
|
+
launcher.setPointerCapture(down.pointerId);
|
|
1861
|
+
let dragging = false;
|
|
1862
|
+
const move = (e) => {
|
|
1863
|
+
const dx = e.clientX - down.clientX;
|
|
1864
|
+
const dy = e.clientY - down.clientY;
|
|
1865
|
+
if (!dragging && Math.hypot(dx, dy) < 5) return;
|
|
1866
|
+
if (!dragging) {
|
|
1867
|
+
dragging = true;
|
|
1868
|
+
launcher.classList.add("dragging");
|
|
1869
|
+
}
|
|
1870
|
+
const x = Math.max(0, Math.min(innerWidth - start.width, start.left + dx));
|
|
1871
|
+
const y = Math.max(0, Math.min(innerHeight - start.height, start.top + dy));
|
|
1872
|
+
launcher.style.cssText = `left:${x}px;top:${y}px;right:auto;bottom:auto;`;
|
|
1873
|
+
};
|
|
1874
|
+
const up = () => {
|
|
1875
|
+
launcher.removeEventListener("pointermove", move);
|
|
1876
|
+
launcher.removeEventListener("pointerup", up);
|
|
1877
|
+
launcher.removeEventListener("pointercancel", up);
|
|
1878
|
+
if (!dragging) return;
|
|
1879
|
+
justDragged = true;
|
|
1880
|
+
setTimeout(() => justDragged = false, 0);
|
|
1881
|
+
const from = launcher.getBoundingClientRect();
|
|
1882
|
+
const cx = from.left + from.width / 2;
|
|
1883
|
+
const cy = from.top + from.height / 2;
|
|
1884
|
+
const corner = `${cy < innerHeight / 2 ? "t" : "b"}${cx < innerWidth / 2 ? "l" : "r"}`;
|
|
1885
|
+
launcher.style.cssText = "";
|
|
1886
|
+
launcher.classList.remove("dragging");
|
|
1887
|
+
setCorner(corner);
|
|
1888
|
+
try {
|
|
1889
|
+
localStorage.setItem(CORNER_KEY, corner);
|
|
1890
|
+
} catch {
|
|
1891
|
+
}
|
|
1892
|
+
const to = launcher.getBoundingClientRect();
|
|
1893
|
+
launcher.animate(
|
|
1894
|
+
[{ transform: `translate(${from.left - to.left}px, ${from.top - to.top}px)` }, { transform: "none" }],
|
|
1895
|
+
{ duration: matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 220, easing: "cubic-bezier(.2, .9, .3, 1.15)" }
|
|
1896
|
+
);
|
|
1897
|
+
};
|
|
1898
|
+
launcher.addEventListener("pointermove", move);
|
|
1899
|
+
launcher.addEventListener("pointerup", up);
|
|
1900
|
+
launcher.addEventListener("pointercancel", up);
|
|
1901
|
+
});
|
|
1902
|
+
const onMove = (e) => {
|
|
1903
|
+
pointer = { x: e.clientX, y: e.clientY };
|
|
1904
|
+
if (inspecting()) updateHover();
|
|
1905
|
+
};
|
|
1906
|
+
let altAlone = false;
|
|
1907
|
+
const onKeyDown = (e) => {
|
|
1908
|
+
if (e.key === "Alt") {
|
|
1909
|
+
if (!altDown) {
|
|
1910
|
+
altDown = true;
|
|
1911
|
+
altAlone = true;
|
|
1912
|
+
setInspecting();
|
|
1913
|
+
}
|
|
1914
|
+
e.preventDefault();
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
altAlone = false;
|
|
1918
|
+
if (e.key === "Escape") {
|
|
1919
|
+
if (sticky) {
|
|
1920
|
+
sticky = false;
|
|
1921
|
+
setInspecting();
|
|
1922
|
+
} else if (selected) closePanel();
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
const onKeyUp = (e) => {
|
|
1926
|
+
if (e.key === "Alt") {
|
|
1927
|
+
altDown = false;
|
|
1928
|
+
if (altAlone) sticky = !sticky;
|
|
1929
|
+
altAlone = false;
|
|
1930
|
+
setInspecting();
|
|
1931
|
+
e.preventDefault();
|
|
1932
|
+
}
|
|
1933
|
+
};
|
|
1934
|
+
const onBlur = () => {
|
|
1935
|
+
altDown = false;
|
|
1936
|
+
altAlone = false;
|
|
1937
|
+
setInspecting();
|
|
1938
|
+
};
|
|
1939
|
+
const swallow = (e) => {
|
|
1940
|
+
if (e.type === "pointerdown") altAlone = false;
|
|
1941
|
+
if (!inspecting() || !hovered || host.contains(e.target)) return;
|
|
1942
|
+
e.preventDefault();
|
|
1943
|
+
e.stopPropagation();
|
|
1944
|
+
e.stopImmediatePropagation();
|
|
1945
|
+
if (e.type === "click") select(hovered);
|
|
1946
|
+
};
|
|
1947
|
+
const onScroll = () => inspecting() && updateHover();
|
|
1948
|
+
addEventListener("pointermove", onMove, { capture: true, passive: true });
|
|
1949
|
+
addEventListener("keydown", onKeyDown, true);
|
|
1950
|
+
addEventListener("keyup", onKeyUp, true);
|
|
1951
|
+
addEventListener("blur", onBlur);
|
|
1952
|
+
addEventListener("scroll", onScroll, { capture: true, passive: true });
|
|
1953
|
+
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) addEventListener(type, swallow, true);
|
|
1954
|
+
const cleanup = () => {
|
|
1955
|
+
request?.abort();
|
|
1956
|
+
cancelAnimationFrame(raf);
|
|
1957
|
+
removeEventListener("pointermove", onMove, true);
|
|
1958
|
+
removeEventListener("keydown", onKeyDown, true);
|
|
1959
|
+
removeEventListener("keyup", onKeyUp, true);
|
|
1960
|
+
removeEventListener("blur", onBlur);
|
|
1961
|
+
removeEventListener("scroll", onScroll, true);
|
|
1962
|
+
for (const type of ["pointerdown", "mousedown", "pointerup", "mouseup", "click"]) removeEventListener(type, swallow, true);
|
|
1963
|
+
document.documentElement.style.cursor = "";
|
|
1964
|
+
host.remove();
|
|
1965
|
+
delete w.__reactDetective;
|
|
1966
|
+
};
|
|
1967
|
+
w.__reactDetective = cleanup;
|
|
1968
|
+
return cleanup;
|
|
1969
|
+
}
|
|
1970
|
+
async function readStream(url, body, signal, onEvent) {
|
|
1971
|
+
const res = await fetch(url, {
|
|
1972
|
+
method: "POST",
|
|
1973
|
+
headers: { "content-type": "application/json" },
|
|
1974
|
+
body: JSON.stringify(body),
|
|
1975
|
+
signal
|
|
1976
|
+
});
|
|
1977
|
+
if (!res.ok || !res.body) {
|
|
1978
|
+
const text = await res.text().catch(() => "");
|
|
1979
|
+
let message = text;
|
|
1980
|
+
try {
|
|
1981
|
+
message = JSON.parse(text).error ?? text;
|
|
1982
|
+
} catch {
|
|
1983
|
+
}
|
|
1984
|
+
throw new Error(`${res.status} ${message}`);
|
|
1985
|
+
}
|
|
1986
|
+
const reader = res.body.getReader();
|
|
1987
|
+
const decoder = new TextDecoder();
|
|
1988
|
+
let buf = "";
|
|
1989
|
+
for (; ; ) {
|
|
1990
|
+
const { done, value } = await reader.read();
|
|
1991
|
+
if (done) break;
|
|
1992
|
+
buf += decoder.decode(value, { stream: true });
|
|
1993
|
+
let nl;
|
|
1994
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
1995
|
+
const line = buf.slice(0, nl).trim();
|
|
1996
|
+
buf = buf.slice(nl + 1);
|
|
1997
|
+
if (line) onEvent(JSON.parse(line));
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function renderDiff(diff) {
|
|
2002
|
+
const lines = diff.split("\n").filter((l) => !l.startsWith("--- ") && !l.startsWith("+++ ") && !l.startsWith("Index:") && !l.startsWith("\\ No newline")).map((l) => {
|
|
2003
|
+
const cls = l.startsWith("@@") ? "hunk" : l.startsWith("+") ? "add" : l.startsWith("-") ? "del" : "";
|
|
2004
|
+
return `<span class="${cls}">${esc(l) || " "}</span>`;
|
|
2005
|
+
}).join("");
|
|
2006
|
+
const file = diff.match(/^\+\+\+ b\/(.+?)(\t|$)/m)?.[1] ?? "";
|
|
2007
|
+
return `<div class="difffile">${esc(file)}</div><pre class="diff">${lines}</pre>`;
|
|
2008
|
+
}
|
|
2009
|
+
function timeAgo(ts) {
|
|
2010
|
+
if (!ts) return "";
|
|
2011
|
+
const s = Math.round((Date.now() - ts) / 1e3);
|
|
2012
|
+
if (s < 60) return "just now";
|
|
2013
|
+
if (s < 3600) return `${Math.round(s / 60)}m ago`;
|
|
2014
|
+
if (s < 86400) return `${Math.round(s / 3600)}h ago`;
|
|
2015
|
+
return `${Math.round(s / 86400)}d ago`;
|
|
2016
|
+
}
|
|
2017
|
+
function normalizeQuestion(q) {
|
|
2018
|
+
return q.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, " ").trim();
|
|
2019
|
+
}
|
|
2020
|
+
function shortDate(iso) {
|
|
2021
|
+
const d = /* @__PURE__ */ new Date(`${iso}T00:00:00`);
|
|
2022
|
+
if (isNaN(d.getTime())) return iso;
|
|
2023
|
+
const sameYear = d.getFullYear() === (/* @__PURE__ */ new Date()).getFullYear();
|
|
2024
|
+
return d.toLocaleDateString(void 0, { month: "short", day: "numeric", ...sameYear ? {} : { year: "numeric" } });
|
|
2025
|
+
}
|
|
2026
|
+
export {
|
|
2027
|
+
init
|
|
2028
|
+
};
|