@fluixi/devtools 0.2.0-alpha.2 → 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 (158) hide show
  1. package/README.md +80 -2
  2. package/dist/callsite.cjs +72 -0
  3. package/dist/callsite.d.ts +22 -0
  4. package/dist/callsite.d.ts.map +1 -0
  5. package/dist/callsite.js +82 -0
  6. package/dist/callsite.mjs +51 -0
  7. package/dist/change-view.cjs +134 -0
  8. package/dist/change-view.d.ts +10 -0
  9. package/dist/change-view.d.ts.map +1 -0
  10. package/dist/change-view.js +102 -0
  11. package/dist/change-view.mjs +111 -0
  12. package/dist/copy.cjs +63 -0
  13. package/dist/copy.d.ts +9 -0
  14. package/dist/copy.d.ts.map +1 -0
  15. package/dist/copy.js +52 -0
  16. package/dist/copy.mjs +42 -0
  17. package/dist/describe.cjs +55 -0
  18. package/dist/describe.d.ts +3 -0
  19. package/dist/describe.d.ts.map +1 -0
  20. package/dist/describe.js +47 -0
  21. package/dist/describe.mjs +34 -0
  22. package/dist/detail-panel.cjs +307 -0
  23. package/dist/detail-panel.d.ts +17 -0
  24. package/dist/detail-panel.d.ts.map +1 -0
  25. package/dist/detail-panel.js +217 -0
  26. package/dist/detail-panel.mjs +284 -0
  27. package/dist/diff.cjs +225 -0
  28. package/dist/diff.d.ts +31 -0
  29. package/dist/diff.d.ts.map +1 -0
  30. package/dist/diff.js +132 -0
  31. package/dist/diff.mjs +202 -0
  32. package/dist/dom.cjs +204 -0
  33. package/dist/dom.d.ts +81 -0
  34. package/dist/dom.d.ts.map +1 -0
  35. package/dist/dom.js +269 -0
  36. package/dist/dom.mjs +183 -0
  37. package/dist/export.cjs +221 -0
  38. package/dist/export.d.ts +55 -0
  39. package/dist/export.d.ts.map +1 -0
  40. package/dist/export.js +90 -0
  41. package/dist/export.mjs +209 -0
  42. package/dist/float.cjs +232 -0
  43. package/dist/float.d.ts +54 -0
  44. package/dist/float.d.ts.map +1 -0
  45. package/dist/float.js +256 -0
  46. package/dist/float.mjs +211 -0
  47. package/dist/format.cjs +175 -0
  48. package/dist/format.d.ts +16 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +188 -0
  51. package/dist/format.mjs +154 -0
  52. package/dist/graph-view.cjs +893 -0
  53. package/dist/graph-view.d.ts +79 -2
  54. package/dist/graph-view.d.ts.map +1 -1
  55. package/dist/graph-view.js +866 -50
  56. package/dist/graph-view.mjs +870 -0
  57. package/dist/highlight.cjs +140 -0
  58. package/dist/highlight.d.ts +56 -0
  59. package/dist/highlight.d.ts.map +1 -0
  60. package/dist/highlight.js +154 -0
  61. package/dist/highlight.mjs +119 -0
  62. package/dist/history.cjs +95 -0
  63. package/dist/history.d.ts +59 -0
  64. package/dist/history.d.ts.map +1 -0
  65. package/dist/history.js +85 -0
  66. package/dist/history.mjs +74 -0
  67. package/dist/hook.cjs +1562 -0
  68. package/dist/hook.d.ts +60 -0
  69. package/dist/hook.d.ts.map +1 -0
  70. package/dist/hook.js +142 -0
  71. package/dist/hook.mjs +1553 -0
  72. package/dist/index.cjs +4552 -0
  73. package/dist/index.d.ts +44 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +25 -1
  76. package/dist/index.mjs +4544 -0
  77. package/dist/inspector.cjs +688 -0
  78. package/dist/inspector.d.ts +60 -0
  79. package/dist/inspector.d.ts.map +1 -0
  80. package/dist/inspector.js +415 -0
  81. package/dist/inspector.mjs +676 -0
  82. package/dist/instrument.cjs +497 -0
  83. package/dist/instrument.d.ts +147 -5
  84. package/dist/instrument.d.ts.map +1 -1
  85. package/dist/instrument.js +164 -44
  86. package/dist/instrument.mjs +487 -0
  87. package/dist/kind-filter.cjs +172 -0
  88. package/dist/kind-filter.d.ts +39 -0
  89. package/dist/kind-filter.d.ts.map +1 -0
  90. package/dist/kind-filter.js +151 -0
  91. package/dist/kind-filter.mjs +151 -0
  92. package/dist/menu.cjs +141 -0
  93. package/dist/menu.d.ts +37 -0
  94. package/dist/menu.d.ts.map +1 -0
  95. package/dist/menu.js +125 -0
  96. package/dist/menu.mjs +120 -0
  97. package/dist/observe.cjs +1352 -0
  98. package/dist/observe.d.ts +80 -0
  99. package/dist/observe.d.ts.map +1 -0
  100. package/dist/observe.js +893 -0
  101. package/dist/observe.mjs +1345 -0
  102. package/dist/palette.cjs +52 -0
  103. package/dist/palette.d.ts +11 -0
  104. package/dist/palette.d.ts.map +1 -0
  105. package/dist/palette.js +27 -0
  106. package/dist/palette.mjs +31 -0
  107. package/dist/path.cjs +45 -0
  108. package/dist/path.d.ts +19 -0
  109. package/dist/path.d.ts.map +1 -0
  110. package/dist/path.js +49 -0
  111. package/dist/path.mjs +24 -0
  112. package/dist/plugin.cjs +87 -0
  113. package/dist/plugin.d.ts +121 -0
  114. package/dist/plugin.d.ts.map +1 -0
  115. package/dist/plugin.js +74 -0
  116. package/dist/plugin.mjs +66 -0
  117. package/dist/plugins/context.cjs +98 -0
  118. package/dist/plugins/context.d.ts +10 -0
  119. package/dist/plugins/context.d.ts.map +1 -0
  120. package/dist/plugins/context.js +80 -0
  121. package/dist/plugins/context.mjs +75 -0
  122. package/dist/plugins/directives.cjs +98 -0
  123. package/dist/plugins/directives.d.ts +25 -0
  124. package/dist/plugins/directives.d.ts.map +1 -0
  125. package/dist/plugins/directives.js +78 -0
  126. package/dist/plugins/directives.mjs +75 -0
  127. package/dist/plugins/index.cjs +162 -0
  128. package/dist/plugins/index.d.ts +7 -0
  129. package/dist/plugins/index.d.ts.map +1 -0
  130. package/dist/plugins/index.js +9 -0
  131. package/dist/plugins/index.mjs +140 -0
  132. package/dist/source.cjs +111 -0
  133. package/dist/source.d.ts +72 -0
  134. package/dist/source.d.ts.map +1 -0
  135. package/dist/source.js +153 -0
  136. package/dist/source.mjs +90 -0
  137. package/dist/timeline.cjs +350 -0
  138. package/dist/timeline.d.ts +30 -0
  139. package/dist/timeline.d.ts.map +1 -0
  140. package/dist/timeline.js +271 -0
  141. package/dist/timeline.mjs +327 -0
  142. package/dist/tree.cjs +574 -0
  143. package/dist/tree.d.ts +128 -0
  144. package/dist/tree.d.ts.map +1 -0
  145. package/dist/tree.js +645 -0
  146. package/dist/tree.mjs +551 -0
  147. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  148. package/dist/value-pane.cjs +223 -0
  149. package/dist/value-pane.d.ts +10 -0
  150. package/dist/value-pane.d.ts.map +1 -0
  151. package/dist/value-pane.js +61 -0
  152. package/dist/value-pane.mjs +200 -0
  153. package/dist/wire.cjs +109 -0
  154. package/dist/wire.d.ts +100 -0
  155. package/dist/wire.d.ts.map +1 -0
  156. package/dist/wire.js +93 -0
  157. package/dist/wire.mjs +89 -0
  158. package/package.json +17 -5
@@ -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,17 @@
1
+ import type { Commit } from './history.js';
2
+ import type { RNode } from './instrument.js';
3
+ export interface DetailPanel {
4
+ /** Show a commit. Opening a second one replaces the first rather than stacking. */
5
+ open(commit: Commit, graph: Map<number, RNode>): void;
6
+ close(): void;
7
+ readonly isOpen: boolean;
8
+ /** Remove the panel and its listeners. */
9
+ destroy(): void;
10
+ }
11
+ export interface DetailPanelOptions {
12
+ /** A node in the panel was clicked — the host can select it on the graph. */
13
+ onPick?(id: number): void;
14
+ }
15
+ export declare function createDetailPanel(host: HTMLElement, options?: DetailPanelOptions): DetailPanel;
16
+ export declare const DETAIL_PANEL_CSS = "\n.dp-panel {\n position: fixed; z-index: 40; display: flex; flex-direction: column;\n border: 1px solid var(--fx-line, #ffffff26); border-radius: 8px;\n background: var(--fx-panel, #14161c); color: var(--fx-label, #e7e9ee);\n box-shadow: 0 12px 40px rgba(0, 0, 0, .45); overflow: hidden;\n}\n.dp-panel[hidden] { display: none; }\n.dp-bar {\n display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;\n cursor: move; user-select: none;\n border-bottom: 1px solid var(--fx-line, #ffffff1f);\n}\n.dp-title { font-family: ui-monospace, monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n.dp-close {\n margin-left: auto; width: 22px; height: 20px; padding: 0; cursor: pointer; font: inherit; font-size: 11px;\n border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;\n background: transparent; color: var(--fx-muted, #8a90a2);\n}\n.dp-close:hover { color: var(--fx-label, #e7e9ee); }\n.dp-body { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; }\n.dp-change { font-family: ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }\n.dp-group { margin-top: 10px; }\n.dp-verb { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--fx-muted, #8a90a2); }\n.dp-nodes { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }\n.dp-node {\n padding: 2px 7px; cursor: pointer; font: inherit; font-family: ui-monospace, monospace; font-size: 11px;\n border: 1px solid var(--fx-line, #ffffff1f); border-radius: 999px;\n background: transparent; color: var(--fx-label, #e7e9ee);\n}\n.dp-node:hover { border-color: var(--fx-accent, #5eead4); }\n.dp-targets { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: var(--fx-label, #e7e9ee); }\n.dp-where { color: var(--fx-muted, #8a90a2); }\n/* Bottom-right, where every desktop puts it. */\n.dp-grip {\n position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize;\n background: linear-gradient(135deg, transparent 50%, var(--fx-line, #ffffff40) 50%);\n}\n\n.ch-list { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; }\n.ch-size { color: var(--fx-muted, #8a90a2); margin-bottom: 2px; }\n.ch-row { display: flex; gap: 6px; align-items: baseline; padding: 1px 0; }\n.ch-row.ch-stacked { align-items: flex-start; padding: 3px 0; }\n.ch-sign { flex: none; width: 9px; text-align: center; }\n.ch-added .ch-sign { color: var(--state, #34d399); }\n.ch-removed .ch-sign { color: var(--fx-write, #fb7185); }\n.ch-changed .ch-sign { color: var(--effect, #fbbf24); }\n.ch-key { flex: none; color: var(--fx-muted, #8a90a2); }\n.ch-sides { min-width: 0; flex: 1; overflow-wrap: anywhere; }\n.ch-stacked .ch-sides { display: flex; flex-direction: column; gap: 2px; }\n.ch-from { color: var(--fx-muted, #8a90a2); }\n.ch-row:not(.ch-stacked) .ch-from { text-decoration: line-through; }\n.ch-arrow { color: var(--fx-muted, #8a90a2); margin: 0 4px; }\n.ch-to { color: var(--fx-label, #e7e9ee); }\n.ch-more { color: var(--fx-muted, #8a90a2); margin-top: 2px; }\n/* In the corner: an object can be tall, and a button under it is a scroll away. */\n.ch-block { position: relative; border-radius: 4px; border: 1px solid var(--fx-line, #ffffff14); }\n.ch-block.ch-from { opacity: .72; }\n.ch-json { margin: 0; padding: 5px 7px; overflow-x: auto; white-space: pre; font: inherit; }\n.ch-copy {\n position: absolute; top: 3px; right: 3px; padding: 1px 6px; cursor: pointer;\n font: inherit; font-size: 9.5px; opacity: 0; transition: opacity .12s;\n border: 1px solid var(--fx-line, #ffffff26); border-radius: 3px;\n background: var(--fx-panel, #14161c); color: var(--fx-muted, #8a90a2);\n}\n.ch-block:hover .ch-copy, .ch-copy:focus { opacity: 1; }\n.ch-copy:hover { color: var(--fx-label, #e7e9ee); }\n";
17
+ //# sourceMappingURL=detail-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detail-panel.d.ts","sourceRoot":"","sources":["../src/detail-panel.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,MAAM,EAAc,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;AAE7C,MAAM,WAAW,WAAW;IAC1B,mFAAmF;IACnF,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,IAAI,CAAC;IACtD,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,0CAA0C;IAC1C,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,6EAA6E;IAC7E,MAAM,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AA2DD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAwIlG;AAED,eAAO,MAAM,gBAAgB,0vHAsCf,CAAC"}
@@ -0,0 +1,217 @@
1
+ // One commit, with room to read it.
2
+ //
3
+ // A column beside the graph is the wrong shape for six before-and-after objects, so the full
4
+ // view floats over the page, moved and resized by whoever is reading it.
5
+ //
6
+ // It does not block: the graph keeps updating behind it and stays clickable.
7
+ import { changeRows, handleCopyClick, CHANGE_CSS } from './change-view.js';
8
+ const escape = (text) => text.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
9
+ const clock = (t) => {
10
+ const d = new Date(t);
11
+ const pad = (n, w = 2) => String(n).padStart(w, '0');
12
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
13
+ };
14
+ const VERBS = {
15
+ write: 'written',
16
+ ran: 'ran',
17
+ created: 'made',
18
+ disposed: 'gone',
19
+ };
20
+ /** So it cannot be resized into a sliver. */
21
+ const MIN_W = 320;
22
+ const MIN_H = 200;
23
+ /**
24
+ * Pointer capture, for the reason the splitters need it: without it the events stop arriving
25
+ * when the pointer crosses the graph or an iframe, the release is never seen, and the panel
26
+ * keeps following the mouse.
27
+ */
28
+ function draggable(handle, onMove) {
29
+ handle.addEventListener('pointerdown', (event) => {
30
+ if (event.button !== 0)
31
+ return;
32
+ // Capturing here retargets the following click onto the bar, so the button never hears
33
+ // it. That is why the close button did nothing.
34
+ if (event.target.closest('button'))
35
+ return;
36
+ event.preventDefault();
37
+ event.stopPropagation();
38
+ handle.setPointerCapture(event.pointerId);
39
+ let lastX = event.clientX;
40
+ let lastY = event.clientY;
41
+ const move = (e) => {
42
+ onMove(e.clientX - lastX, e.clientY - lastY);
43
+ lastX = e.clientX;
44
+ lastY = e.clientY;
45
+ };
46
+ const stop = () => {
47
+ handle.removeEventListener('pointermove', move);
48
+ handle.removeEventListener('pointerup', stop);
49
+ handle.removeEventListener('pointercancel', stop);
50
+ handle.removeEventListener('lostpointercapture', stop);
51
+ if (handle.hasPointerCapture(event.pointerId))
52
+ handle.releasePointerCapture(event.pointerId);
53
+ };
54
+ handle.addEventListener('pointermove', move);
55
+ handle.addEventListener('pointerup', stop);
56
+ handle.addEventListener('pointercancel', stop);
57
+ handle.addEventListener('lostpointercapture', stop);
58
+ });
59
+ }
60
+ export function createDetailPanel(host, options = {}) {
61
+ const panel = host.ownerDocument.createElement('div');
62
+ panel.className = 'dp-panel';
63
+ panel.hidden = true;
64
+ panel.innerHTML =
65
+ '<div class="dp-bar"><span class="dp-title"></span>' +
66
+ '<button type="button" class="dp-close" title="Close" aria-label="Close">✕</button></div>' +
67
+ '<div class="dp-body"></div>' +
68
+ '<div class="dp-grip" title="Drag to resize"></div>';
69
+ host.appendChild(panel);
70
+ const title = panel.querySelector('.dp-title');
71
+ const body = panel.querySelector('.dp-body');
72
+ const bar = panel.querySelector('.dp-bar');
73
+ const grip = panel.querySelector('.dp-grip');
74
+ let x = 0;
75
+ let y = 0;
76
+ let width = 520;
77
+ let height = 380;
78
+ let open = false;
79
+ const place = () => {
80
+ panel.style.left = `${Math.round(x)}px`;
81
+ panel.style.top = `${Math.round(y)}px`;
82
+ panel.style.width = `${Math.round(width)}px`;
83
+ panel.style.height = `${Math.round(height)}px`;
84
+ };
85
+ draggable(bar, (dx, dy) => {
86
+ // Clamped, or a panel dragged off the top has no bar left to drag back by.
87
+ const view = panel.ownerDocument.defaultView;
88
+ x = Math.max(0, Math.min((view?.innerWidth ?? width) - 60, x + dx));
89
+ y = Math.max(0, Math.min((view?.innerHeight ?? height) - 30, y + dy));
90
+ place();
91
+ });
92
+ draggable(grip, (dx, dy) => {
93
+ width = Math.max(MIN_W, width + dx);
94
+ height = Math.max(MIN_H, height + dy);
95
+ place();
96
+ });
97
+ panel.querySelector('.dp-close')?.addEventListener('click', () => close());
98
+ body.addEventListener('click', (e) => {
99
+ // Copy first: the button sits inside a row.
100
+ if (handleCopyClick(e))
101
+ return;
102
+ const row = e.target.closest('.dp-node');
103
+ if (row?.dataset.id)
104
+ options.onPick?.(Number(row.dataset.id));
105
+ });
106
+ function close() {
107
+ open = false;
108
+ panel.hidden = true;
109
+ }
110
+ return {
111
+ open(commit, graph) {
112
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
113
+ const cause = commit.cause;
114
+ title.textContent = cause
115
+ ? `${nameOf(cause)} · ${clock(commit.t)}`
116
+ : `${commit.commit === 0 ? 'first render' : `commit ${commit.commit}`} · ${clock(commit.t)}`;
117
+ const headline = cause && cause.from !== undefined && cause.to !== undefined
118
+ ? `<div class="dp-change"><span class="ch-from">${escape(cause.from)}</span>` +
119
+ `<span class="ch-arrow">→</span><span class="ch-to">${escape(cause.to)}</span></div>`
120
+ : '';
121
+ // Every event: this view exists because the row could not hold them all.
122
+ const groups = ['write', 'ran', 'created', 'disposed']
123
+ .map((kind) => {
124
+ const rows = commit.events.filter((e) => e.kind === kind && e !== cause);
125
+ if (!rows.length)
126
+ return '';
127
+ const seen = new Map();
128
+ for (const event of rows) {
129
+ const found = seen.get(event.id);
130
+ if (found)
131
+ found.count++;
132
+ else
133
+ seen.set(event.id, { name: nameOf(event), count: 1 });
134
+ }
135
+ const items = [...seen]
136
+ .map(([id, { name, count }]) => `<button type="button" class="dp-node" data-id="${id}">${escape(name)}${count > 1 ? ` ×${count}` : ''}</button>`)
137
+ .join('');
138
+ return `<div class="dp-group"><div class="dp-verb">${VERBS[kind]}</div><div class="dp-nodes">${items}</div></div>`;
139
+ })
140
+ .join('');
141
+ // Where the value reaches the page — the last step of the chain.
142
+ const node = cause ? graph.get(cause.id) : undefined;
143
+ // Both: a template's holes can share one original position, and then only the compiled
144
+ // line tells them apart.
145
+ const where = (t) => {
146
+ if (!t.source)
147
+ return '';
148
+ const file = (t.source.file ?? '').split('/').pop() ?? '';
149
+ const out = t.source.compiled;
150
+ const tail = out && out.line !== t.source.line ? ` · out ${out.line}` : '';
151
+ return ` — ${file}:${t.source.line ?? ''}${tail}`;
152
+ };
153
+ const targets = node?.targets?.length
154
+ ? `<div class="dp-group"><div class="dp-verb">updates</div><div class="dp-targets">${node.targets
155
+ .map((t) => `<div>${escape(t.name ? `${t.name} of ${t.element}` : t.element)}` +
156
+ `<span class="dp-where">${escape(where(t))}</span></div>`)
157
+ .join('')}</div></div>`
158
+ : '';
159
+ body.innerHTML = headline + changeRows(cause?.change) + groups + targets;
160
+ if (!open) {
161
+ // Once per opening: jumping back to the middle on every entry would be unusable.
162
+ const view = panel.ownerDocument.defaultView;
163
+ x = Math.max(12, ((view?.innerWidth ?? 900) - width) / 2);
164
+ y = Math.max(12, ((view?.innerHeight ?? 600) - height) / 3);
165
+ place();
166
+ }
167
+ open = true;
168
+ panel.hidden = false;
169
+ },
170
+ close,
171
+ get isOpen() {
172
+ return open;
173
+ },
174
+ destroy() {
175
+ panel.remove();
176
+ },
177
+ };
178
+ }
179
+ export const DETAIL_PANEL_CSS = `
180
+ .dp-panel {
181
+ position: fixed; z-index: 40; display: flex; flex-direction: column;
182
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 8px;
183
+ background: var(--fx-panel, #14161c); color: var(--fx-label, #e7e9ee);
184
+ box-shadow: 0 12px 40px rgba(0, 0, 0, .45); overflow: hidden;
185
+ }
186
+ .dp-panel[hidden] { display: none; }
187
+ .dp-bar {
188
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px 6px 12px;
189
+ cursor: move; user-select: none;
190
+ border-bottom: 1px solid var(--fx-line, #ffffff1f);
191
+ }
192
+ .dp-title { font-family: ui-monospace, monospace; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
193
+ .dp-close {
194
+ margin-left: auto; width: 22px; height: 20px; padding: 0; cursor: pointer; font: inherit; font-size: 11px;
195
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
196
+ background: transparent; color: var(--fx-muted, #8a90a2);
197
+ }
198
+ .dp-close:hover { color: var(--fx-label, #e7e9ee); }
199
+ .dp-body { flex: 1; min-height: 0; overflow: auto; padding: 10px 12px; }
200
+ .dp-change { font-family: ui-monospace, monospace; font-size: 12px; overflow-wrap: anywhere; }
201
+ .dp-group { margin-top: 10px; }
202
+ .dp-verb { font-size: 10.5px; text-transform: uppercase; letter-spacing: 1px; color: var(--fx-muted, #8a90a2); }
203
+ .dp-nodes { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }
204
+ .dp-node {
205
+ padding: 2px 7px; cursor: pointer; font: inherit; font-family: ui-monospace, monospace; font-size: 11px;
206
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 999px;
207
+ background: transparent; color: var(--fx-label, #e7e9ee);
208
+ }
209
+ .dp-node:hover { border-color: var(--fx-accent, #5eead4); }
210
+ .dp-targets { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; color: var(--fx-label, #e7e9ee); }
211
+ .dp-where { color: var(--fx-muted, #8a90a2); }
212
+ /* Bottom-right, where every desktop puts it. */
213
+ .dp-grip {
214
+ position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize;
215
+ background: linear-gradient(135deg, transparent 50%, var(--fx-line, #ffffff40) 50%);
216
+ }
217
+ ${CHANGE_CSS}`;