aberdeen 1.18.2 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +9 -0
  2. package/dist/src/aberdeen.d.ts +27 -0
  3. package/{dist-docs/assets/aberdeen/aberdeen.js → dist/src/aberdeen.dev.js} +776 -15
  4. package/dist/src/aberdeen.dev.js.map +12 -0
  5. package/dist/src/aberdeen.js +59 -13
  6. package/dist/src/aberdeen.js.map +3 -3
  7. package/dist/src/devtools.d.ts +26 -0
  8. package/html-to-aberdeen +9 -5
  9. package/package.json +17 -10
  10. package/skill/A.md +127 -0
  11. package/skill/Dispatcher.md +51 -0
  12. package/skill/OPAQUE.md +15 -0
  13. package/skill/PromiseProxy.md +25 -0
  14. package/skill/Route.md +50 -0
  15. package/skill/SKILL.md +272 -156
  16. package/skill/applyCanon.md +20 -0
  17. package/skill/applyPrediction.md +15 -0
  18. package/skill/back.md +11 -0
  19. package/skill/clean.md +43 -0
  20. package/skill/clone.md +15 -0
  21. package/skill/copy.md +40 -0
  22. package/skill/count.md +32 -0
  23. package/skill/cssVars.md +15 -0
  24. package/skill/darkMode.md +26 -0
  25. package/skill/derive.md +56 -0
  26. package/skill/developer-tools.md +23 -0
  27. package/skill/disableCreateDestroy.md +11 -0
  28. package/skill/dump.md +37 -0
  29. package/skill/freeze.md +20 -0
  30. package/skill/full-example-multi-page-app.md +146 -0
  31. package/skill/go.md +25 -0
  32. package/skill/grow.md +13 -0
  33. package/skill/html-to-aberdeen.md +9 -0
  34. package/skill/insertCss.md +79 -0
  35. package/skill/insertGlobalCss.md +68 -0
  36. package/skill/interceptLinks.md +19 -0
  37. package/skill/invertString.md +32 -0
  38. package/skill/isEmpty.md +38 -0
  39. package/skill/map.md +16 -0
  40. package/skill/matchCurrent.md +27 -0
  41. package/skill/merge.md +27 -0
  42. package/skill/mount.md +48 -0
  43. package/skill/multiMap.md +15 -0
  44. package/skill/onEach.md +14 -0
  45. package/skill/partition.md +15 -0
  46. package/skill/peek.md +36 -0
  47. package/skill/persistScroll.md +13 -0
  48. package/skill/proxy.md +11 -0
  49. package/skill/push.md +13 -0
  50. package/skill/ref.md +39 -0
  51. package/skill/runQueue.md +34 -0
  52. package/skill/setErrorHandler.md +48 -0
  53. package/skill/setLog.md +9 -0
  54. package/skill/setSpacingCssVars.md +31 -0
  55. package/skill/shrink.md +13 -0
  56. package/skill/unmountAll.md +9 -0
  57. package/skill/unproxy.md +43 -0
  58. package/skill/up.md +13 -0
  59. package/src/aberdeen.ts +194 -73
  60. package/src/devtools.ts +685 -0
  61. package/dist-docs/.nojekyll +0 -1
  62. package/dist-docs/Tutorial/index.html +0 -305
  63. package/dist-docs/aberdeen/A/index.html +0 -116
  64. package/dist-docs/aberdeen/CUSTOM_DUMP/index.html +0 -7
  65. package/dist-docs/aberdeen/NO_COPY/index.html +0 -2
  66. package/dist-docs/aberdeen/OPAQUE/index.html +0 -12
  67. package/dist-docs/aberdeen/PromiseProxy/index.html +0 -8
  68. package/dist-docs/aberdeen/clean/index.html +0 -11
  69. package/dist-docs/aberdeen/clone/index.html +0 -5
  70. package/dist-docs/aberdeen/copy/index.html +0 -22
  71. package/dist-docs/aberdeen/count/index.html +0 -7
  72. package/dist-docs/aberdeen/cssVars/index.html +0 -11
  73. package/dist-docs/aberdeen/darkMode/index.html +0 -9
  74. package/dist-docs/aberdeen/default/index.html +0 -342
  75. package/dist-docs/aberdeen/derive/index.html +0 -18
  76. package/dist-docs/aberdeen/disableCreateDestroy/index.html +0 -6
  77. package/dist-docs/aberdeen/dump/index.html +0 -11
  78. package/dist-docs/aberdeen/index.html +0 -54
  79. package/dist-docs/aberdeen/insertCss/index.html +0 -30
  80. package/dist-docs/aberdeen/insertGlobalCss/index.html +0 -19
  81. package/dist-docs/aberdeen/invertString/index.html +0 -13
  82. package/dist-docs/aberdeen/isEmpty/index.html +0 -12
  83. package/dist-docs/aberdeen/map/index.html +0 -26
  84. package/dist-docs/aberdeen/merge/index.html +0 -11
  85. package/dist-docs/aberdeen/mount/index.html +0 -17
  86. package/dist-docs/aberdeen/multiMap/index.html +0 -28
  87. package/dist-docs/aberdeen/onEach/index.html +0 -19
  88. package/dist-docs/aberdeen/partition/index.html +0 -36
  89. package/dist-docs/aberdeen/peek/index.html +0 -40
  90. package/dist-docs/aberdeen/proxy/index.html +0 -35
  91. package/dist-docs/aberdeen/ref/index.html +0 -13
  92. package/dist-docs/aberdeen/runQueue/index.html +0 -13
  93. package/dist-docs/aberdeen/setErrorHandler/index.html +0 -16
  94. package/dist-docs/aberdeen/setSpacingCssVars/index.html +0 -8
  95. package/dist-docs/aberdeen/unmountAll/index.html +0 -5
  96. package/dist-docs/aberdeen/unproxy/index.html +0 -11
  97. package/dist-docs/assets/aberdeen/aberdeen.d.ts +0 -963
  98. package/dist-docs/assets/aberdeen/aberdeen.js.map +0 -11
  99. package/dist-docs/assets/aberdeen/dispatcher.d.ts +0 -57
  100. package/dist-docs/assets/aberdeen/dispatcher.js +0 -66
  101. package/dist-docs/assets/aberdeen/dispatcher.js.map +0 -10
  102. package/dist-docs/assets/aberdeen/helpers/reverseSortedSet.d.ts +0 -96
  103. package/dist-docs/assets/aberdeen/prediction.d.ts +0 -33
  104. package/dist-docs/assets/aberdeen/prediction.js +0 -112
  105. package/dist-docs/assets/aberdeen/prediction.js.map +0 -10
  106. package/dist-docs/assets/aberdeen/route.d.ts +0 -137
  107. package/dist-docs/assets/aberdeen/route.js +0 -287
  108. package/dist-docs/assets/aberdeen/route.js.map +0 -10
  109. package/dist-docs/assets/aberdeen/transitions.d.ts +0 -18
  110. package/dist-docs/assets/aberdeen/transitions.js +0 -47
  111. package/dist-docs/assets/aberdeen/transitions.js.map +0 -10
  112. package/dist-docs/assets/custom.js +0 -573
  113. package/dist-docs/assets/favicon.png +0 -0
  114. package/dist-docs/assets/hierarchy.js +0 -1
  115. package/dist-docs/assets/highlight.css +0 -134
  116. package/dist-docs/assets/icons.js +0 -18
  117. package/dist-docs/assets/icons.svg +0 -1
  118. package/dist-docs/assets/main.js +0 -60
  119. package/dist-docs/assets/navigation.js +0 -1
  120. package/dist-docs/assets/search.js +0 -1
  121. package/dist-docs/assets/style.css +0 -1640
  122. package/dist-docs/dispatcher/Dispatcher/index.html +0 -21
  123. package/dist-docs/dispatcher/MATCH_FAILED/index.html +0 -2
  124. package/dist-docs/dispatcher/MATCH_REST/index.html +0 -2
  125. package/dist-docs/dispatcher/index.html +0 -4
  126. package/dist-docs/hierarchy.html +0 -1
  127. package/dist-docs/index.html +0 -61
  128. package/dist-docs/media/CHANGELOG.md +0 -266
  129. package/dist-docs/modules.html +0 -1
  130. package/dist-docs/prediction/applyCanon/index.html +0 -13
  131. package/dist-docs/prediction/applyPrediction/index.html +0 -8
  132. package/dist-docs/prediction/index.html +0 -9
  133. package/dist-docs/route/Route/index.html +0 -23
  134. package/dist-docs/route/back/index.html +0 -4
  135. package/dist-docs/route/current/index.html +0 -2
  136. package/dist-docs/route/go/index.html +0 -14
  137. package/dist-docs/route/index.html +0 -15
  138. package/dist-docs/route/interceptLinks/index.html +0 -8
  139. package/dist-docs/route/matchCurrent/index.html +0 -10
  140. package/dist-docs/route/persistScroll/index.html +0 -6
  141. package/dist-docs/route/push/index.html +0 -6
  142. package/dist-docs/route/setLog/index.html +0 -3
  143. package/dist-docs/route/up/index.html +0 -5
  144. package/dist-docs/sitemap.xml +0 -239
  145. package/dist-docs/transitions/grow/index.html +0 -6
  146. package/dist-docs/transitions/index.html +0 -5
  147. package/dist-docs/transitions/shrink/index.html +0 -6
  148. package/skill/aberdeen.md +0 -4586
  149. package/skill/dispatcher.md +0 -129
  150. package/skill/prediction.md +0 -73
  151. package/skill/route.md +0 -322
  152. package/skill/transitions.md +0 -59
@@ -0,0 +1,685 @@
1
+ /**
2
+ * Aberdeen developer tools — a live scope-tree inspector.
3
+ *
4
+ * This module is a thin *wrapper* around the core: it re-exports the whole `aberdeen`
5
+ * API and additionally wires up the dev tools. In a dev build the `aberdeen` entry point
6
+ * resolves to the bundle produced from this file, so importing `aberdeen` gives the app
7
+ * the normal API *plus* the tooling. Production resolves to the bare core instead, where
8
+ * all of this is absent.
9
+ *
10
+ * The core emits instrumentation events through a sink we install via `_connectDev`. Since
11
+ * this wrapper loads synchronously (before the app's first `mount()`), the sink is in place
12
+ * in time to capture the initial render — no dynamic import, no event buffering.
13
+ *
14
+ * Event protocol (args after an implicit timestamp):
15
+ * create scope, parentScope, Error — a scope was created
16
+ * render scope — a scope (re)started rendering
17
+ * node scope, node, atScopeLevel — a DOM node was added by `scope`
18
+ * read scope, target, key — `scope` subscribed to target[key]
19
+ * schedule scope, key, oldVal, newVal, Error — a change scheduled `scope` to re-render
20
+ * delete scope — a scope was permanently removed
21
+ *
22
+ * Open with `?abdev=1` or Ctrl/Cmd+Alt+A.
23
+ */
24
+ import A, { _setDev } from "./aberdeen.js";
25
+ export * from "./aberdeen.js";
26
+ export default A;
27
+
28
+ let tool: DevTools | undefined;
29
+
30
+ function activate() {
31
+ if (tool) return;
32
+ try { (Error as any).stackTraceLimit = 50; } catch { /* ignore */ }
33
+ const t = new DevTools();
34
+ tool = t;
35
+ // The DevTools instance implements DevHooks; install it last (nothing emits before this).
36
+ _setDev(t);
37
+ }
38
+
39
+ interface DevScope {
40
+ id: number;
41
+ scope: any;
42
+ type: string;
43
+ parent: DevScope | undefined;
44
+ children: DevScope[];
45
+ el?: Element;
46
+ createdTime?: number; // undefined for scopes replayed on connect (they predate the tools)
47
+ createError?: Error;
48
+ renderTime?: number;
49
+ renderError?: Error;
50
+ renderChange?: { index: any; oldData: any; newData: any };
51
+ renderCount: number;
52
+ subs?: Array<[any, any]>;
53
+ topNodes: Node[];
54
+ }
55
+
56
+ // Per-scope-type presentation, keyed by the scope's constructor name.
57
+ const TYPES: Record<string, { label: string; color: string }> = {
58
+ RegularScope: { label: "scope", color: "#34d399" },
59
+ MountScope: { label: "mount", color: "#60a5fa" },
60
+ OnEachScope: { label: "onEach", color: "#c084fc" },
61
+ OnEachItemScope: { label: "item", color: "#f472b6" },
62
+ ResultScope: { label: "derive", color: "#fbbf24" },
63
+ SetArgScope: { label: "attr", color: "#94a3b8" },
64
+ RootScope: { label: "root", color: "#64748b" },
65
+ };
66
+ const typeInfo = (t: string) => TYPES[t] || { label: t, color: "#cbd5e1" };
67
+
68
+ const STYLE = `
69
+ /* Whole overlay tree is click-through (pointer-events inherits); only the dialog opts in. */
70
+ :host { all: initial; position: fixed; top: 0; left: 0; pointer-events: none; }
71
+ * { box-sizing: border-box; font-family: ui-monospace, monospace; }
72
+
73
+ .box { position: fixed; pointer-events: none; border-radius: 2px; z-index: 2147483000; }
74
+ .box.parent { border: 2px dotted #f59e0b; background: rgba(245,158,11,.06); }
75
+ .box.child { border: 2px solid #34d399; background: rgba(52,211,153,.10); }
76
+
77
+ .dlg {
78
+ position: fixed; left: 16px; top: 16px; width: 660px; max-width: 95vw; height: 480px; max-height: 90vh;
79
+ background: #1e1b2e; color: #ede9fe; border: 1px solid #6d28d9; border-radius: 8px;
80
+ box-shadow: 0 10px 36px rgba(0,0,0,.6); display: flex; flex-direction: column;
81
+ font-size: 12px; pointer-events: auto; z-index: 2147483600; /* above the highlight boxes */
82
+ }
83
+ .hd { display: flex; align-items: center; justify-content: space-between; gap: 10px;
84
+ padding: 6px 6px 6px 12px; border-bottom: 1px solid #4c1d95; cursor: move; user-select: none; }
85
+ .hd .title { font-weight: bold; color: #c4b5fd; }
86
+ .hd .x { cursor: pointer; color: #c4b5fd; padding: 2px 8px; border-radius: 4px; }
87
+ .hd .x:hover { background: #6d28d9; color: #fff; }
88
+ .cols { display: flex; flex: 1; min-height: 0; }
89
+ .left { width: 56%; display: flex; flex-direction: column; min-width: 0; border-right: 1px solid #4c1d95; }
90
+ .right { width: 44%; overflow: auto; padding: 8px 10px; }
91
+
92
+ .menu { display: flex; flex-wrap: wrap; gap: 6px; padding: 6px 8px; border-bottom: 1px solid #2e2a40; }
93
+ .menu button { font: inherit; background: #2e2a40; color: #ddd6fe; border: 1px solid #4c1d95;
94
+ border-radius: 5px; padding: 3px 8px; cursor: pointer; }
95
+ .menu button:hover { background: #6d28d9; }
96
+ .menu button.on { background: #6d28d9; color: #fff; }
97
+
98
+ .tree { flex: 1; overflow: auto; padding: 4px 0; }
99
+ .node { white-space: nowrap; display: flex; align-items: center; gap: 6px; padding: 1px 8px; cursor: pointer; }
100
+ .node:hover { background: #312a4a; }
101
+ .node.sel { background: #4c1d95; }
102
+ .node .tw { width: 12px; color: #94a3b8; text-align: center; flex: none; }
103
+ .badge { flex: none; font-size: 10px; padding: 0 5px; border-radius: 3px; color: #0b0a12; font-weight: bold; }
104
+ .node .loc { color: #e2e8f0; overflow: hidden; text-overflow: ellipsis; }
105
+ .node .t { color: #8b82a8; flex: none; }
106
+
107
+ .right h4 { margin: 0 0 6px; color: #c4b5fd; font-size: 12px; }
108
+ .right .row { margin-bottom: 3px; }
109
+ .right .k { color: #94a3b8; }
110
+ .sec { color: #a78bfa; font-weight: bold; font-size: 10px; text-transform: uppercase; letter-spacing: .06em;
111
+ margin: 12px 0 5px; border-bottom: 1px solid #3a3450; padding-bottom: 3px; }
112
+ .loclink { color: #93c5fd; cursor: pointer; text-decoration: underline dotted; }
113
+ .loclink:hover { color: #c4d9ff; }
114
+ .logbtn { font: inherit; font-size: 10px; background: #2e2a40; color: #ddd6fe; border: 1px solid #4c1d95;
115
+ border-radius: 4px; padding: 0 6px; cursor: pointer; }
116
+ .logbtn:hover { background: #6d28d9; }
117
+ .stack { white-space: pre-wrap; color: #cbd5e1; font-size: 11px; line-height: 1.5; margin-top: 2px;
118
+ border-left: 2px solid #3a3450; padding-left: 6px; }
119
+ .muted { color: #64748b; }
120
+ .reads .key { color: #fbbf24; } .reads .val { color: #93c5fd; }
121
+ .empty { color: #64748b; padding: 16px; text-align: center; white-space: pre-line; line-height: 1.5; }
122
+ `;
123
+
124
+ // Remembered across close/open within a single pageview (not persisted across reloads).
125
+ let lastPos: { left: number; top: number } | undefined;
126
+
127
+ class DevTools {
128
+ // --- reconstructed model --------------------------------------------------
129
+ root: DevScope = { id: 0, scope: null, type: "RootScope", parent: undefined, children: [], createdTime: 0, renderCount: 0, topNodes: [] };
130
+ scopeMap = new WeakMap<any, DevScope>();
131
+ pending = new WeakMap<any, { change: any; error: Error }>();
132
+ nodeMap = new WeakMap<Node, DevScope>();
133
+ elOwner = new WeakMap<Element, DevScope>();
134
+ origin = -1;
135
+ nextId = 1;
136
+
137
+ // --- UI -------------------------------------------------------------------
138
+ host = document.createElement("div");
139
+ shadow: ShadowRoot;
140
+ overlay: HTMLElement;
141
+ dlg!: HTMLElement;
142
+ treeEl!: HTMLElement;
143
+ rightEl!: HTMLElement;
144
+ menuEl!: HTMLElement;
145
+
146
+ mode: "tree" | "recent" = "tree";
147
+ selectedId: number | undefined;
148
+ collapsed = new Set<number>();
149
+ recentSince = 0;
150
+ boxes: HTMLElement[] = [];
151
+ thaw: (() => void) | undefined; // set while "freeze redraws" is on (holds A.freeze()'s releaser)
152
+ altPressed = false;
153
+ rebuildScheduled = false;
154
+ lastX = -1;
155
+ lastY = -1;
156
+
157
+ constructor() {
158
+ this.shadow = this.host.attachShadow({ mode: "open" });
159
+ const style = document.createElement("style");
160
+ style.textContent = STYLE;
161
+ this.shadow.appendChild(style);
162
+ this.overlay = eln("div");
163
+ this.shadow.appendChild(this.overlay);
164
+ this.buildDialog();
165
+ document.body.appendChild(this.host);
166
+ this.positionDialog();
167
+ this.renderEmptyDetails();
168
+
169
+ window.addEventListener("keydown", this.onKeyDown, true);
170
+ window.addEventListener("keyup", this.onKeyUp, true);
171
+ window.addEventListener("blur", this.onBlur, true);
172
+ window.addEventListener("pointermove", this.onPointerMove, true);
173
+ window.addEventListener("scroll", this.reposition, true);
174
+ window.addEventListener("resize", this.reposition, true);
175
+ }
176
+
177
+ destroy() {
178
+ this.savePos();
179
+ window.removeEventListener("keydown", this.onKeyDown, true);
180
+ window.removeEventListener("keyup", this.onKeyUp, true);
181
+ window.removeEventListener("blur", this.onBlur, true);
182
+ window.removeEventListener("pointermove", this.onPointerMove, true);
183
+ window.removeEventListener("scroll", this.reposition, true);
184
+ window.removeEventListener("resize", this.reposition, true);
185
+ this.clearBoxes();
186
+ if (this.thaw) this.thaw(); // release any outstanding freeze
187
+ this.host.remove();
188
+ tool = undefined;
189
+ _setDev(undefined); // tell the core to stop emitting events (drops `dev`)
190
+ }
191
+
192
+ // --- DevHooks: the core calls these (the instance is passed to `_setDev`) -------------
193
+ // Relative time (ms since the first event), captured when the hook fires.
194
+ private stamp(): number {
195
+ const now = performance.now();
196
+ if (this.origin < 0) this.origin = now;
197
+ return now - this.origin;
198
+ }
199
+
200
+ create(scope: any, parentScope: any, err: Error | undefined) {
201
+ const parent = this.scopeMap.get(parentScope) || this.root;
202
+ const d: DevScope = {
203
+ id: ++this.nextId, scope, type: scope.constructor.name, parent, children: [],
204
+ el: scope.el instanceof Element ? scope.el : undefined,
205
+ createdTime: err ? this.stamp() : undefined, createError: err, renderCount: 0, topNodes: [],
206
+ };
207
+ this.scopeMap.set(scope, d);
208
+ parent.children.push(d);
209
+ if (d.el) this.elOwner.set(d.el, d);
210
+ this.scheduleRebuild();
211
+ }
212
+
213
+ render(scope: any) {
214
+ const d = this.scopeMap.get(scope);
215
+ if (!d) return;
216
+ // Every render (initial or triggered) rebuilds the scope's children, nodes and reads.
217
+ d.children.length = 0;
218
+ d.topNodes = [];
219
+ d.subs = [];
220
+ // Only a re-render scheduled by a proxy change is a "trigger"; the initial render
221
+ // (no pending change) is not — leave the trigger fields unset for it.
222
+ const p = this.pending.get(scope);
223
+ if (p) {
224
+ d.renderTime = this.stamp();
225
+ d.renderCount++;
226
+ d.renderError = p.error;
227
+ d.renderChange = p.change;
228
+ this.pending.delete(scope);
229
+ }
230
+ this.scheduleRebuild();
231
+ }
232
+
233
+ // The triggering change/stack is recorded against the scope and consumed by `render`.
234
+ schedule(scope: any, index: any, oldData: any, newData: any, err: Error) {
235
+ this.pending.set(scope, { change: { index, oldData, newData }, error: err });
236
+ }
237
+
238
+ // Nodes don't change the tree itself; the maps/topNodes are read lazily on hover/select.
239
+ node(scope: any, node: Node, top: boolean) {
240
+ const d = this.scopeMap.get(scope);
241
+ if (!d) return;
242
+ this.nodeMap.set(node, d);
243
+ if (top) d.topNodes.push(node);
244
+ }
245
+
246
+ read(scope: any, target: any, index: any) {
247
+ const d = this.scopeMap.get(scope);
248
+ if (d) { (d.subs ||= []); if (d.subs.length < 500) d.subs.push([target, index]); }
249
+ }
250
+
251
+ delete(scope: any) {
252
+ const d = this.scopeMap.get(scope);
253
+ if (!d) return;
254
+ if (d.parent) { const i = d.parent.children.indexOf(d); if (i >= 0) d.parent.children.splice(i, 1); }
255
+ this.scopeMap.delete(scope);
256
+ this.scheduleRebuild();
257
+ }
258
+
259
+ // --- dialog skeleton ------------------------------------------------------
260
+ buildDialog() {
261
+ this.dlg = eln("div", "dlg");
262
+ const hd = eln("div", "hd");
263
+ hd.appendChild(eln("div", "title", "Aberdeen dev tools"));
264
+ const x = eln("div", "x", "✕");
265
+ x.addEventListener("click", () => this.destroy());
266
+ hd.appendChild(x);
267
+ this.dlg.appendChild(hd);
268
+ this.setupDrag(hd);
269
+
270
+ const cols = eln("div", "cols");
271
+ const left = eln("div", "left");
272
+ this.menuEl = eln("div", "menu");
273
+ this.treeEl = eln("div", "tree");
274
+ left.appendChild(this.menuEl);
275
+ left.appendChild(this.treeEl);
276
+ this.rightEl = eln("div", "right");
277
+ cols.appendChild(left);
278
+ cols.appendChild(this.rightEl);
279
+ this.dlg.appendChild(cols);
280
+ this.shadow.appendChild(this.dlg);
281
+ this.buildMenu();
282
+ }
283
+
284
+ positionDialog() {
285
+ const m = 16;
286
+ const w = this.dlg.offsetWidth, h = this.dlg.offsetHeight;
287
+ const left = lastPos ? lastPos.left : Math.max(m, window.innerWidth - w - m);
288
+ const top = lastPos ? lastPos.top : Math.max(m, window.innerHeight - h - m);
289
+ this.dlg.style.left = left + "px";
290
+ this.dlg.style.top = top + "px";
291
+ }
292
+
293
+ savePos() { lastPos = { left: this.dlg.offsetLeft, top: this.dlg.offsetTop }; }
294
+
295
+ buildMenu() {
296
+ this.menuEl.textContent = "";
297
+ const btn = (label: string, on: boolean, fn: () => void) => {
298
+ const b = eln("button", on ? "on" : undefined, label);
299
+ b.addEventListener("click", () => { fn(); this.buildMenu(); });
300
+ this.menuEl.appendChild(b);
301
+ };
302
+ btn("Tree", this.mode === "tree", () => { this.mode = "tree"; this.rebuild(); });
303
+ btn("Recent", this.mode === "recent", () => { this.mode = "recent"; this.rebuild(); });
304
+ btn("Freeze", !!this.thaw, () => {
305
+ if (this.thaw) { this.thaw(); this.thaw = undefined; }
306
+ else this.thaw = A.freeze();
307
+ });
308
+ if (this.mode === "recent") btn("Clear", false, () => { this.recentSince = this.lastEventTime(); this.rebuild(); });
309
+ this.menuEl.appendChild(eln("span", "muted", " Alt-hover an element to select its scope"));
310
+ }
311
+
312
+ lastEventTime(): number {
313
+ let max = 0;
314
+ const walk = (d: DevScope) => { if (d.renderTime != null && d.renderTime > max) max = d.renderTime; d.children.forEach(walk); };
315
+ this.root.children.forEach(walk);
316
+ return max;
317
+ }
318
+
319
+ // --- live tree ------------------------------------------------------------
320
+ scheduleRebuild() {
321
+ if (this.rebuildScheduled) return;
322
+ this.rebuildScheduled = true;
323
+ requestAnimationFrame(() => { this.rebuildScheduled = false; this.rebuild(); });
324
+ }
325
+
326
+ rebuild() {
327
+ const scrollTop = this.treeEl.scrollTop;
328
+ this.treeEl.textContent = "";
329
+ if (this.mode === "tree") {
330
+ for (const child of this.root.children) this.renderNode(child, 0);
331
+ if (!this.root.children.length) this.treeEl.appendChild(eln("div", "empty", "No live scopes yet."));
332
+ } else {
333
+ this.renderRecent();
334
+ }
335
+ this.treeEl.scrollTop = scrollTop;
336
+ const d = this.selectedId != null ? this.findScope(this.selectedId) : undefined;
337
+ if (d) { this.renderDetails(d); this.highlight(d); }
338
+ else { this.selectedId = undefined; this.clearBoxes(); this.renderEmptyDetails(); }
339
+ }
340
+
341
+ renderEmptyDetails() {
342
+ if (this.rightEl.firstElementChild?.classList.contains("empty")) return; // already shown
343
+ this.rightEl.textContent = "";
344
+ this.rightEl.appendChild(eln("div", "empty",
345
+ "No scope selected.\n\nClick a scope in the tree — or hold Alt and hover an element on the page to select the scope that drew it."));
346
+ }
347
+
348
+ renderRecent() {
349
+ const all: DevScope[] = [];
350
+ const walk = (d: DevScope) => { if (d.renderTime != null && d.renderTime > this.recentSince) all.push(d); d.children.forEach(walk); };
351
+ this.root.children.forEach(walk);
352
+ all.sort((x, y) => (y.renderTime as number) - (x.renderTime as number));
353
+ if (!all.length) { this.treeEl.appendChild(eln("div", "empty", "No re-renders observed yet.")); return; }
354
+ for (const d of all.slice(0, 60)) {
355
+ const row = eln("div", "node" + (d.id === this.selectedId ? " sel" : ""));
356
+ row.dataset.id = String(d.id);
357
+ row.style.paddingLeft = "8px";
358
+ const info = typeInfo(d.type);
359
+ const badge = eln("span", "badge", info.label); badge.style.background = info.color;
360
+ row.appendChild(badge);
361
+ row.appendChild(eln("span", "loc", d.renderChange ? changeSummary(d.renderChange) : appFrame(d.createError) || d.type));
362
+ row.appendChild(eln("span", "t", ms(d.renderTime as number, 0)));
363
+ row.addEventListener("click", () => this.select(d));
364
+ row.addEventListener("pointerenter", () => this.highlight(d));
365
+ row.addEventListener("pointerleave", () => this.highlight(this.selectedId != null ? this.findScope(this.selectedId) : undefined));
366
+ this.treeEl.appendChild(row);
367
+ }
368
+ }
369
+
370
+ renderNode(d: DevScope, depth: number) {
371
+ const row = eln("div", "node" + (d.id === this.selectedId ? " sel" : ""));
372
+ row.dataset.id = String(d.id);
373
+ row.style.paddingLeft = (8 + depth * 14) + "px";
374
+ const hasKids = d.children.length > 0;
375
+ const tw = eln("div", "tw", hasKids ? (this.collapsed.has(d.id) ? "▸" : "▾") : "");
376
+ if (hasKids) tw.addEventListener("click", (e) => {
377
+ e.stopPropagation();
378
+ if (this.collapsed.has(d.id)) this.collapsed.delete(d.id); else this.collapsed.add(d.id);
379
+ this.rebuild();
380
+ });
381
+ row.appendChild(tw);
382
+
383
+ const info = typeInfo(d.type);
384
+ const badge = eln("span", "badge", info.label);
385
+ badge.style.background = info.color;
386
+ row.appendChild(badge);
387
+
388
+ row.appendChild(eln("span", "loc", appFrame(d.createError) || (d.createError === undefined ? "(pre-open)" : "(internal)")));
389
+ if (d.createdTime != null) row.appendChild(eln("span", "t", ms(d.createdTime, 0)));
390
+
391
+ row.addEventListener("click", () => this.select(d));
392
+ row.addEventListener("pointerenter", () => this.highlight(d));
393
+ row.addEventListener("pointerleave", () => this.highlight(this.selectedId != null ? this.findScope(this.selectedId) : undefined));
394
+ this.treeEl.appendChild(row);
395
+
396
+ if (hasKids && !this.collapsed.has(d.id)) for (const c of d.children) this.renderNode(c, depth + 1);
397
+ }
398
+
399
+ findScope(id: number, node: DevScope = this.root): DevScope | undefined {
400
+ if (node.id === id) return node;
401
+ for (const c of node.children) { const r = this.findScope(id, c); if (r) return r; }
402
+ return undefined;
403
+ }
404
+
405
+ // --- selection + details --------------------------------------------------
406
+ select(d: DevScope | undefined) {
407
+ this.selectedId = d ? d.id : undefined;
408
+ for (const el of this.treeEl.querySelectorAll(".node.sel")) el.classList.remove("sel");
409
+ if (!d) { this.renderEmptyDetails(); this.clearBoxes(); return; }
410
+ const row = this.treeEl.querySelector(`.node[data-id="${d.id}"]`);
411
+ if (row) { row.classList.add("sel"); (row as HTMLElement).scrollIntoView({ block: "nearest" }); }
412
+ this.renderDetails(d);
413
+ this.highlight(d);
414
+ }
415
+
416
+ renderDetails(d: DevScope) {
417
+ const r = this.rightEl;
418
+ r.textContent = "";
419
+ const info = typeInfo(d.type);
420
+ const head = eln("h4");
421
+ const badge = eln("span", "badge", info.label); badge.style.background = info.color;
422
+ head.appendChild(badge); head.appendChild(document.createTextNode(" " + d.type));
423
+ r.appendChild(head);
424
+
425
+ // --- State: what the scope currently is ---
426
+ r.appendChild(kv("owned nodes", String(d.topNodes.length)));
427
+ r.appendChild(kv("child scopes", String(d.children.length)));
428
+ if (d.type === "OnEachItemScope" && d.scope) r.appendChild(renderItemValue(d.scope));
429
+ if (d.subs && d.subs.length) r.appendChild(this.renderReads(d));
430
+
431
+ // --- Creation: when/where the scope was made ---
432
+ r.appendChild(section("Creation"));
433
+ r.appendChild(kv("time", d.createdTime != null ? ms(d.createdTime) : "unknown"));
434
+ r.appendChild(stackRow("stack", d.createError, "unknown"));
435
+ if (!d.createError) r.appendChild(eln("div", "muted", "Reload with ?abdev=1 to capture creation stacks."));
436
+
437
+ // --- Last trigger: what most recently re-ran the scope ---
438
+ r.appendChild(section("Last trigger"));
439
+ if (d.renderTime != null) {
440
+ r.appendChild(kv("time", ms(d.renderTime) + " (" + d.renderCount + "×)"));
441
+ if (d.renderChange) r.appendChild(kv("change", changeSummary(d.renderChange)));
442
+ r.appendChild(stackRow("stack", d.renderError, "(no application frame)"));
443
+ } else {
444
+ r.appendChild(eln("div", "muted", "not triggered yet"));
445
+ }
446
+ }
447
+
448
+ renderReads(d: DevScope): HTMLElement {
449
+ const wrap = eln("div", "row reads");
450
+ wrap.appendChild(eln("div", "k", "observing:"));
451
+ const groups = new Map<any, Set<any>>();
452
+ for (const [obj, key] of d.subs!) {
453
+ let s = groups.get(obj); if (!s) groups.set(obj, (s = new Set())); s.add(key);
454
+ }
455
+ if (!groups.size) { wrap.appendChild(eln("div", "muted", "(none)")); return wrap; }
456
+ let n = 0;
457
+ for (const [obj, keys] of groups) {
458
+ if (n++ > 16) { wrap.appendChild(eln("div", "muted", "…")); break; }
459
+ const line = eln("div");
460
+ line.appendChild(document.createTextNode("{ "));
461
+ let first = true;
462
+ for (const k of [...keys].slice(0, 16)) {
463
+ if (!first) line.appendChild(document.createTextNode(", ")); first = false;
464
+ line.appendChild(eln("span", "key", readKey(k)));
465
+ // Symbol keys (`*` / `size`) are meta-subscriptions with no scalar value to show.
466
+ if (typeof k !== "symbol") {
467
+ line.appendChild(document.createTextNode("="));
468
+ let v: any; try { v = (obj as any)[k]; } catch { v = "?"; }
469
+ line.appendChild(eln("span", "val", preview(v)));
470
+ }
471
+ }
472
+ line.appendChild(document.createTextNode(" }"));
473
+ wrap.appendChild(line);
474
+ }
475
+ return wrap;
476
+ }
477
+
478
+ // --- DOM highlight --------------------------------------------------------
479
+ highlight(d: DevScope | undefined) {
480
+ this.clearBoxes();
481
+ if (!d) return;
482
+ // Dotted orange: the element this scope renders *into* (its `el`).
483
+ if (d.el instanceof Element) this.box(d.el.getBoundingClientRect(), "parent");
484
+ // Green: what this scope produced — its own elements plus each child scope's region.
485
+ for (const n of d.topNodes) if (n instanceof Element) this.box(n.getBoundingClientRect(), "child");
486
+ for (const c of d.children) this.box(regionRect(c), "child");
487
+ }
488
+
489
+ box(rect: DOMRect | undefined, kind: string) {
490
+ if (!rect || (!rect.width && !rect.height)) return;
491
+ const b = eln("div", "box " + kind);
492
+ Object.assign(b.style, { left: rect.left + "px", top: rect.top + "px", width: rect.width + "px", height: rect.height + "px" });
493
+ this.overlay.appendChild(b);
494
+ this.boxes.push(b);
495
+ }
496
+
497
+ clearBoxes() { for (const b of this.boxes) b.remove(); this.boxes.length = 0; }
498
+
499
+ reposition = () => {
500
+ const d = this.selectedId != null ? this.findScope(this.selectedId) : undefined;
501
+ if (d) this.highlight(d);
502
+ };
503
+
504
+ // --- input ----------------------------------------------------------------
505
+ overUI(t: EventTarget | null): boolean { return !(t instanceof Node) || t === this.host || this.host.contains(t); }
506
+
507
+ onKeyDown = (e: KeyboardEvent) => {
508
+ if (e.key === "Alt") { this.altPressed = true; this.resolveAt(this.lastX, this.lastY); }
509
+ else if (e.key === "Escape") { e.preventDefault(); this.destroy(); }
510
+ };
511
+ onKeyUp = (e: KeyboardEvent) => { if (e.key === "Alt") this.altPressed = false; };
512
+ onBlur = () => { this.altPressed = false; };
513
+
514
+ onPointerMove = (e: PointerEvent) => {
515
+ this.lastX = e.clientX; this.lastY = e.clientY;
516
+ if (this.altPressed) this.resolveAt(this.lastX, this.lastY);
517
+ };
518
+
519
+ resolveAt(x: number, y: number) {
520
+ if (x < 0) return;
521
+ const target = document.elementFromPoint(x, y);
522
+ if (!target || this.overUI(target)) return;
523
+ let node: Node | null = target;
524
+ let d: DevScope | undefined;
525
+ while (node) {
526
+ // Prefer the scope that renders into this element (orange `el`); fall back to
527
+ // the scope that created the node (green). Whichever is found first walking up.
528
+ d = (node instanceof Element ? this.elOwner.get(node) : undefined) || this.nodeMap.get(node);
529
+ if (d) break;
530
+ node = node.parentNode;
531
+ }
532
+ if (d && d.id !== this.selectedId) this.select(d);
533
+ }
534
+
535
+ // --- dragging -------------------------------------------------------------
536
+ setupDrag(handle: HTMLElement) {
537
+ let sx = 0, sy = 0, ox = 0, oy = 0, down = false;
538
+ handle.addEventListener("pointerdown", (e) => {
539
+ if ((e.target as HTMLElement).classList.contains("x")) return;
540
+ down = true; sx = e.clientX; sy = e.clientY; ox = this.dlg.offsetLeft; oy = this.dlg.offsetTop;
541
+ handle.setPointerCapture(e.pointerId);
542
+ });
543
+ handle.addEventListener("pointermove", (e) => {
544
+ if (!down) return;
545
+ this.dlg.style.left = Math.max(0, Math.min(window.innerWidth - 80, ox + e.clientX - sx)) + "px";
546
+ this.dlg.style.top = Math.max(0, Math.min(window.innerHeight - 24, oy + e.clientY - sy)) + "px";
547
+ });
548
+ handle.addEventListener("pointerup", () => { down = false; this.savePos(); });
549
+ }
550
+ }
551
+
552
+ // --- helpers ----------------------------------------------------------------
553
+
554
+ function eln(tag: string, cls?: string, text?: string): HTMLElement {
555
+ const e = document.createElement(tag);
556
+ if (cls) e.className = cls;
557
+ if (text != null) e.textContent = text;
558
+ return e;
559
+ }
560
+
561
+ function kv(k: string, v: string): HTMLElement {
562
+ const row = eln("div", "row");
563
+ row.appendChild(eln("span", "k", k + ": "));
564
+ row.appendChild(document.createTextNode(v));
565
+ return row;
566
+ }
567
+
568
+ function section(title: string): HTMLElement {
569
+ return eln("div", "sec", title);
570
+ }
571
+
572
+ // Shows a compact "file:line" (like the tree) that expands to the full stack on click.
573
+ function stackRow(label: string, err: Error | undefined, emptyMsg: string): HTMLElement {
574
+ const row = eln("div", "row");
575
+ row.appendChild(eln("span", "k", label + ": "));
576
+ const full = formatStack(err && err.stack);
577
+ if (!full) { row.appendChild(eln("span", "muted", emptyMsg)); return row; }
578
+ const link = eln("span", "loclink", appFrame(err) || "(internal)");
579
+ const pre = eln("div", "stack");
580
+ pre.textContent = full;
581
+ pre.style.display = "none";
582
+ link.addEventListener("click", () => { pre.style.display = pre.style.display ? "" : "none"; });
583
+ row.appendChild(link);
584
+ row.appendChild(pre);
585
+ return row;
586
+ }
587
+
588
+ // Renders a subscription/change key. The only symbols Aberdeen uses as keys are
589
+ // ANY_SYMBOL (a wildcard over the whole collection) and MAP_SIZE_SYMBOL (its size/length).
590
+ function readKey(k: any): string {
591
+ if (typeof k !== "symbol") return String(k);
592
+ return k.description === "any" ? "*" : k.description === "mapSize" ? "size" : (k.description || "?");
593
+ }
594
+
595
+ function changeSummary(c: { index: any; oldData: any; newData: any }): string {
596
+ return `${readKey(c.index)}: ${preview(c.oldData)} → ${preview(c.newData)}`;
597
+ }
598
+
599
+ // Shows the value an onEach *item* scope is iterating (`parent.target[itemIndex]`), plus a
600
+ // button to log it to the console for exploration.
601
+ function renderItemValue(scope: any): HTMLElement {
602
+ const target = scope.parent?.target;
603
+ const idx = scope.itemIndex;
604
+ const isSet = target instanceof Set;
605
+ const value = isSet ? idx : target instanceof Map ? target.get(idx) : target ? target[idx] : undefined;
606
+
607
+ const wrap = eln("div", "row");
608
+ if (!isSet) wrap.appendChild(kv("key", String(idx)));
609
+ const line = eln("div");
610
+ line.appendChild(eln("span", "k", "value: "));
611
+ line.appendChild(eln("span", "val", preview(value)));
612
+ const btn = eln("button", "logbtn", "to console");
613
+ btn.addEventListener("click", () => console.log(value));
614
+ line.appendChild(document.createTextNode(" "));
615
+ line.appendChild(btn);
616
+ wrap.appendChild(line);
617
+ return wrap;
618
+ }
619
+
620
+ // Milliseconds with a thousands separator, e.g. "+1,234.5ms".
621
+ function ms(t: number, dp = 1): string {
622
+ return "+" + t.toLocaleString(undefined, { minimumFractionDigits: dp, maximumFractionDigits: dp }) + "ms";
623
+ }
624
+
625
+ function preview(v: any): string {
626
+ if (v === null) return "null";
627
+ if (typeof v === "string") return JSON.stringify(v.length > 30 ? v.slice(0, 30) + "…" : v);
628
+ if (typeof v === "object") return Array.isArray(v) ? `[${v.length}]` : "{…}";
629
+ if (typeof v === "function") return "fn";
630
+ return String(v);
631
+ }
632
+
633
+ /** All DOM nodes a scope produced (its own top-level nodes plus its descendants'). */
634
+ function deepNodes(d: DevScope, out: Node[] = []): Node[] {
635
+ for (const n of d.topNodes) out.push(n);
636
+ for (const c of d.children) deepNodes(c, out);
637
+ return out;
638
+ }
639
+
640
+ /** Bounding rect covering a scope's whole DOM region (viewport coords). */
641
+ function regionRect(d: DevScope): DOMRect | undefined {
642
+ let l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
643
+ for (const n of deepNodes(d)) {
644
+ if (!(n instanceof Element)) continue;
645
+ const q = n.getBoundingClientRect();
646
+ if (!q.width && !q.height) continue;
647
+ l = Math.min(l, q.left); t = Math.min(t, q.top); r = Math.max(r, q.right); b = Math.max(b, q.bottom);
648
+ }
649
+ if (l === Infinity) return undefined;
650
+ return new DOMRect(l, t, r - l, b - t);
651
+ }
652
+
653
+ /** First application stack frame as "file:line", skipping Aberdeen's own frames. */
654
+ function appFrame(err: Error | undefined): string {
655
+ const s = formatStack(err && err.stack);
656
+ if (!s) return "";
657
+ const first = s.split("\n")[0];
658
+ const m = first.match(/([^\/\\(]+):(\d+):\d+\)?$/);
659
+ return m ? `${m[1]}:${m[2]}` : first.replace(/^at\s+/, "");
660
+ }
661
+
662
+ /** Drop the "Error" header and Aberdeen's own frames, leaving application frames. */
663
+ function formatStack(stack: string | undefined): string {
664
+ if (!stack) return "";
665
+ const origin = location.protocol + "//" + location.host; // strip for brevity, leaving paths relative
666
+ return stack.split("\n").map((l) => l.trim().split(origin).join("")).filter((l) =>
667
+ (l.startsWith("at ") || l.includes("@")) &&
668
+ // In dev builds the core + this tool are bundled together as aberdeen.dev.js.
669
+ !/aberdeen(\.dev)?\.[jt]s|devtools\.[jt]s/.test(l)
670
+ ).join("\n");
671
+ }
672
+
673
+ // Bootstrap — placed at the end so `activate()` runs after `DevTools` is initialized. The
674
+ // wrapper module evaluates synchronously when the app imports `aberdeen`, before its first
675
+ // `mount()`, so `?abdev=1` captures the initial render.
676
+ if (typeof window !== "undefined" && typeof document !== "undefined") {
677
+ if (/[?&]abdev=1\b/.test(location.search)) activate();
678
+ window.addEventListener("keydown", (e) => {
679
+ // Ctrl/Cmd+Alt+A ("A" for Aberdeen). `e.code` so it's layout-independent.
680
+ if ((e.ctrlKey || e.metaKey) && e.altKey && e.code === "KeyA") {
681
+ e.preventDefault();
682
+ if (tool) tool.destroy(); else activate(); // toggle
683
+ }
684
+ });
685
+ }
@@ -1 +0,0 @@
1
- TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false.