@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,271 @@
1
+ // The history, as something to read.
2
+ //
3
+ // Commits, not a flat log: one click makes a write, four runs and a disposal, interleaved with
4
+ // whatever the router was doing. Grouped, they read as "this is what that click did".
5
+ //
6
+ // Newest first, and re-rendered only when it would say something different — a panel that
7
+ // repaints on every poll cannot be scrolled or selected.
8
+ import { toCommits } from './history.js';
9
+ import { changeRows, handleCopyClick, CHANGE_CSS } from './change-view.js';
10
+ const escape = (text) => text.replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c]);
11
+ const clock = (t) => {
12
+ const d = new Date(t);
13
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
14
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
15
+ };
16
+ /** The name of what the commit started from: `count`, or what to say when there is none. */
17
+ function subject(commit, nameOf) {
18
+ if (!commit.cause)
19
+ return commit.commit === 0 ? 'first render' : 'continued';
20
+ return nameOf(commit.cause);
21
+ }
22
+ /** `0 → 1`, when the write carried both sides of it. */
23
+ function change(commit) {
24
+ const cause = commit.cause;
25
+ if (!cause || cause.from === undefined || cause.to === undefined)
26
+ return '';
27
+ return `${cause.from} → ${cause.to}`;
28
+ }
29
+ /** The whole of it, for the tooltip: the row elides, this does not. */
30
+ function detail(commit, nameOf) {
31
+ const lines = [`${subject(commit, nameOf)} ${change(commit)}`.trim()];
32
+ const named = (kind, verb) => {
33
+ const ids = commit.events.filter((e) => e.kind === kind && e !== commit.cause).map(nameOf);
34
+ if (ids.length)
35
+ lines.push(`${verb}: ${[...new Set(ids)].join(', ')}`);
36
+ };
37
+ named('ran', 'ran');
38
+ named('write', 'written');
39
+ named('created', 'made');
40
+ named('disposed', 'gone');
41
+ return lines.join('\n');
42
+ }
43
+ /** "2 ran · 1 created" — what the commit caused, without listing every line of it. */
44
+ function summary(commit) {
45
+ const counts = { ran: 0, created: 0, disposed: 0, write: 0 };
46
+ for (const event of commit.events)
47
+ counts[event.kind]++;
48
+ // The opening write is the title, so it is not also a thing the commit did.
49
+ const writes = counts.write - (commit.cause ? 1 : 0);
50
+ return [
51
+ counts.ran && `${counts.ran} ran`,
52
+ writes > 0 && `${writes} written`,
53
+ counts.created && `${counts.created} made`,
54
+ counts.disposed && `${counts.disposed} gone`,
55
+ ]
56
+ .filter(Boolean)
57
+ .join(' · ');
58
+ }
59
+ export function createTimeline(host, options = {}) {
60
+ const empty = options.empty ?? 'nothing has happened yet';
61
+ const limit = options.limit ?? 100;
62
+ let events = [];
63
+ /** The highest seq folded in. Every producer counts up, so this is all dedupe needs. */
64
+ let seen = -1;
65
+ /** Selected, expanded and lit on the graph are one state: all three say "this one". */
66
+ let selected = null;
67
+ let filter = '';
68
+ let signature = '';
69
+ /**
70
+ * Held still on request.
71
+ *
72
+ * A page that writes on every frame fills this faster than anyone can read it, and the
73
+ * entry being looked at scrolls away. Dropped rather than buffered: catching up on a
74
+ * thousand events nobody watched is not what the button is for.
75
+ */
76
+ let paused = false;
77
+ // Built once: re-rendering the box being typed in would take the caret with it.
78
+ host.innerHTML =
79
+ '<div class="tl-head">' +
80
+ '<input class="tl-filter" type="search" placeholder="filter by name or value" aria-label="Filter the timeline" />' +
81
+ '<button type="button" class="tl-pause" title="Stop taking new updates, so the list holds still while you read it">❚❚</button>' +
82
+ '</div>' +
83
+ '<div class="tl-list"></div>';
84
+ const input = host.querySelector('.tl-filter');
85
+ const list = host.querySelector('.tl-list');
86
+ const pause = host.querySelector('.tl-pause');
87
+ const all = () => toCommits(events).slice(-limit).reverse();
88
+ const idsOf = (commit) => {
89
+ const ids = new Set();
90
+ for (const event of commit.events) {
91
+ ids.add(event.id);
92
+ if (event.by !== undefined)
93
+ ids.add(event.by);
94
+ }
95
+ return [...ids];
96
+ };
97
+ /** Everything the row says, which is what a reader is searching against. */
98
+ const haystack = (commit, nameOf) => `${detail(commit, nameOf)} ${summary(commit)}`.toLowerCase();
99
+ const shown = (nameOf) => filter ? all().filter((c) => haystack(c, nameOf).includes(filter)) : all();
100
+ input.addEventListener('input', () => {
101
+ filter = input.value.trim().toLowerCase();
102
+ signature = '';
103
+ });
104
+ const showPaused = () => {
105
+ pause.textContent = paused ? '▶' : '❚❚';
106
+ pause.classList.toggle('on', paused);
107
+ pause.title = paused
108
+ ? 'Taking updates again'
109
+ : 'Stop taking new updates, so the list holds still while you read it';
110
+ };
111
+ pause.addEventListener('click', () => {
112
+ paused = !paused;
113
+ showPaused();
114
+ signature = '';
115
+ });
116
+ list.addEventListener('click', (e) => {
117
+ // Copying from an open row must not also shut it.
118
+ if (handleCopyClick(e))
119
+ return;
120
+ const row = e.target?.closest?.('.tl-commit');
121
+ if (!row)
122
+ return;
123
+ const number = Number(row.dataset.commit);
124
+ // Inside the row, so it would otherwise shut what it was opening.
125
+ if (e.target.classList.contains('tl-more')) {
126
+ const full = all().find((c) => c.commit === number);
127
+ if (full)
128
+ options.onExpand?.(full);
129
+ return;
130
+ }
131
+ // Clicking the open one shuts it — the only way back to watching everything.
132
+ selected = selected === number ? null : number;
133
+ const commit = all().find((c) => c.commit === selected) ?? null;
134
+ options.onSelect?.(commit ? idsOf(commit) : [], commit);
135
+ signature = '';
136
+ });
137
+ return {
138
+ setPaused(next) {
139
+ paused = next;
140
+ showPaused();
141
+ signature = '';
142
+ },
143
+ get paused() {
144
+ return paused;
145
+ },
146
+ push(incoming) {
147
+ if (paused)
148
+ return;
149
+ let added = false;
150
+ for (const event of incoming) {
151
+ if (event.seq <= seen)
152
+ continue;
153
+ events.push(event);
154
+ seen = event.seq;
155
+ added = true;
156
+ }
157
+ if (!added)
158
+ return;
159
+ // Bounded by commits: cutting mid-commit shows a click with half of what it caused.
160
+ if (events.length > limit * 40)
161
+ events = events.slice(-limit * 40);
162
+ },
163
+ render(graph) {
164
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
165
+ const rows = shown(nameOf);
166
+ const next = `${seen}|${selected ?? ''}|${filter}|${rows.length}|${graph.size}`;
167
+ if (next === signature)
168
+ return;
169
+ signature = next;
170
+ if (!rows.length) {
171
+ // With a filter typed, "nothing has happened" would read as recording being broken.
172
+ list.innerHTML = `<span class="tl-empty">${escape(filter ? 'nothing matches' : empty)}</span>`;
173
+ return;
174
+ }
175
+ list.innerHTML = rows
176
+ .map((commit) => {
177
+ const open = commit.commit === selected;
178
+ const did = summary(commit);
179
+ const moved = change(commit);
180
+ const parts = [
181
+ ['ran', 'ran'],
182
+ ['write', 'written'],
183
+ ['created', 'made'],
184
+ ['disposed', 'gone'],
185
+ ];
186
+ const body = parts
187
+ .map(([kind, verb]) => {
188
+ const names = [
189
+ ...new Set(commit.events.filter((e) => e.kind === kind && e !== commit.cause).map(nameOf)),
190
+ ];
191
+ return names.length
192
+ ? `<div class="tl-part"><span class="tl-verb">${verb}</span> ${escape(names.join(', '))}</div>`
193
+ : '';
194
+ })
195
+ .join('');
196
+ return (`<div class="tl-commit${open ? ' selected' : ''}"` +
197
+ ` data-commit="${commit.commit}" title="${escape(detail(commit, nameOf))}">` +
198
+ '<div class="tl-line">' +
199
+ `<span class="tl-caret">${open ? '▾' : '▸'}</span>` +
200
+ `<span class="tl-time">${clock(commit.t)}</span>` +
201
+ `<span class="tl-name">${escape(subject(commit, nameOf))}</span>` +
202
+ (did ? `<span class="tl-did">${escape(did)}</span>` : '') +
203
+ '</div>' +
204
+ // Open, the value gets the width and wraps.
205
+ (open
206
+ ? `<div class="tl-body">${moved ? `<div class="tl-change">${escape(moved)}</div>` : ''}${changeRows(commit.cause?.change, 6)}${body}${
207
+ // Only worth offering when there is more than the row can hold.
208
+ options.onExpand && (commit.cause?.change || commit.events.length > 6)
209
+ ? '<button type="button" class="tl-more">open in full</button>'
210
+ : ''}</div>`
211
+ : '') +
212
+ '</div>');
213
+ })
214
+ .join('');
215
+ },
216
+ selected() {
217
+ const commit = all().find((c) => c.commit === selected);
218
+ return commit ? idsOf(commit) : [];
219
+ },
220
+ clear() {
221
+ events = [];
222
+ seen = -1;
223
+ selected = null;
224
+ signature = '';
225
+ // The filter box stays: a new run is usually a rerun of what they were narrowing to.
226
+ list.innerHTML = `<span class="tl-empty">${escape(empty)}</span>`;
227
+ },
228
+ };
229
+ }
230
+ /** Default styling for what createTimeline writes, in the shape DEVTOOLS_CSS is used. */
231
+ export const TIMELINE_CSS = `
232
+ .tl-head { position: relative; display: flex; gap: 4px; padding: 0 .25rem; }
233
+ .tl-pause {
234
+ position: absolute; right: .4rem; top: 40%; transform: translateY(-50%);
235
+ flex: none; width: 26px; cursor: pointer; font: inherit; font-size: 10px;
236
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
237
+ background: transparent; color: var(--fx-muted, #8a90a2);
238
+ }
239
+ .tl-pause:hover { color: var(--fx-label, #e7e9ee); }
240
+ .tl-pause.on { color: var(--fx-accent, #5eead4); border-color: var(--fx-accent, #5eead4); }
241
+ .tl-filter {
242
+ flex: 1; min-width: 0; box-sizing: border-box; padding: 3px 6px;
243
+ font: inherit; font-size: 11.5px; border-radius: 4px;
244
+ border: 1px solid var(--fx-line, #ffffff1f);
245
+ background: var(--fx-input, transparent); color: var(--fx-label, #e7e9ee);
246
+ padding-inline-end: 2rem;
247
+ }
248
+ .tl-empty { color: var(--fx-muted, #8a90a2); font-size: 12px; }
249
+ .tl-commit { padding: 4px 6px; border-radius: 4px; cursor: pointer; font-size: 11.5px; }
250
+ .tl-commit:hover { background: var(--fx-hover, #ffffff10); }
251
+ .tl-commit.selected { background: var(--fx-selected, #ffffff1a); }
252
+ .tl-line { display: flex; align-items: baseline; gap: 6px; }
253
+ .tl-caret { color: var(--fx-muted, #8a90a2); font-size: 9px; flex: none; }
254
+ .tl-time { font-family: ui-monospace, monospace; font-size: 10px; color: var(--fx-muted, #8a90a2); flex: none; }
255
+ .tl-name { color: var(--fx-label, #e7e9ee); font-family: ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
256
+ .tl-did { margin-left: auto; color: var(--fx-muted, #8a90a2); flex: none; }
257
+ .tl-body { margin: 3px 0 2px 15px; }
258
+ /* Wraps: an open entry exists to show the whole value. */
259
+ .tl-change {
260
+ font-family: ui-monospace, monospace; font-size: 11px;
261
+ color: var(--fx-accent, #5eead4); overflow-wrap: anywhere; white-space: pre-wrap;
262
+ }
263
+ .tl-part { margin-top: 2px; color: var(--fx-label, #e7e9ee); font-size: 11px; overflow-wrap: anywhere; }
264
+ .tl-verb { color: var(--fx-muted, #8a90a2); }
265
+ .tl-more {
266
+ margin-top: 5px; padding: 2px 8px; font: inherit; font-size: 10.5px; cursor: pointer;
267
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
268
+ background: transparent; color: var(--fx-muted, #8a90a2);
269
+ }
270
+ .tl-more:hover { color: var(--fx-label, #e7e9ee); }
271
+ ${CHANGE_CSS}`;
@@ -0,0 +1,327 @@
1
+ // src/history.ts
2
+ function toCommits(events) {
3
+ const byNumber = /* @__PURE__ */ new Map();
4
+ for (const event of events) {
5
+ const bucket = byNumber.get(event.commit);
6
+ if (bucket) bucket.push(event);
7
+ else byNumber.set(event.commit, [event]);
8
+ }
9
+ return [...byNumber.entries()].sort((a, b) => a[0] - b[0]).map(([commit, group]) => ({
10
+ commit,
11
+ t: group[0].t,
12
+ events: group,
13
+ cause: group.find((e) => e.kind === "write" && e.by === void 0)
14
+ }));
15
+ }
16
+
17
+ // src/copy.ts
18
+ function confirmOn(button, said = "copied") {
19
+ if (!button) return;
20
+ const was = button.textContent;
21
+ button.textContent = said;
22
+ setTimeout(() => {
23
+ button.textContent = was;
24
+ }, 1200);
25
+ }
26
+ function fallback(text, button) {
27
+ try {
28
+ const doc = button?.ownerDocument ?? document;
29
+ const area = doc.createElement("textarea");
30
+ area.value = text;
31
+ area.setAttribute("readonly", "");
32
+ area.style.position = "fixed";
33
+ area.style.opacity = "0";
34
+ doc.body.appendChild(area);
35
+ area.select();
36
+ doc.execCommand("copy");
37
+ area.remove();
38
+ confirmOn(button);
39
+ return true;
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ async function copyText(text, button) {
45
+ const clipboard = globalThis.navigator?.clipboard;
46
+ if (clipboard?.writeText) {
47
+ try {
48
+ await clipboard.writeText(text);
49
+ confirmOn(button);
50
+ return true;
51
+ } catch {
52
+ }
53
+ }
54
+ return fallback(text, button);
55
+ }
56
+
57
+ // src/change-view.ts
58
+ var escape = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
59
+ var SIGN = { added: "+", removed: "−", changed: "~" };
60
+ function pretty(json) {
61
+ if (!json) return void 0;
62
+ try {
63
+ return JSON.stringify(JSON.parse(json), null, 2);
64
+ } catch {
65
+ return void 0;
66
+ }
67
+ }
68
+ function sideOf(text, json, cls) {
69
+ const block = pretty(json);
70
+ if (!block) return `<span class="ch-${cls}">${escape(text ?? "")}</span>`;
71
+ 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>`;
72
+ }
73
+ function changeRows(change2, limit = Infinity) {
74
+ if (!change2?.entries?.length) return "";
75
+ const shown = change2.entries.slice(0, limit);
76
+ const hidden = change2.entries.length - shown.length + (change2.more ?? 0);
77
+ const rows = shown.map((entry) => {
78
+ const from = sideOf(entry.from, entry.fromJson, "from");
79
+ const to = sideOf(entry.to, entry.toJson, "to");
80
+ const sides = entry.op === "changed" ? `${from}<span class="ch-arrow">→</span>${to}` : entry.op === "added" ? to : from;
81
+ const stacked = entry.fromJson || entry.toJson ? " ch-stacked" : "";
82
+ 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>`;
83
+ }).join("");
84
+ const size = change2.length ? `<div class="ch-size">${change2.length.from} → ${change2.length.to} items</div>` : "";
85
+ return `<div class="ch-list">${size}${rows}${hidden > 0 ? `<div class="ch-more">and ${hidden} more</div>` : ""}</div>`;
86
+ }
87
+ function handleCopyClick(event) {
88
+ const button = event.target?.closest?.(".ch-copy");
89
+ if (!button) return false;
90
+ void copyText(button.dataset.copy ?? "", button);
91
+ return true;
92
+ }
93
+ var CHANGE_CSS = `
94
+ .ch-list { margin-top: 4px; font-family: ui-monospace, monospace; font-size: 11px; }
95
+ .ch-size { color: var(--fx-muted, #8a90a2); margin-bottom: 2px; }
96
+ .ch-row { display: flex; gap: 6px; align-items: baseline; padding: 1px 0; }
97
+ .ch-row.ch-stacked { align-items: flex-start; padding: 3px 0; }
98
+ .ch-sign { flex: none; width: 9px; text-align: center; }
99
+ .ch-added .ch-sign { color: var(--state, #34d399); }
100
+ .ch-removed .ch-sign { color: var(--fx-write, #fb7185); }
101
+ .ch-changed .ch-sign { color: var(--effect, #fbbf24); }
102
+ .ch-key { flex: none; color: var(--fx-muted, #8a90a2); }
103
+ .ch-sides { min-width: 0; flex: 1; overflow-wrap: anywhere; }
104
+ .ch-stacked .ch-sides { display: flex; flex-direction: column; gap: 2px; }
105
+ .ch-from { color: var(--fx-muted, #8a90a2); }
106
+ .ch-row:not(.ch-stacked) .ch-from { text-decoration: line-through; }
107
+ .ch-arrow { color: var(--fx-muted, #8a90a2); margin: 0 4px; }
108
+ .ch-to { color: var(--fx-label, #e7e9ee); }
109
+ .ch-more { color: var(--fx-muted, #8a90a2); margin-top: 2px; }
110
+ /* In the corner: an object can be tall, and a button under it is a scroll away. */
111
+ .ch-block { position: relative; border-radius: 4px; border: 1px solid var(--fx-line, #ffffff14); }
112
+ .ch-block.ch-from { opacity: .72; }
113
+ .ch-json { margin: 0; padding: 5px 7px; overflow-x: auto; white-space: pre; font: inherit; }
114
+ .ch-copy {
115
+ position: absolute; top: 3px; right: 3px; padding: 1px 6px; cursor: pointer;
116
+ font: inherit; font-size: 9.5px; opacity: 0; transition: opacity .12s;
117
+ border: 1px solid var(--fx-line, #ffffff26); border-radius: 3px;
118
+ background: var(--fx-panel, #14161c); color: var(--fx-muted, #8a90a2);
119
+ }
120
+ .ch-block:hover .ch-copy, .ch-copy:focus { opacity: 1; }
121
+ .ch-copy:hover { color: var(--fx-label, #e7e9ee); }
122
+ `;
123
+
124
+ // src/timeline.ts
125
+ var escape2 = (text) => text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
126
+ var clock = (t) => {
127
+ const d = new Date(t);
128
+ const pad = (n, width = 2) => String(n).padStart(width, "0");
129
+ return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}`;
130
+ };
131
+ function subject(commit, nameOf) {
132
+ if (!commit.cause) return commit.commit === 0 ? "first render" : "continued";
133
+ return nameOf(commit.cause);
134
+ }
135
+ function change(commit) {
136
+ const cause = commit.cause;
137
+ if (!cause || cause.from === void 0 || cause.to === void 0) return "";
138
+ return `${cause.from} → ${cause.to}`;
139
+ }
140
+ function detail(commit, nameOf) {
141
+ const lines = [`${subject(commit, nameOf)} ${change(commit)}`.trim()];
142
+ const named = (kind, verb) => {
143
+ const ids = commit.events.filter((e) => e.kind === kind && e !== commit.cause).map(nameOf);
144
+ if (ids.length) lines.push(`${verb}: ${[...new Set(ids)].join(", ")}`);
145
+ };
146
+ named("ran", "ran");
147
+ named("write", "written");
148
+ named("created", "made");
149
+ named("disposed", "gone");
150
+ return lines.join("\n");
151
+ }
152
+ function summary(commit) {
153
+ const counts = { ran: 0, created: 0, disposed: 0, write: 0 };
154
+ for (const event of commit.events) counts[event.kind]++;
155
+ const writes = counts.write - (commit.cause ? 1 : 0);
156
+ return [
157
+ counts.ran && `${counts.ran} ran`,
158
+ writes > 0 && `${writes} written`,
159
+ counts.created && `${counts.created} made`,
160
+ counts.disposed && `${counts.disposed} gone`
161
+ ].filter(Boolean).join(" · ");
162
+ }
163
+ function createTimeline(host, options = {}) {
164
+ const empty = options.empty ?? "nothing has happened yet";
165
+ const limit = options.limit ?? 100;
166
+ let events = [];
167
+ let seen = -1;
168
+ let selected = null;
169
+ let filter = "";
170
+ let signature = "";
171
+ let paused = false;
172
+ host.innerHTML = '<div class="tl-head"><input class="tl-filter" type="search" placeholder="filter by name or value" aria-label="Filter the timeline" /><button type="button" class="tl-pause" title="Stop taking new updates, so the list holds still while you read it">❚❚</button></div><div class="tl-list"></div>';
173
+ const input = host.querySelector(".tl-filter");
174
+ const list = host.querySelector(".tl-list");
175
+ const pause = host.querySelector(".tl-pause");
176
+ const all = () => toCommits(events).slice(-limit).reverse();
177
+ const idsOf = (commit) => {
178
+ const ids = /* @__PURE__ */ new Set();
179
+ for (const event of commit.events) {
180
+ ids.add(event.id);
181
+ if (event.by !== void 0) ids.add(event.by);
182
+ }
183
+ return [...ids];
184
+ };
185
+ const haystack = (commit, nameOf) => `${detail(commit, nameOf)} ${summary(commit)}`.toLowerCase();
186
+ const shown = (nameOf) => filter ? all().filter((c) => haystack(c, nameOf).includes(filter)) : all();
187
+ input.addEventListener("input", () => {
188
+ filter = input.value.trim().toLowerCase();
189
+ signature = "";
190
+ });
191
+ const showPaused = () => {
192
+ pause.textContent = paused ? "▶" : "❚❚";
193
+ pause.classList.toggle("on", paused);
194
+ pause.title = paused ? "Taking updates again" : "Stop taking new updates, so the list holds still while you read it";
195
+ };
196
+ pause.addEventListener("click", () => {
197
+ paused = !paused;
198
+ showPaused();
199
+ signature = "";
200
+ });
201
+ list.addEventListener("click", (e) => {
202
+ if (handleCopyClick(e)) return;
203
+ const row = e.target?.closest?.(".tl-commit");
204
+ if (!row) return;
205
+ const number = Number(row.dataset.commit);
206
+ if (e.target.classList.contains("tl-more")) {
207
+ const full = all().find((c) => c.commit === number);
208
+ if (full) options.onExpand?.(full);
209
+ return;
210
+ }
211
+ selected = selected === number ? null : number;
212
+ const commit = all().find((c) => c.commit === selected) ?? null;
213
+ options.onSelect?.(commit ? idsOf(commit) : [], commit);
214
+ signature = "";
215
+ });
216
+ return {
217
+ setPaused(next) {
218
+ paused = next;
219
+ showPaused();
220
+ signature = "";
221
+ },
222
+ get paused() {
223
+ return paused;
224
+ },
225
+ push(incoming) {
226
+ if (paused) return;
227
+ let added = false;
228
+ for (const event of incoming) {
229
+ if (event.seq <= seen) continue;
230
+ events.push(event);
231
+ seen = event.seq;
232
+ added = true;
233
+ }
234
+ if (!added) return;
235
+ if (events.length > limit * 40) events = events.slice(-limit * 40);
236
+ },
237
+ render(graph) {
238
+ const nameOf = (e) => graph.get(e.id)?.label ?? e.label ?? `#${e.id}`;
239
+ const rows = shown(nameOf);
240
+ const next = `${seen}|${selected ?? ""}|${filter}|${rows.length}|${graph.size}`;
241
+ if (next === signature) return;
242
+ signature = next;
243
+ if (!rows.length) {
244
+ list.innerHTML = `<span class="tl-empty">${escape2(filter ? "nothing matches" : empty)}</span>`;
245
+ return;
246
+ }
247
+ list.innerHTML = rows.map((commit) => {
248
+ const open = commit.commit === selected;
249
+ const did = summary(commit);
250
+ const moved = change(commit);
251
+ const parts = [
252
+ ["ran", "ran"],
253
+ ["write", "written"],
254
+ ["created", "made"],
255
+ ["disposed", "gone"]
256
+ ];
257
+ const body = parts.map(([kind, verb]) => {
258
+ const names = [
259
+ ...new Set(
260
+ commit.events.filter((e) => e.kind === kind && e !== commit.cause).map(nameOf)
261
+ )
262
+ ];
263
+ return names.length ? `<div class="tl-part"><span class="tl-verb">${verb}</span> ${escape2(names.join(", "))}</div>` : "";
264
+ }).join("");
265
+ return `<div class="tl-commit${open ? " selected" : ""}" data-commit="${commit.commit}" title="${escape2(detail(commit, nameOf))}"><div class="tl-line"><span class="tl-caret">${open ? "▾" : "▸"}</span><span class="tl-time">${clock(commit.t)}</span><span class="tl-name">${escape2(subject(commit, nameOf))}</span>` + (did ? `<span class="tl-did">${escape2(did)}</span>` : "") + "</div>" + // Open, the value gets the width and wraps.
266
+ (open ? `<div class="tl-body">${moved ? `<div class="tl-change">${escape2(moved)}</div>` : ""}${changeRows(commit.cause?.change, 6)}${body}${// Only worth offering when there is more than the row can hold.
267
+ options.onExpand && (commit.cause?.change || commit.events.length > 6) ? '<button type="button" class="tl-more">open in full</button>' : ""}</div>` : "") + "</div>";
268
+ }).join("");
269
+ },
270
+ selected() {
271
+ const commit = all().find((c) => c.commit === selected);
272
+ return commit ? idsOf(commit) : [];
273
+ },
274
+ clear() {
275
+ events = [];
276
+ seen = -1;
277
+ selected = null;
278
+ signature = "";
279
+ list.innerHTML = `<span class="tl-empty">${escape2(empty)}</span>`;
280
+ }
281
+ };
282
+ }
283
+ var TIMELINE_CSS = `
284
+ .tl-head { position: relative; display: flex; gap: 4px; padding: 0 .25rem; }
285
+ .tl-pause {
286
+ position: absolute; right: .4rem; top: 40%; transform: translateY(-50%);
287
+ flex: none; width: 26px; cursor: pointer; font: inherit; font-size: 10px;
288
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
289
+ background: transparent; color: var(--fx-muted, #8a90a2);
290
+ }
291
+ .tl-pause:hover { color: var(--fx-label, #e7e9ee); }
292
+ .tl-pause.on { color: var(--fx-accent, #5eead4); border-color: var(--fx-accent, #5eead4); }
293
+ .tl-filter {
294
+ flex: 1; min-width: 0; box-sizing: border-box; padding: 3px 6px;
295
+ font: inherit; font-size: 11.5px; border-radius: 4px;
296
+ border: 1px solid var(--fx-line, #ffffff1f);
297
+ background: var(--fx-input, transparent); color: var(--fx-label, #e7e9ee);
298
+ padding-inline-end: 2rem;
299
+ }
300
+ .tl-empty { color: var(--fx-muted, #8a90a2); font-size: 12px; }
301
+ .tl-commit { padding: 4px 6px; border-radius: 4px; cursor: pointer; font-size: 11.5px; }
302
+ .tl-commit:hover { background: var(--fx-hover, #ffffff10); }
303
+ .tl-commit.selected { background: var(--fx-selected, #ffffff1a); }
304
+ .tl-line { display: flex; align-items: baseline; gap: 6px; }
305
+ .tl-caret { color: var(--fx-muted, #8a90a2); font-size: 9px; flex: none; }
306
+ .tl-time { font-family: ui-monospace, monospace; font-size: 10px; color: var(--fx-muted, #8a90a2); flex: none; }
307
+ .tl-name { color: var(--fx-label, #e7e9ee); font-family: ui-monospace, monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
308
+ .tl-did { margin-left: auto; color: var(--fx-muted, #8a90a2); flex: none; }
309
+ .tl-body { margin: 3px 0 2px 15px; }
310
+ /* Wraps: an open entry exists to show the whole value. */
311
+ .tl-change {
312
+ font-family: ui-monospace, monospace; font-size: 11px;
313
+ color: var(--fx-accent, #5eead4); overflow-wrap: anywhere; white-space: pre-wrap;
314
+ }
315
+ .tl-part { margin-top: 2px; color: var(--fx-label, #e7e9ee); font-size: 11px; overflow-wrap: anywhere; }
316
+ .tl-verb { color: var(--fx-muted, #8a90a2); }
317
+ .tl-more {
318
+ margin-top: 5px; padding: 2px 8px; font: inherit; font-size: 10.5px; cursor: pointer;
319
+ border: 1px solid var(--fx-line, #ffffff1f); border-radius: 4px;
320
+ background: transparent; color: var(--fx-muted, #8a90a2);
321
+ }
322
+ .tl-more:hover { color: var(--fx-label, #e7e9ee); }
323
+ ${CHANGE_CSS}`;
324
+ export {
325
+ TIMELINE_CSS,
326
+ createTimeline
327
+ };