@jxsuite/studio 5.2.0 → 5.3.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 (71) hide show
  1. package/dist/chunks/{cf-account-picker-h4dab9bq.js → cf-account-picker-nwnwdm6p.js} +5 -5
  2. package/dist/chunks/{content-source-sgg1sv6n.js → content-source-4rzvmp0m.js} +16 -16
  3. package/dist/chunks/{convert-to-repeater-mf1cxgfn.js → convert-to-repeater-jxsv62v7.js} +16 -16
  4. package/dist/chunks/{entry-commands-w9qsxa6q.js → entry-commands-1wnsqn5d.js} +19 -19
  5. package/dist/chunks/{extension-sections-c6zwzbre.js → extension-sections-2mrptyek.js} +20 -20
  6. package/dist/chunks/{file-ops-vrpq1jjk.js → file-ops-knfqshrm.js} +15 -15
  7. package/dist/chunks/{files-msaf95y6.js → files-y6fr4he9.js} +24 -23
  8. package/dist/chunks/{format-host-f64fnesp.js → format-host-1k4qrcqd.js} +2 -2
  9. package/dist/chunks/{git-panel-bv5e22j0.js → git-panel-2rkrsjva.js} +15 -15
  10. package/dist/chunks/{grid-view-paj67nr5.js → grid-view-a7b87n2c.js} +18 -18
  11. package/dist/chunks/{iframe-host-dh5np8vk.js → iframe-host-scpw01h8.js} +7 -7
  12. package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
  13. package/dist/chunks/{layers-1vcfdbet.js → layers-jnvdbqva.js} +3 -3
  14. package/dist/chunks/layers-panel-73gfkcga.js +41 -0
  15. package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
  16. package/dist/chunks/{media-picker-p49nhqhs.js → media-picker-f66nav16.js} +9 -9
  17. package/dist/chunks/{menu-qht48vnc.js → menu-1dfhry4s.js} +4 -4
  18. package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-km6abc6q.js} +2 -2
  19. package/dist/chunks/{publish-panel-vz23wwmg.js → publish-panel-z1zd2da1.js} +8 -8
  20. package/dist/chunks/quick-search-zdv7h8ht.js +47 -0
  21. package/dist/chunks/{right-panel-j97t2yvy.js → right-panel-889vbnm5.js} +18 -18
  22. package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
  23. package/dist/chunks/{studio-ympe0n7a.js → studio-0j7r7s9g.js} +2 -2
  24. package/dist/chunks/{studio-68fydsrj.js → studio-151w0xnh.js} +13 -13
  25. package/dist/chunks/{studio-x72j58vw.js → studio-27q131j3.js} +3 -3
  26. package/dist/chunks/{studio-ctm7bhx5.js → studio-4f3psrk1.js} +5 -5
  27. package/dist/chunks/{studio-t3adrr56.js → studio-50wfwy44.js} +7 -7
  28. package/dist/chunks/{studio-28vs5k2y.js → studio-6gryj0sg.js} +5 -5
  29. package/dist/chunks/{studio-ncn9e189.js → studio-6kvgrkrp.js} +5 -5
  30. package/dist/chunks/{studio-14d9tq4d.js → studio-7144epdr.js} +5 -5
  31. package/dist/chunks/{studio-01jeck2y.js → studio-7rrb1esn.js} +16 -16
  32. package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
  33. package/dist/chunks/{studio-4r14yzyy.js → studio-a65w20ex.js} +2 -2
  34. package/dist/chunks/{studio-c07s5ckd.js → studio-ab87phj1.js} +3 -3
  35. package/dist/chunks/{studio-hhskq2mr.js → studio-b66rsh78.js} +12 -12
  36. package/dist/chunks/{studio-en82c639.js → studio-b7xacrwe.js} +4 -4
  37. package/dist/chunks/{studio-15f3kxqe.js → studio-c0qgc3tn.js} +10 -10
  38. package/dist/chunks/{studio-y4f9fdge.js → studio-cgprcdxn.js} +6 -6
  39. package/dist/chunks/{studio-p6pmwkvh.js → studio-d1n2bykn.js} +32 -5
  40. package/dist/chunks/{studio-4xmjcrg3.js → studio-dd1qqkry.js} +4 -4
  41. package/dist/chunks/{studio-etvx9s2g.js → studio-drg8qj9k.js} +6 -6
  42. package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
  43. package/dist/chunks/{studio-mpzwze9x.js → studio-fzjx96t2.js} +4 -4
  44. package/dist/chunks/{studio-66arbpc2.js → studio-g33v4fe9.js} +15 -14
  45. package/dist/chunks/{studio-ez11dmdh.js → studio-gk2twyh4.js} +2 -2
  46. package/dist/chunks/{studio-5nz00bhh.js → studio-gtfq1dkz.js} +3 -3
  47. package/dist/chunks/{studio-5t7ena5z.js → studio-j0w60gft.js} +5 -5
  48. package/dist/chunks/{studio-qa7fzxan.js → studio-jt27ta6x.js} +3 -3
  49. package/dist/chunks/{studio-x10fp4tp.js → studio-ke09tn9k.js} +11 -11
  50. package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
  51. package/dist/chunks/{studio-wjpgxywa.js → studio-msc1ee1m.js} +3 -3
  52. package/dist/chunks/{studio-2mdbbc76.js → studio-nfx354t1.js} +7 -7
  53. package/dist/chunks/{studio-9j6sfeba.js → studio-nsgw7dbk.js} +4 -4
  54. package/dist/chunks/{studio-hdc2jt3z.js → studio-rtskkm43.js} +3 -3
  55. package/dist/chunks/{studio-vt0tj32j.js → studio-xajk70w0.js} +16 -16
  56. package/dist/chunks/{studio-a1pswps3.js → studio-ye9drbk8.js} +24 -23
  57. package/dist/chunks/{studio-sar8g54p.js → studio-ze46gzjh.js} +7 -7
  58. package/dist/chunks/{workspace-bnw54cha.js → workspace-np10mayv.js} +3 -3
  59. package/dist/iframe-entry.js +3 -3
  60. package/dist/studio.js +116 -79
  61. package/package.json +5 -5
  62. package/src/panels/chat-panel.ts +40 -5
  63. package/src/services/module-preloads.ts +67 -0
  64. package/src/studio.ts +6 -0
  65. package/src/surfaces/formula-palette.json +8 -7
  66. package/src/surfaces/formula-palette.ts +10 -6
  67. package/src/surfaces/palette.json +8 -7
  68. package/src/ui/panel-resize.ts +3 -23
  69. package/src/ui/virtual-window.ts +57 -4
  70. package/dist/chunks/layers-panel-fksbdg4q.js +0 -40
  71. package/dist/chunks/quick-search-e9rp74m8.js +0 -46
@@ -12,10 +12,19 @@
12
12
  * in `ui/regions.ts`'s shell table — the assistant no longer has a shell host to name. Three
13
13
  * screenshot shots address that id and none of them changed.
14
14
  *
15
- * Hosts the assistant UI from ai-panel.ts unconditionally: with no project (welcome screen), with a
16
- * project but no open document, and with a document open. The panel is mounted once at studio boot
17
- * and never tears down on tab switches — the assistant's module state and DOM (composer draft,
18
- * scroll position) persist.
15
+ * Hosts the assistant machinery from ai-panel.ts unconditionally: with no project (welcome screen),
16
+ * with a project but no open document, and with a document open. The machinery is mounted once at
17
+ * studio boot and never tears down on tab switches — the assistant's module state (composer draft,
18
+ * scroll position, transcript) persists.
19
+ *
20
+ * **The assistant's SURFACE document is what this module defers.** Mounting `surfaces/ai-chat.ts`
21
+ * costs a traced 100–290 ms of connectedCallback at boot (its island projections; see
22
+ * `scripts/perf/REPORT.md`), paid on every window open although the document is invisible until
23
+ * the tab is picked. Binding is the same seam (`bindAiPanelHost`) and it is now deferred to the
24
+ * first time the Assistant tab is actually shown or revealed: `renderAiPanel` no-ops while the
25
+ * surface is null, and the surface's own watcher projects whatever the machinery wrote before that
26
+ * point, so a prompt seeded into a boot the reader never spends in the tab is not lost — it is one
27
+ * more message waiting when the body first appears.
19
28
  *
20
29
  * **This module is the SEAM, and it has no markup of its own.** The assistant is a Jx document
21
30
  * (`surfaces/ai-chat.json`), and a document CLEARS the host it is given — so it needs a container
@@ -32,6 +41,7 @@ import { effect, effectScope } from "../reactivity";
32
41
  import { workspace } from "../workspace/workspace";
33
42
  import { consumePendingAgentPrompt, hasPendingAgentPrompt } from "../services/agent-seed";
34
43
  import { REGION_ATTR } from "../ui/regions";
44
+ import { inspectorTab } from "./right-panel";
35
45
  import {
36
46
  bindAiPanelHost,
37
47
  mountAiPanel,
@@ -48,6 +58,23 @@ const ASSISTANT_REGION = "inspector.assistant";
48
58
  let _host: HTMLElement | null = null;
49
59
  let _container: HTMLElement | null = null;
50
60
  let _scope: EffectScope | null = null;
61
+ /** Whether the surface document has been bound. One bind per host; a remount resets it. */
62
+ let _surfaceBound = false;
63
+
64
+ /**
65
+ * Bind the assistant's surface document the first time its tab is shown or revealed.
66
+ *
67
+ * The container, the region attribute and every prompt path are ready before this; the surface is
68
+ * the expensive part and is invisible until the tab is picked, so this is the earliest moment its
69
+ * cost is worth paying. A restore that lands on the Assistant tab binds on the same tick.
70
+ */
71
+ function ensureAssistantSurface(): void {
72
+ if (_surfaceBound || !_container) {
73
+ return;
74
+ }
75
+ _surfaceBound = true;
76
+ bindAiPanelHost(_container);
77
+ }
51
78
 
52
79
  /**
53
80
  * Mount the assistant into the host the Inspector hands it. Idempotent per host: the persistent
@@ -68,11 +95,18 @@ export function mount(host: HTMLElement | null) {
68
95
  host.append(_container);
69
96
 
70
97
  mountAiPanel();
71
- bindAiPanelHost(_container);
98
+ _surfaceBound = false;
72
99
 
73
100
  _scope?.stop();
74
101
  _scope = effectScope();
75
102
  _scope.run(() => {
103
+ effect(() => {
104
+ // The surface's bind follows the TAB: a session restored onto the Assistant is bound by
105
+ // One re-run of this effect, not by anyone remembering to call the seam directly.
106
+ if (inspectorTab() === "assistant") {
107
+ ensureAssistantSurface();
108
+ }
109
+ });
76
110
  effect(() => {
77
111
  // A pending agent prompt (stored by the New Project flow, possibly from another window) is
78
112
  // Keyed by the absolute project root — consume it as soon as this window adopts that root.
@@ -98,6 +132,7 @@ export function mount(host: HTMLElement | null) {
98
132
  export function unmount() {
99
133
  _scope?.stop();
100
134
  _scope = null;
135
+ _surfaceBound = false;
101
136
  // `null` unbinds the surface: the document is disposed and the chat-state watcher stopped.
102
137
  bindAiPanelHost(null);
103
138
  if (_host) {
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Module-preloads.ts — batch Bun's per-graph `<link rel=modulepreload>` insertions onto one frame.
3
+ *
4
+ * The bundler's dynamic-import runtime warm-fetches each lazy graph by appending a `<link
5
+ * rel="modulepreload">` to `head` the moment the first `import()` of that graph runs. The insertion
6
+ * itself costs a recalculation sweep of the WHOLE document — a link's addition invalidates style
7
+ * matching everywhere, so the DevTools timeline bills a large batch of invalidation records (`Style
8
+ * rule change → <every element>`) to whatever read next. Studio's palette pays this on its first
9
+ * open, once per inserted link: the traced cost is a document recalculation per link, laid on top
10
+ * of quick-search's own work.
11
+ *
12
+ * Batching is the whole fix: links queued during a task are appended together on ONE animation
13
+ * frame, so the browser merges their invalidations into a single sweep. Nothing is dropped and no
14
+ * fetch is slowed — `import()` resolves on its own fetch regardless of the link, and one frame is
15
+ * shorter than any chunk is likely to be. The original append order is kept because the graph's
16
+ * entry chunk should warm before its children.
17
+ *
18
+ * Idempotent by a flag on `head`: installed once per document, before any dynamic `import()` of
19
+ * studio's own lazy surfaces runs.
20
+ */
21
+
22
+ const QUEUED: HTMLLinkElement[] = [];
23
+ let frame: number | null = null;
24
+ let originalAppend: (<T extends Node>(node: T) => T) | null = null;
25
+
26
+ function flush(): void {
27
+ frame = null;
28
+ const singleOrigin = originalAppend;
29
+ if (!singleOrigin) {
30
+ return;
31
+ }
32
+ for (const link of QUEUED.splice(0)) {
33
+ singleOrigin.call(document.head, link);
34
+ }
35
+ }
36
+
37
+ /**
38
+ * `appendChildPatched` — defer a modulepreload link to the shared frame; anything else goes
39
+ * straight. Returns the appended node either way, matching `Node.appendChild`'s own contract, so a
40
+ * caller that reads the return value (as `Node.appendChild` promises) sees the node it passed in
41
+ * rather than `undefined` on the deferred path.
42
+ */
43
+ function appendChildPatched<T extends Node>(this: Node, node: T): T {
44
+ if (node instanceof HTMLLinkElement && node.rel === "modulepreload") {
45
+ QUEUED.push(node);
46
+ if (frame === null) {
47
+ frame = requestAnimationFrame(flush);
48
+ }
49
+ return node;
50
+ }
51
+ // `originalAppend` is already bound to `head` at install time, so no `this` needs passing here.
52
+ return originalAppend ? originalAppend(node) : node;
53
+ }
54
+
55
+ export function installModulePreloadBatcher(): void {
56
+ const { head } = document;
57
+ if (
58
+ !head ||
59
+ (head as HTMLElement & { __modulePreloadsBatched?: boolean }).__modulePreloadsBatched
60
+ ) {
61
+ return;
62
+ }
63
+ const patched = head as HTMLElement & { __modulePreloadsBatched?: boolean };
64
+ originalAppend = Node.prototype.appendChild.bind(head);
65
+ patched.__modulePreloadsBatched = true;
66
+ head.appendChild = appendChildPatched as unknown as typeof head.appendChild;
67
+ }
package/src/studio.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  * registration) on first use by source mode, the function editor, or the formula workspace.
13
13
  */
14
14
  import { errorMessage } from "@jxsuite/schema/parse";
15
+ import { installModulePreloadBatcher } from "./services/module-preloads";
15
16
  import { getPanel } from "./panels/panel-registry";
16
17
 
17
18
  import {
@@ -267,6 +268,11 @@ import type { JxDocument, JxMutableNode, ProjectConfig } from "@jxsuite/schema/t
267
268
  import { setBundleBase } from "./services/bundle-base";
268
269
  import { mountShellTree } from "./shell/tree";
269
270
 
271
+ // The bundler's `<link rel=modulepreload>` batching is installed on the module scope: before the
272
+ // First lazy `import()` of any surface that runs, so the palette's first open does not pay a
273
+ // Document recalculation per chunk (see the module's header for the trace that asked for this).
274
+ installModulePreloadBatcher();
275
+
270
276
  /**
271
277
  * Anchor every shipped-asset URL to THIS module's directory.
272
278
  *
@@ -20,6 +20,9 @@
20
20
  "paddingTop": "60px",
21
21
  "background": "rgba(0, 0, 0, 0.4)"
22
22
  },
23
+ "& [part=\"overlay\"][hidden]": {
24
+ "display": "none"
25
+ },
23
26
  "& [part=\"panel\"]": {
24
27
  "width": "560px",
25
28
  "maxWidth": "calc(100vw - 32px)",
@@ -97,14 +100,12 @@
97
100
  "color": "inherit"
98
101
  }
99
102
  },
100
- "$switch": {
101
- "$ref": "#/state/open"
102
- },
103
- "cases": {
104
- "true": {
103
+ "children": [
104
+ {
105
105
  "tagName": "div",
106
106
  "attributes": {
107
- "part": "overlay"
107
+ "part": "overlay",
108
+ "hidden": "${state.open ? null : ''}"
108
109
  },
109
110
  "onclick": {
110
111
  "$prototype": "Function",
@@ -360,5 +361,5 @@
360
361
  }
361
362
  ]
362
363
  }
363
- }
364
+ ]
364
365
  }
@@ -11,9 +11,12 @@
11
11
  *
12
12
  * **The mount is standing.** One slot in the popover layer, one document in it, and `open` is a
13
13
  * field of the scope rather than a render call — so opening the palette twice reuses the same
14
- * element tree and the `$switch` on `open` is what puts the overlay on screen and takes it away.
15
- * `getLayerSlot` hands back the same slot every time until something clears it, which is the one
16
- * case that re-mounts: a slot the layer no longer holds is not one this module may render into.
14
+ * element tree: the overlay is always PRESENT and a `hidden` attribute bound to `open` is what puts
15
+ * it on screen and takes it away, rather than a `$switch` that tore the whole subtree down and
16
+ * rebuilt it (and its `jx-listbox`) on every open — the per-open document-wide style recalculation
17
+ * that cost traced (`scripts/perf/REPORT.md`). `getLayerSlot` hands back the same slot every time
18
+ * until something clears it, which is the one case that re-mounts: a slot the layer no longer holds
19
+ * is not one this module may render into.
17
20
  *
18
21
  * **The rows and the highlight are separate fields**, exactly as `surfaces/palette.json`'s are —
19
22
  * and the highlight is now ONE field rather than a per-row comparison. A key moves `activeId`, the
@@ -285,9 +288,10 @@ function project(): void {
285
288
  /**
286
289
  * Focus the field once the document has drawn it.
287
290
  *
288
- * The mount settling means the DOCUMENT rendered; the `$switch` on `open` reconciles one microtask
289
- * later, so the field exists a turn after that — which is why this waits for the mount and then for
290
- * a turn, rather than reaching for the element straight away.
291
+ * The field is standing now, same as the overlay, but it stays behind `hidden` until the reactive
292
+ * binding on `open` reconciles — one microtask after the mount settles — so a focus reaching for it
293
+ * straight away would land on an element the reader cannot yet see. This waits for the mount and
294
+ * then for a turn, rather than reaching for the element immediately.
291
295
  */
292
296
  function focusInput(): void {
293
297
  const pending = _mount;
@@ -17,6 +17,9 @@
17
17
  "paddingTop": "60px",
18
18
  "background": "rgba(0, 0, 0, 0.4)"
19
19
  },
20
+ "& [part=\"overlay\"][hidden]": {
21
+ "display": "none"
22
+ },
20
23
  "& [part=\"panel\"]": {
21
24
  "width": "560px",
22
25
  "maxWidth": "calc(100vw - 32px)",
@@ -133,14 +136,12 @@
133
136
  "color": "var(--jx-fg-dim)"
134
137
  }
135
138
  },
136
- "$switch": {
137
- "$ref": "#/state/open"
138
- },
139
- "cases": {
140
- "true": {
139
+ "children": [
140
+ {
141
141
  "tagName": "div",
142
142
  "attributes": {
143
- "part": "overlay"
143
+ "part": "overlay",
144
+ "hidden": "${state.open ? null : ''}"
144
145
  },
145
146
  "onclick": {
146
147
  "$prototype": "Function",
@@ -531,5 +532,5 @@
531
532
  }
532
533
  ]
533
534
  }
534
- }
535
+ ]
535
536
  }
@@ -43,8 +43,7 @@ import {
43
43
  } from "../shell";
44
44
  import type { DockId } from "../shell";
45
45
  import { effect } from "../reactivity";
46
- import { rectOf } from "../utils/geometry";
47
- import { splitModifiersOf } from "@jxsuite/ui/behaviors/split";
46
+ import { splitModifiersOf, trackOf } from "@jxsuite/ui/behaviors/split";
48
47
  import type { SplitModifiers } from "@jxsuite/ui/behaviors/split";
49
48
 
50
49
  /**
@@ -160,25 +159,6 @@ export interface SplitElement extends HTMLElement {
160
159
  collapse: number;
161
160
  }
162
161
 
163
- /**
164
- * The length of the box a handle divides, along its axis — the same box `jx-split` measures at
165
- * pointerdown, read here so the share this adapter writes and the one the element drags against are
166
- * two readings of one number.
167
- */
168
- function trackOf(handle: HTMLElement, axis: "x" | "y"): number {
169
- for (let node = handle.parentElement; node; node = node.parentElement) {
170
- if (globalThis.getComputedStyle(node).display === "contents") {
171
- continue;
172
- }
173
- const box = rectOf(node);
174
- const length = axis === "x" ? box.width : box.height;
175
- if (length > 0) {
176
- return length;
177
- }
178
- }
179
- return 0;
180
- }
181
-
182
162
  /**
183
163
  * A target's pixel size as the leading side's share of the track, and back.
184
164
  *
@@ -231,7 +211,7 @@ function sizeOf(target: ResizeTarget, share: number, track: number): number {
231
211
  */
232
212
  export function bindSplit(handle: SplitElement, target: ResizeTarget): () => void {
233
213
  const sync = (): void => {
234
- const track = trackOf(handle, target.axis);
214
+ const track = trackOf(handle, target.axis === "x");
235
215
  if (track <= 0) {
236
216
  return;
237
217
  }
@@ -243,7 +223,7 @@ export function bindSplit(handle: SplitElement, target: ResizeTarget): () => voi
243
223
  handle.value = shareOf(target, target.read(), track);
244
224
  };
245
225
  const onInput = (event: Event): void => {
246
- const track = trackOf(handle, target.axis);
226
+ const track = trackOf(handle, target.axis === "x");
247
227
  if (track <= 0) {
248
228
  return;
249
229
  }
@@ -300,6 +300,44 @@ export function nearestScroller(el: HTMLElement | null): HTMLElement | null {
300
300
  return null;
301
301
  }
302
302
 
303
+ /**
304
+ * The scroller that resolved for `list` last time, memoized while the tree still holds it.
305
+ *
306
+ * Every render pass asks {@link nearestScroller} the same question about the same list, and each
307
+ * ask is a walk whose reads (scrollHeight, clientHeight, computed overflow) land on a tree that is
308
+ * mid-paint — they force the flush the DevTools timeline charges to whatever function happened to
309
+ * read first. Profiling the program boot put 400 ms of style recalculation behind a handful of
310
+ * these walks (`trackOf` and this one together), and the answer did not change between any two
311
+ * repaints.
312
+ *
313
+ * Only a POSITIVE resolution is cached, and negative answers are never: a list that has not grown
314
+ * its rows yet has nothing to scroll (the first paint of every session), and a null answer memoized
315
+ * at that instant would never be revisited. Once a scroller is found it is kept while
316
+ * `cached.contains(list)` and both are connected — containment and connectivity are plain node
317
+ * walks that never read layout, so the cache invalidates itself on reparenting or teardown without
318
+ * paying for it.
319
+ *
320
+ * **The one semantic residue is deliberate**: the original walk additionally required the scroller
321
+ * to HAVE something to scroll right then (the "declares overflow but never scrolls" guard above). A
322
+ * memoized scroller that has since stopped scrolling stays answered — which is harmless for the
323
+ * window: `computeWindow` then yields the whole list (a list that fits has no window to cut), and
324
+ * the watch's own ResizeObserver drives a measure when the box moves so the empty answer of the
325
+ * next moment arrives without a walk.
326
+ */
327
+ export function scrollerFor(list: HTMLElement): HTMLElement | null {
328
+ const cached = scrollerCache.get(list);
329
+ if (cached && cached.isConnected && list.isConnected && cached.contains(list)) {
330
+ return cached;
331
+ }
332
+ const found = nearestScroller(list);
333
+ if (found) {
334
+ scrollerCache.set(list, found);
335
+ } else {
336
+ scrollerCache.delete(list);
337
+ }
338
+ return found;
339
+ }
340
+
303
341
  /**
304
342
  * How far `list`'s first row has scrolled above the top of `scroller`'s viewport.
305
343
  *
@@ -327,7 +365,7 @@ export function listWindow(
327
365
  list: HTMLElement | null,
328
366
  spec: { count: number; rowHeight: number; columns?: number; overscanRows?: number },
329
367
  ): WindowRange {
330
- const scroller = list?.isConnected === true ? nearestScroller(list) : null;
368
+ const scroller = list?.isConnected === true ? scrollerFor(list) : null;
331
369
  if (!scroller || !list) {
332
370
  const count = Math.max(0, Math.trunc(spec.count));
333
371
  return { end: count, padBottom: 0, padTop: 0, start: 0, totalRows: count };
@@ -377,7 +415,7 @@ export function scrollTopToReveal(spec: {
377
415
  * would paint twice for one keystroke.
378
416
  */
379
417
  export function revealListRow(list: HTMLElement | null, index: number, rowHeight: number): boolean {
380
- const scroller = list?.isConnected === true ? nearestScroller(list) : null;
418
+ const scroller = list?.isConnected === true ? scrollerFor(list) : null;
381
419
  if (!scroller || !list || index < 0 || !(rowHeight > 0)) {
382
420
  return false;
383
421
  }
@@ -437,9 +475,24 @@ export interface ListWindowWatch {
437
475
  window: VirtualWindow;
438
476
  }
439
477
 
478
+ const scrollerCache = new WeakMap<HTMLElement, HTMLElement>();
479
+
440
480
  /**
441
481
  * Bind — or keep — the scroll watch that repaints a windowed list.
442
482
  *
483
+ * Runs synchronously, on purpose. Deferring the resolve-and-bind to the next animation frame was
484
+ * tried and measured (`scripts/perf/REPORT.md`, "deferring the first measure a frame"): it moved
485
+ * the boot trace's forced-flush cost rather than removing it, since the layout work still runs on
486
+ * the frame's own pass either way, and it cost eight test-semantics changes in this file's test
487
+ * suite for the same totals — so it was reverted. What actually removed the repeated flush is
488
+ * `scrollerFor`'s identity memoization (alongside `trackOf`'s, in `split.ts`): once a scroller is
489
+ * resolved for a list, later calls skip the ancestor walk entirely instead of re-reading
490
+ * `scrollHeight`/`clientHeight` on a dirty tree every render.
491
+ *
492
+ * A steady state where the SAME list and scroller are bound hands back `previous` unchanged — no
493
+ * work happens when nothing changed. Only a rebind (new list, changed scroller, first bind) creates
494
+ * a new watch.
495
+ *
443
496
  * Called from the surface's `afterRender`, where the rows exist and the scroller can be resolved,
444
497
  * and handed back its own previous handle so re-binding is the exception rather than the rule: the
445
498
  * tree element survives every re-render (lit reuses it), so the steady state is "same list, same
@@ -450,14 +503,14 @@ export interface ListWindowWatch {
450
503
  * the element whose box moves when the ROWS change height, so it is what the window observes to
451
504
  * learn that `rowHeight()` now answers differently ({@link createVirtualWindow}).
452
505
  *
453
- * Returns the handle to keep, or null when nothing scrolls this list.
506
+ * @returns The handle to keep, or null when nothing scrolls this list.
454
507
  */
455
508
  export function watchListWindow(
456
509
  previous: ListWindowWatch | null,
457
510
  list: HTMLElement,
458
511
  spec: { count: () => number; rowHeight: () => number; onChange: () => void },
459
512
  ): ListWindowWatch | null {
460
- const scroller = nearestScroller(list);
513
+ const scroller = scrollerFor(list);
461
514
  if (previous && previous.list === list && previous.scroller === scroller) {
462
515
  return previous;
463
516
  }
@@ -1,40 +0,0 @@
1
- import {
2
- startLayerTitleEdit2,
3
- registerLayersPanel2
4
- } from "./studio-hhskq2mr.js";
5
- import"./studio-2mdbbc76.js";
6
- import"./studio-a1pswps3.js";
7
- import"./studio-ncn9e189.js";
8
- import"./studio-5nz00bhh.js";
9
- import"./studio-ctm7bhx5.js";
10
- import"./studio-q0a3yk4n.js";
11
- import"./studio-4r14yzyy.js";
12
- import"./studio-x72j58vw.js";
13
- import"./studio-nq5yf5mp.js";
14
- import"./studio-etvx9s2g.js";
15
- import"./studio-mpzwze9x.js";
16
- import"./studio-zjjde71a.js";
17
- import"./studio-z2smx5ce.js";
18
- import"./studio-en82c639.js";
19
- import"./studio-c07s5ckd.js";
20
- import"./studio-ez11dmdh.js";
21
- import"./studio-wnsyffg1.js";
22
- import"./studio-m1p9b6c3.js";
23
- import"./studio-esrxgwh6.js";
24
- import"./studio-28vs5k2y.js";
25
- import"./studio-6nzaktvy.js";
26
- import"./studio-qa7fzxan.js";
27
- import"./studio-ympe0n7a.js";
28
- import"./studio-1k325pwf.js";
29
- import"./studio-p6pmwkvh.js";
30
- import"./studio-yxa81f45.js";
31
- import"./studio-3ntvq6cg.js";
32
- import"./studio-d8z5bajd.js";
33
- import"./studio-tjjk7jvk.js";
34
- export {
35
- registerLayersPanel2 as registerLayersPanel,
36
- startLayerTitleEdit2 as startLayerTitleEdit
37
- };
38
-
39
- //# debugId=0A17594D4CFC8C3764756E2164756E21
40
- //# sourceMappingURL=layers-panel-fksbdg4q.js.map
@@ -1,46 +0,0 @@
1
- import {
2
- initQuickSearch2,
3
- openQuickSearch2
4
- } from "./studio-66arbpc2.js";
5
- import"./studio-01jeck2y.js";
6
- import"./studio-vt0tj32j.js";
7
- import"./studio-wjpgxywa.js";
8
- import"./studio-t171hxhe.js";
9
- import"./studio-5nz00bhh.js";
10
- import"./studio-hdc2jt3z.js";
11
- import"./studio-t3adrr56.js";
12
- import"./studio-5t7ena5z.js";
13
- import"./studio-x10fp4tp.js";
14
- import"./studio-4c041r8y.js";
15
- import"./studio-q0a3yk4n.js";
16
- import"./studio-4r14yzyy.js";
17
- import"./studio-y4f9fdge.js";
18
- import"./studio-9j6sfeba.js";
19
- import"./studio-x72j58vw.js";
20
- import"./studio-nq5yf5mp.js";
21
- import"./studio-etvx9s2g.js";
22
- import"./studio-mpzwze9x.js";
23
- import"./studio-zjjde71a.js";
24
- import"./studio-z2smx5ce.js";
25
- import"./studio-en82c639.js";
26
- import"./studio-c07s5ckd.js";
27
- import"./studio-ez11dmdh.js";
28
- import"./studio-wnsyffg1.js";
29
- import"./studio-m1p9b6c3.js";
30
- import"./studio-esrxgwh6.js";
31
- import"./studio-28vs5k2y.js";
32
- import"./studio-6nzaktvy.js";
33
- import"./studio-qa7fzxan.js";
34
- import"./studio-ympe0n7a.js";
35
- import"./studio-1k325pwf.js";
36
- import"./studio-p6pmwkvh.js";
37
- import"./studio-yxa81f45.js";
38
- import"./studio-3ntvq6cg.js";
39
- import"./studio-d8z5bajd.js";
40
- export {
41
- initQuickSearch2 as initQuickSearch,
42
- openQuickSearch2 as openQuickSearch
43
- };
44
-
45
- //# debugId=1B07B932BE385C7264756E2164756E21
46
- //# sourceMappingURL=quick-search-e9rp74m8.js.map