@fluixi/devtools 0.2.0-alpha.3 → 0.2.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/callsite.cjs +72 -0
  2. package/dist/callsite.mjs +51 -0
  3. package/dist/change-view.cjs +134 -0
  4. package/dist/change-view.mjs +111 -0
  5. package/dist/copy.cjs +63 -0
  6. package/dist/copy.mjs +42 -0
  7. package/dist/describe.cjs +55 -0
  8. package/dist/describe.mjs +34 -0
  9. package/dist/detail-panel.cjs +307 -0
  10. package/dist/detail-panel.mjs +284 -0
  11. package/dist/diff.cjs +225 -0
  12. package/dist/diff.mjs +202 -0
  13. package/dist/dom.cjs +204 -0
  14. package/dist/dom.mjs +183 -0
  15. package/dist/export.cjs +221 -0
  16. package/dist/export.mjs +209 -0
  17. package/dist/float.cjs +232 -0
  18. package/dist/float.mjs +211 -0
  19. package/dist/format.cjs +175 -0
  20. package/dist/format.mjs +154 -0
  21. package/dist/graph-view.cjs +893 -0
  22. package/dist/graph-view.mjs +870 -0
  23. package/dist/highlight.cjs +140 -0
  24. package/dist/highlight.mjs +119 -0
  25. package/dist/history.cjs +95 -0
  26. package/dist/history.mjs +74 -0
  27. package/dist/hook.cjs +1562 -0
  28. package/dist/hook.mjs +1553 -0
  29. package/dist/index.cjs +4552 -0
  30. package/dist/index.mjs +4544 -0
  31. package/dist/inspector.cjs +688 -0
  32. package/dist/inspector.mjs +676 -0
  33. package/dist/instrument.cjs +497 -0
  34. package/dist/instrument.mjs +487 -0
  35. package/dist/kind-filter.cjs +172 -0
  36. package/dist/kind-filter.mjs +151 -0
  37. package/dist/menu.cjs +141 -0
  38. package/dist/menu.mjs +120 -0
  39. package/dist/observe.cjs +1352 -0
  40. package/dist/observe.mjs +1345 -0
  41. package/dist/palette.cjs +52 -0
  42. package/dist/palette.mjs +31 -0
  43. package/dist/path.cjs +45 -0
  44. package/dist/path.mjs +24 -0
  45. package/dist/plugin.cjs +87 -0
  46. package/dist/plugin.mjs +66 -0
  47. package/dist/plugins/context.cjs +98 -0
  48. package/dist/plugins/context.mjs +75 -0
  49. package/dist/plugins/directives.cjs +98 -0
  50. package/dist/plugins/directives.mjs +75 -0
  51. package/dist/plugins/index.cjs +162 -0
  52. package/dist/plugins/index.mjs +140 -0
  53. package/dist/source.cjs +111 -0
  54. package/dist/source.mjs +90 -0
  55. package/dist/timeline.cjs +350 -0
  56. package/dist/timeline.mjs +327 -0
  57. package/dist/tree.cjs +574 -0
  58. package/dist/tree.mjs +551 -0
  59. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  60. package/dist/value-pane.cjs +223 -0
  61. package/dist/value-pane.mjs +200 -0
  62. package/dist/wire.cjs +109 -0
  63. package/dist/wire.mjs +89 -0
  64. package/package.json +13 -6
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/detail-panel.ts
21
+ var detail_panel_exports = {};
22
+ __export(detail_panel_exports, {
23
+ DETAIL_PANEL_CSS: () => DETAIL_PANEL_CSS,
24
+ createDetailPanel: () => createDetailPanel
25
+ });
26
+ module.exports = __toCommonJS(detail_panel_exports);
27
+
28
+ // src/copy.ts
29
+ function confirmOn(button, said = "copied") {
30
+ if (!button) return;
31
+ const was = button.textContent;
32
+ button.textContent = said;
33
+ setTimeout(() => {
34
+ button.textContent = was;
35
+ }, 1200);
36
+ }
37
+ function fallback(text, button) {
38
+ try {
39
+ const doc = button?.ownerDocument ?? document;
40
+ const area = doc.createElement("textarea");
41
+ area.value = text;
42
+ area.setAttribute("readonly", "");
43
+ area.style.position = "fixed";
44
+ area.style.opacity = "0";
45
+ doc.body.appendChild(area);
46
+ area.select();
47
+ doc.execCommand("copy");
48
+ area.remove();
49
+ confirmOn(button);
50
+ return true;
51
+ } catch {
52
+ return false;
53
+ }
54
+ }
55
+ async function copyText(text, button) {
56
+ const clipboard = globalThis.navigator?.clipboard;
57
+ if (clipboard?.writeText) {
58
+ try {
59
+ await clipboard.writeText(text);
60
+ confirmOn(button);
61
+ return true;
62
+ } catch {
63
+ }
64
+ }
65
+ return fallback(text, button);
66
+ }
67
+
68
+ // src/change-view.ts
69
+ var escape = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
70
+ var SIGN = { added: "+", removed: "−", changed: "~" };
71
+ function pretty(json) {
72
+ if (!json) return void 0;
73
+ try {
74
+ return JSON.stringify(JSON.parse(json), null, 2);
75
+ } catch {
76
+ return void 0;
77
+ }
78
+ }
79
+ function sideOf(text, json, cls) {
80
+ const block = pretty(json);
81
+ if (!block) return `<span class="ch-${cls}">${escape(text ?? "")}</span>`;
82
+ return `<div class="ch-block ch-${cls}"><button type="button" class="ch-copy" title="Copy" data-copy="${escape(block)}">copy</button><pre class="ch-json">${escape(block)}</pre></div>`;
83
+ }
84
+ function changeRows(change, limit = Infinity) {
85
+ if (!change?.entries?.length) return "";
86
+ const shown = change.entries.slice(0, limit);
87
+ const hidden = change.entries.length - shown.length + (change.more ?? 0);
88
+ const rows = shown.map((entry) => {
89
+ const from = sideOf(entry.from, entry.fromJson, "from");
90
+ const to = sideOf(entry.to, entry.toJson, "to");
91
+ const sides = entry.op === "changed" ? `${from}<span class="ch-arrow">→</span>${to}` : entry.op === "added" ? to : from;
92
+ const stacked = entry.fromJson || entry.toJson ? " ch-stacked" : "";
93
+ return `<div class="ch-row ch-${entry.op}${stacked}"><span class="ch-sign">${SIGN[entry.op] ?? "·"}</span><span class="ch-key">${escape(entry.key)}</span><span class="ch-sides">${sides}</span></div>`;
94
+ }).join("");
95
+ const size = change.length ? `<div class="ch-size">${change.length.from} → ${change.length.to} items</div>` : "";
96
+ return `<div class="ch-list">${size}${rows}${hidden > 0 ? `<div class="ch-more">and ${hidden} more</div>` : ""}</div>`;
97
+ }
98
+ function handleCopyClick(event) {
99
+ const button = event.target?.closest?.(".ch-copy");
100
+ if (!button) return false;
101
+ void copyText(button.dataset.copy ?? "", button);
102
+ return true;
103
+ }
104
+ var CHANGE_CSS = `
105
+ .ch-list { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; }
106
+ .ch-size { color: var(--fx-muted, #8a90a2); margin-bottom: 2px; }
107
+ .ch-row { display: flex; gap: 6px; align-items: baseline; padding: 1px 0; }
108
+ .ch-row.ch-stacked { align-items: flex-start; padding: 3px 0; }
109
+ .ch-sign { flex: none; width: 9px; text-align: center; }
110
+ .ch-added .ch-sign { color: var(--state, #34d399); }
111
+ .ch-removed .ch-sign { color: var(--fx-write, #fb7185); }
112
+ .ch-changed .ch-sign { color: var(--effect, #fbbf24); }
113
+ .ch-key { flex: none; color: var(--fx-muted, #8a90a2); }
114
+ .ch-sides { min-width: 0; flex: 1; overflow-wrap: anywhere; }
115
+ .ch-stacked .ch-sides { display: flex; flex-direction: column; gap: 2px; }
116
+ .ch-from { color: var(--fx-muted, #8a90a2); }
117
+ .ch-row:not(.ch-stacked) .ch-from { text-decoration: line-through; }
118
+ .ch-arrow { color: var(--fx-muted, #8a90a2); margin: 0 4px; }
119
+ .ch-to { color: var(--fx-label, #e7e9ee); }
120
+ .ch-more { color: var(--fx-muted, #8a90a2); margin-top: 2px; }
121
+ /* In the corner: an object can be tall, and a button under it is a scroll away. */
122
+ .ch-block { position: relative; border-radius: 4px; border: 1px solid var(--fx-line, #ffffff14); }
123
+ .ch-block.ch-from { opacity: .72; }
124
+ .ch-json { margin: 0; padding: 5px 7px; overflow-x: auto; white-space: pre; font: inherit; }
125
+ .ch-copy {
126
+ position: absolute; top: 3px; right: 3px; padding: 1px 6px; cursor: pointer;
127
+ font: inherit; font-size: 9.5px; opacity: 0; transition: opacity .12s;
128
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 3px;
129
+ background: var(--fx-panel, #14161c); color: var(--fx-muted, #8a90a2);
130
+ }
131
+ .ch-block:hover .ch-copy, .ch-copy:focus { opacity: 1; }
132
+ .ch-copy:hover { color: var(--fx-label, #e7e9ee); }
133
+ `;
134
+
135
+ // src/detail-panel.ts
136
+ var escape2 = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
137
+ var clock = (t) => {
138
+ const d = new Date(t);
139
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
140
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
141
+ };
142
+ var VERBS = {
143
+ write: "written",
144
+ ran: "ran",
145
+ created: "made",
146
+ disposed: "gone"
147
+ };
148
+ var MIN_W = 320;
149
+ var MIN_H = 200;
150
+ function draggable(handle, onMove) {
151
+ handle.addEventListener("pointerdown", (event) => {
152
+ if (event.button !== 0) return;
153
+ if (event.target.closest("button")) return;
154
+ event.preventDefault();
155
+ event.stopPropagation();
156
+ handle.setPointerCapture(event.pointerId);
157
+ let lastX = event.clientX;
158
+ let lastY = event.clientY;
159
+ const move = (e) => {
160
+ onMove(e.clientX - lastX, e.clientY - lastY);
161
+ lastX = e.clientX;
162
+ lastY = e.clientY;
163
+ };
164
+ const stop = () => {
165
+ handle.removeEventListener("pointermove", move);
166
+ handle.removeEventListener("pointerup", stop);
167
+ handle.removeEventListener("pointercancel", stop);
168
+ handle.removeEventListener("lostpointercapture", stop);
169
+ if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
170
+ };
171
+ handle.addEventListener("pointermove", move);
172
+ handle.addEventListener("pointerup", stop);
173
+ handle.addEventListener("pointercancel", stop);
174
+ handle.addEventListener("lostpointercapture", stop);
175
+ });
176
+ }
177
+ function createDetailPanel(host, options = {}) {
178
+ const panel = host.ownerDocument.createElement("div");
179
+ panel.className = "dp-panel";
180
+ panel.hidden = true;
181
+ panel.innerHTML = '<div class="dp-bar"><span class="dp-title"></span><button type="button" class="dp-close" title="Close" aria-label="Close">✕</button></div><div class="dp-body"></div><div class="dp-grip" title="Drag to resize"></div>';
182
+ host.appendChild(panel);
183
+ const title = panel.querySelector(".dp-title");
184
+ const body = panel.querySelector(".dp-body");
185
+ const bar = panel.querySelector(".dp-bar");
186
+ const grip = panel.querySelector(".dp-grip");
187
+ let x = 0;
188
+ let y = 0;
189
+ let width = 520;
190
+ let height = 380;
191
+ let open = false;
192
+ const place = () => {
193
+ panel.style.left = `${Math.round(x)}px`;
194
+ panel.style.top = `${Math.round(y)}px`;
195
+ panel.style.width = `${Math.round(width)}px`;
196
+ panel.style.height = `${Math.round(height)}px`;
197
+ };
198
+ draggable(bar, (dx, dy) => {
199
+ const view = panel.ownerDocument.defaultView;
200
+ x = Math.max(0, Math.min((view?.innerWidth ?? width) - 60, x + dx));
201
+ y = Math.max(0, Math.min((view?.innerHeight ?? height) - 30, y + dy));
202
+ place();
203
+ });
204
+ draggable(grip, (dx, dy) => {
205
+ width = Math.max(MIN_W, width + dx);
206
+ height = Math.max(MIN_H, height + dy);
207
+ place();
208
+ });
209
+ panel.querySelector(".dp-close")?.addEventListener("click", () => close());
210
+ body.addEventListener("click", (e) => {
211
+ if (handleCopyClick(e)) return;
212
+ const row = e.target.closest(".dp-node");
213
+ if (row?.dataset.id) options.onPick?.(Number(row.dataset.id));
214
+ });
215
+ function close() {
216
+ open = false;
217
+ panel.hidden = true;
218
+ }
219
+ return {
220
+ open(commit, graph) {
221
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
222
+ const cause = commit.cause;
223
+ title.textContent = cause ? `${nameOf(cause)} · ${clock(commit.t)}` : `${commit.commit === 0 ? "first render" : `commit ${commit.commit}`} · ${clock(commit.t)}`;
224
+ const headline = cause && cause.from !== void 0 && cause.to !== void 0 ? `<div class="dp-change"><span class="ch-from">${escape2(cause.from)}</span><span class="ch-arrow">→</span><span class="ch-to">${escape2(cause.to)}</span></div>` : "";
225
+ const groups = ["write", "ran", "created", "disposed"].map((kind) => {
226
+ const rows = commit.events.filter((e) => e.kind === kind && e !== cause);
227
+ if (!rows.length) return "";
228
+ const seen = /* @__PURE__ */ new Map();
229
+ for (const event of rows) {
230
+ const found = seen.get(event.id);
231
+ if (found) found.count++;
232
+ else seen.set(event.id, { name: nameOf(event), count: 1 });
233
+ }
234
+ const items = [...seen].map(
235
+ ([id, { name, count }]) => `<button type="button" class="dp-node" data-id="${id}">${escape2(name)}${count > 1 ? ` ×${count}` : ""}</button>`
236
+ ).join("");
237
+ return `<div class="dp-group"><div class="dp-verb">${VERBS[kind]}</div><div class="dp-nodes">${items}</div></div>`;
238
+ }).join("");
239
+ const node = cause ? graph.get(cause.id) : void 0;
240
+ const where = (t) => {
241
+ if (!t.source) return "";
242
+ const file = (t.source.file ?? "").split("/").pop() ?? "";
243
+ const out = t.source.compiled;
244
+ const tail = out && out.line !== t.source.line ? ` · out ${out.line}` : "";
245
+ return ` — ${file}:${t.source.line ?? ""}${tail}`;
246
+ };
247
+ const targets = node?.targets?.length ? `<div class="dp-group"><div class="dp-verb">updates</div><div class="dp-targets">${node.targets.map(
248
+ (t) => `<div>${escape2(t.name ? `${t.name} of ${t.element}` : t.element)}<span class="dp-where">${escape2(where(t))}</span></div>`
249
+ ).join("")}</div></div>` : "";
250
+ body.innerHTML = headline + changeRows(cause?.change) + groups + targets;
251
+ if (!open) {
252
+ const view = panel.ownerDocument.defaultView;
253
+ x = Math.max(12, ((view?.innerWidth ?? 900) - width) / 2);
254
+ y = Math.max(12, ((view?.innerHeight ?? 600) - height) / 3);
255
+ place();
256
+ }
257
+ open = true;
258
+ panel.hidden = false;
259
+ },
260
+ close,
261
+ get isOpen() {
262
+ return open;
263
+ },
264
+ destroy() {
265
+ panel.remove();
266
+ }
267
+ };
268
+ }
269
+ var DETAIL_PANEL_CSS = `
270
+ .dp-panel {
271
+ position: fixed; z-index: 40; display: flex; flex-direction: column;
272
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 8px;
273
+ background: var(--fx-panel, #14161c); color: var(--fx-label, #e7e9ee);
274
+ box-shadow: 0 12px 40px rgba(0, 0, 0, .45); overflow: hidden;
275
+ }
276
+ .dp-panel[hidden] { display: none; }
277
+ .dp-bar {
278
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;
279
+ cursor: move; user-select: none;
280
+ border-bottom: 1px solid var(--fx-line, #ffffff1f);
281
+ }
282
+ .dp-title { font-family: ui-monospace, monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
283
+ .dp-close {
284
+ margin-left: auto; width: 22px; height: 20px; padding: 0; cursor: pointer; font: inherit; font-size: 11px;
285
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
286
+ background: transparent; color: var(--fx-muted, #8a90a2);
287
+ }
288
+ .dp-close:hover { color: var(--fx-label, #e7e9ee); }
289
+ .dp-body { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; }
290
+ .dp-change { font-family: ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }
291
+ .dp-group { margin-top: 10px; }
292
+ .dp-verb { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--fx-muted, #8a90a2); }
293
+ .dp-nodes { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
294
+ .dp-node {
295
+ padding: 2px 7px; cursor: pointer; font: inherit; font-family: ui-monospace, monospace; font-size: 11px;
296
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 999px;
297
+ background: transparent; color: var(--fx-label, #e7e9ee);
298
+ }
299
+ .dp-node:hover { border-color: var(--fx-accent, #5eead4); }
300
+ .dp-targets { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: var(--fx-label, #e7e9ee); }
301
+ .dp-where { color: var(--fx-muted, #8a90a2); }
302
+ /* Bottom-right, where every desktop puts it. */
303
+ .dp-grip {
304
+ position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize;
305
+ background: linear-gradient(135deg, transparent 50%, var(--fx-line, #ffffff40) 50%);
306
+ }
307
+ ${CHANGE_CSS}`;
@@ -0,0 +1,284 @@
1
+ // src/copy.ts
2
+ function confirmOn(button, said = "copied") {
3
+ if (!button) return;
4
+ const was = button.textContent;
5
+ button.textContent = said;
6
+ setTimeout(() => {
7
+ button.textContent = was;
8
+ }, 1200);
9
+ }
10
+ function fallback(text, button) {
11
+ try {
12
+ const doc = button?.ownerDocument ?? document;
13
+ const area = doc.createElement("textarea");
14
+ area.value = text;
15
+ area.setAttribute("readonly", "");
16
+ area.style.position = "fixed";
17
+ area.style.opacity = "0";
18
+ doc.body.appendChild(area);
19
+ area.select();
20
+ doc.execCommand("copy");
21
+ area.remove();
22
+ confirmOn(button);
23
+ return true;
24
+ } catch {
25
+ return false;
26
+ }
27
+ }
28
+ async function copyText(text, button) {
29
+ const clipboard = globalThis.navigator?.clipboard;
30
+ if (clipboard?.writeText) {
31
+ try {
32
+ await clipboard.writeText(text);
33
+ confirmOn(button);
34
+ return true;
35
+ } catch {
36
+ }
37
+ }
38
+ return fallback(text, button);
39
+ }
40
+
41
+ // src/change-view.ts
42
+ var escape = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
43
+ var SIGN = { added: "+", removed: "−", changed: "~" };
44
+ function pretty(json) {
45
+ if (!json) return void 0;
46
+ try {
47
+ return JSON.stringify(JSON.parse(json), null, 2);
48
+ } catch {
49
+ return void 0;
50
+ }
51
+ }
52
+ function sideOf(text, json, cls) {
53
+ const block = pretty(json);
54
+ if (!block) return `<span class="ch-${cls}">${escape(text ?? "")}</span>`;
55
+ return `<div class="ch-block ch-${cls}"><button type="button" class="ch-copy" title="Copy" data-copy="${escape(block)}">copy</button><pre class="ch-json">${escape(block)}</pre></div>`;
56
+ }
57
+ function changeRows(change, limit = Infinity) {
58
+ if (!change?.entries?.length) return "";
59
+ const shown = change.entries.slice(0, limit);
60
+ const hidden = change.entries.length - shown.length + (change.more ?? 0);
61
+ const rows = shown.map((entry) => {
62
+ const from = sideOf(entry.from, entry.fromJson, "from");
63
+ const to = sideOf(entry.to, entry.toJson, "to");
64
+ const sides = entry.op === "changed" ? `${from}<span class="ch-arrow">→</span>${to}` : entry.op === "added" ? to : from;
65
+ const stacked = entry.fromJson || entry.toJson ? " ch-stacked" : "";
66
+ return `<div class="ch-row ch-${entry.op}${stacked}"><span class="ch-sign">${SIGN[entry.op] ?? "·"}</span><span class="ch-key">${escape(entry.key)}</span><span class="ch-sides">${sides}</span></div>`;
67
+ }).join("");
68
+ const size = change.length ? `<div class="ch-size">${change.length.from} → ${change.length.to} items</div>` : "";
69
+ return `<div class="ch-list">${size}${rows}${hidden > 0 ? `<div class="ch-more">and ${hidden} more</div>` : ""}</div>`;
70
+ }
71
+ function handleCopyClick(event) {
72
+ const button = event.target?.closest?.(".ch-copy");
73
+ if (!button) return false;
74
+ void copyText(button.dataset.copy ?? "", button);
75
+ return true;
76
+ }
77
+ var CHANGE_CSS = `
78
+ .ch-list { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; }
79
+ .ch-size { color: var(--fx-muted, #8a90a2); margin-bottom: 2px; }
80
+ .ch-row { display: flex; gap: 6px; align-items: baseline; padding: 1px 0; }
81
+ .ch-row.ch-stacked { align-items: flex-start; padding: 3px 0; }
82
+ .ch-sign { flex: none; width: 9px; text-align: center; }
83
+ .ch-added .ch-sign { color: var(--state, #34d399); }
84
+ .ch-removed .ch-sign { color: var(--fx-write, #fb7185); }
85
+ .ch-changed .ch-sign { color: var(--effect, #fbbf24); }
86
+ .ch-key { flex: none; color: var(--fx-muted, #8a90a2); }
87
+ .ch-sides { min-width: 0; flex: 1; overflow-wrap: anywhere; }
88
+ .ch-stacked .ch-sides { display: flex; flex-direction: column; gap: 2px; }
89
+ .ch-from { color: var(--fx-muted, #8a90a2); }
90
+ .ch-row:not(.ch-stacked) .ch-from { text-decoration: line-through; }
91
+ .ch-arrow { color: var(--fx-muted, #8a90a2); margin: 0 4px; }
92
+ .ch-to { color: var(--fx-label, #e7e9ee); }
93
+ .ch-more { color: var(--fx-muted, #8a90a2); margin-top: 2px; }
94
+ /* In the corner: an object can be tall, and a button under it is a scroll away. */
95
+ .ch-block { position: relative; border-radius: 4px; border: 1px solid var(--fx-line, #ffffff14); }
96
+ .ch-block.ch-from { opacity: .72; }
97
+ .ch-json { margin: 0; padding: 5px 7px; overflow-x: auto; white-space: pre; font: inherit; }
98
+ .ch-copy {
99
+ position: absolute; top: 3px; right: 3px; padding: 1px 6px; cursor: pointer;
100
+ font: inherit; font-size: 9.5px; opacity: 0; transition: opacity .12s;
101
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 3px;
102
+ background: var(--fx-panel, #14161c); color: var(--fx-muted, #8a90a2);
103
+ }
104
+ .ch-block:hover .ch-copy, .ch-copy:focus { opacity: 1; }
105
+ .ch-copy:hover { color: var(--fx-label, #e7e9ee); }
106
+ `;
107
+
108
+ // src/detail-panel.ts
109
+ var escape2 = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
110
+ var clock = (t) => {
111
+ const d = new Date(t);
112
+ const pad = (n, w = 2) => String(n).padStart(w, "0");
113
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
114
+ };
115
+ var VERBS = {
116
+ write: "written",
117
+ ran: "ran",
118
+ created: "made",
119
+ disposed: "gone"
120
+ };
121
+ var MIN_W = 320;
122
+ var MIN_H = 200;
123
+ function draggable(handle, onMove) {
124
+ handle.addEventListener("pointerdown", (event) => {
125
+ if (event.button !== 0) return;
126
+ if (event.target.closest("button")) return;
127
+ event.preventDefault();
128
+ event.stopPropagation();
129
+ handle.setPointerCapture(event.pointerId);
130
+ let lastX = event.clientX;
131
+ let lastY = event.clientY;
132
+ const move = (e) => {
133
+ onMove(e.clientX - lastX, e.clientY - lastY);
134
+ lastX = e.clientX;
135
+ lastY = e.clientY;
136
+ };
137
+ const stop = () => {
138
+ handle.removeEventListener("pointermove", move);
139
+ handle.removeEventListener("pointerup", stop);
140
+ handle.removeEventListener("pointercancel", stop);
141
+ handle.removeEventListener("lostpointercapture", stop);
142
+ if (handle.hasPointerCapture(event.pointerId)) handle.releasePointerCapture(event.pointerId);
143
+ };
144
+ handle.addEventListener("pointermove", move);
145
+ handle.addEventListener("pointerup", stop);
146
+ handle.addEventListener("pointercancel", stop);
147
+ handle.addEventListener("lostpointercapture", stop);
148
+ });
149
+ }
150
+ function createDetailPanel(host, options = {}) {
151
+ const panel = host.ownerDocument.createElement("div");
152
+ panel.className = "dp-panel";
153
+ panel.hidden = true;
154
+ panel.innerHTML = '<div class="dp-bar"><span class="dp-title"></span><button type="button" class="dp-close" title="Close" aria-label="Close">✕</button></div><div class="dp-body"></div><div class="dp-grip" title="Drag to resize"></div>';
155
+ host.appendChild(panel);
156
+ const title = panel.querySelector(".dp-title");
157
+ const body = panel.querySelector(".dp-body");
158
+ const bar = panel.querySelector(".dp-bar");
159
+ const grip = panel.querySelector(".dp-grip");
160
+ let x = 0;
161
+ let y = 0;
162
+ let width = 520;
163
+ let height = 380;
164
+ let open = false;
165
+ const place = () => {
166
+ panel.style.left = `${Math.round(x)}px`;
167
+ panel.style.top = `${Math.round(y)}px`;
168
+ panel.style.width = `${Math.round(width)}px`;
169
+ panel.style.height = `${Math.round(height)}px`;
170
+ };
171
+ draggable(bar, (dx, dy) => {
172
+ const view = panel.ownerDocument.defaultView;
173
+ x = Math.max(0, Math.min((view?.innerWidth ?? width) - 60, x + dx));
174
+ y = Math.max(0, Math.min((view?.innerHeight ?? height) - 30, y + dy));
175
+ place();
176
+ });
177
+ draggable(grip, (dx, dy) => {
178
+ width = Math.max(MIN_W, width + dx);
179
+ height = Math.max(MIN_H, height + dy);
180
+ place();
181
+ });
182
+ panel.querySelector(".dp-close")?.addEventListener("click", () => close());
183
+ body.addEventListener("click", (e) => {
184
+ if (handleCopyClick(e)) return;
185
+ const row = e.target.closest(".dp-node");
186
+ if (row?.dataset.id) options.onPick?.(Number(row.dataset.id));
187
+ });
188
+ function close() {
189
+ open = false;
190
+ panel.hidden = true;
191
+ }
192
+ return {
193
+ open(commit, graph) {
194
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
195
+ const cause = commit.cause;
196
+ title.textContent = cause ? `${nameOf(cause)} · ${clock(commit.t)}` : `${commit.commit === 0 ? "first render" : `commit ${commit.commit}`} · ${clock(commit.t)}`;
197
+ const headline = cause && cause.from !== void 0 && cause.to !== void 0 ? `<div class="dp-change"><span class="ch-from">${escape2(cause.from)}</span><span class="ch-arrow">→</span><span class="ch-to">${escape2(cause.to)}</span></div>` : "";
198
+ const groups = ["write", "ran", "created", "disposed"].map((kind) => {
199
+ const rows = commit.events.filter((e) => e.kind === kind && e !== cause);
200
+ if (!rows.length) return "";
201
+ const seen = /* @__PURE__ */ new Map();
202
+ for (const event of rows) {
203
+ const found = seen.get(event.id);
204
+ if (found) found.count++;
205
+ else seen.set(event.id, { name: nameOf(event), count: 1 });
206
+ }
207
+ const items = [...seen].map(
208
+ ([id, { name, count }]) => `<button type="button" class="dp-node" data-id="${id}">${escape2(name)}${count > 1 ? ` ×${count}` : ""}</button>`
209
+ ).join("");
210
+ return `<div class="dp-group"><div class="dp-verb">${VERBS[kind]}</div><div class="dp-nodes">${items}</div></div>`;
211
+ }).join("");
212
+ const node = cause ? graph.get(cause.id) : void 0;
213
+ const where = (t) => {
214
+ if (!t.source) return "";
215
+ const file = (t.source.file ?? "").split("/").pop() ?? "";
216
+ const out = t.source.compiled;
217
+ const tail = out && out.line !== t.source.line ? ` · out ${out.line}` : "";
218
+ return ` — ${file}:${t.source.line ?? ""}${tail}`;
219
+ };
220
+ const targets = node?.targets?.length ? `<div class="dp-group"><div class="dp-verb">updates</div><div class="dp-targets">${node.targets.map(
221
+ (t) => `<div>${escape2(t.name ? `${t.name} of ${t.element}` : t.element)}<span class="dp-where">${escape2(where(t))}</span></div>`
222
+ ).join("")}</div></div>` : "";
223
+ body.innerHTML = headline + changeRows(cause?.change) + groups + targets;
224
+ if (!open) {
225
+ const view = panel.ownerDocument.defaultView;
226
+ x = Math.max(12, ((view?.innerWidth ?? 900) - width) / 2);
227
+ y = Math.max(12, ((view?.innerHeight ?? 600) - height) / 3);
228
+ place();
229
+ }
230
+ open = true;
231
+ panel.hidden = false;
232
+ },
233
+ close,
234
+ get isOpen() {
235
+ return open;
236
+ },
237
+ destroy() {
238
+ panel.remove();
239
+ }
240
+ };
241
+ }
242
+ var DETAIL_PANEL_CSS = `
243
+ .dp-panel {
244
+ position: fixed; z-index: 40; display: flex; flex-direction: column;
245
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 8px;
246
+ background: var(--fx-panel, #14161c); color: var(--fx-label, #e7e9ee);
247
+ box-shadow: 0 12px 40px rgba(0, 0, 0, .45); overflow: hidden;
248
+ }
249
+ .dp-panel[hidden] { display: none; }
250
+ .dp-bar {
251
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;
252
+ cursor: move; user-select: none;
253
+ border-bottom: 1px solid var(--fx-line, #ffffff1f);
254
+ }
255
+ .dp-title { font-family: ui-monospace, monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
256
+ .dp-close {
257
+ margin-left: auto; width: 22px; height: 20px; padding: 0; cursor: pointer; font: inherit; font-size: 11px;
258
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
259
+ background: transparent; color: var(--fx-muted, #8a90a2);
260
+ }
261
+ .dp-close:hover { color: var(--fx-label, #e7e9ee); }
262
+ .dp-body { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; }
263
+ .dp-change { font-family: ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }
264
+ .dp-group { margin-top: 10px; }
265
+ .dp-verb { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--fx-muted, #8a90a2); }
266
+ .dp-nodes { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
267
+ .dp-node {
268
+ padding: 2px 7px; cursor: pointer; font: inherit; font-family: ui-monospace, monospace; font-size: 11px;
269
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 999px;
270
+ background: transparent; color: var(--fx-label, #e7e9ee);
271
+ }
272
+ .dp-node:hover { border-color: var(--fx-accent, #5eead4); }
273
+ .dp-targets { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: var(--fx-label, #e7e9ee); }
274
+ .dp-where { color: var(--fx-muted, #8a90a2); }
275
+ /* Bottom-right, where every desktop puts it. */
276
+ .dp-grip {
277
+ position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize;
278
+ background: linear-gradient(135deg, transparent 50%, var(--fx-line, #ffffff40) 50%);
279
+ }
280
+ ${CHANGE_CSS}`;
281
+ export {
282
+ DETAIL_PANEL_CSS,
283
+ createDetailPanel
284
+ };