@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.
- package/dist/chunks/{cf-account-picker-h4dab9bq.js → cf-account-picker-nwnwdm6p.js} +5 -5
- package/dist/chunks/{content-source-sgg1sv6n.js → content-source-4rzvmp0m.js} +16 -16
- package/dist/chunks/{convert-to-repeater-mf1cxgfn.js → convert-to-repeater-jxsv62v7.js} +16 -16
- package/dist/chunks/{entry-commands-w9qsxa6q.js → entry-commands-1wnsqn5d.js} +19 -19
- package/dist/chunks/{extension-sections-c6zwzbre.js → extension-sections-2mrptyek.js} +20 -20
- package/dist/chunks/{file-ops-vrpq1jjk.js → file-ops-knfqshrm.js} +15 -15
- package/dist/chunks/{files-msaf95y6.js → files-y6fr4he9.js} +24 -23
- package/dist/chunks/{format-host-f64fnesp.js → format-host-1k4qrcqd.js} +2 -2
- package/dist/chunks/{git-panel-bv5e22j0.js → git-panel-2rkrsjva.js} +15 -15
- package/dist/chunks/{grid-view-paj67nr5.js → grid-view-a7b87n2c.js} +18 -18
- package/dist/chunks/{iframe-host-dh5np8vk.js → iframe-host-scpw01h8.js} +7 -7
- package/dist/chunks/{jx-validate-j0wxk72w.js → jx-validate-epp2z3s3.js} +2 -2
- package/dist/chunks/{layers-1vcfdbet.js → layers-jnvdbqva.js} +3 -3
- package/dist/chunks/layers-panel-73gfkcga.js +41 -0
- package/dist/chunks/{listbox-6v2ac08j.js → listbox-b7jxnhq1.js} +19 -3
- package/dist/chunks/{media-picker-p49nhqhs.js → media-picker-f66nav16.js} +9 -9
- package/dist/chunks/{menu-qht48vnc.js → menu-1dfhry4s.js} +4 -4
- package/dist/chunks/{monaco-setup-8r62fj6y.js → monaco-setup-km6abc6q.js} +2 -2
- package/dist/chunks/{publish-panel-vz23wwmg.js → publish-panel-z1zd2da1.js} +8 -8
- package/dist/chunks/quick-search-zdv7h8ht.js +47 -0
- package/dist/chunks/{right-panel-j97t2yvy.js → right-panel-889vbnm5.js} +18 -18
- package/dist/chunks/{split-g7nm1rgb.js → split-4cz4b6ef.js} +14 -3
- package/dist/chunks/{studio-ympe0n7a.js → studio-0j7r7s9g.js} +2 -2
- package/dist/chunks/{studio-68fydsrj.js → studio-151w0xnh.js} +13 -13
- package/dist/chunks/{studio-x72j58vw.js → studio-27q131j3.js} +3 -3
- package/dist/chunks/{studio-ctm7bhx5.js → studio-4f3psrk1.js} +5 -5
- package/dist/chunks/{studio-t3adrr56.js → studio-50wfwy44.js} +7 -7
- package/dist/chunks/{studio-28vs5k2y.js → studio-6gryj0sg.js} +5 -5
- package/dist/chunks/{studio-ncn9e189.js → studio-6kvgrkrp.js} +5 -5
- package/dist/chunks/{studio-14d9tq4d.js → studio-7144epdr.js} +5 -5
- package/dist/chunks/{studio-01jeck2y.js → studio-7rrb1esn.js} +16 -16
- package/dist/chunks/{studio-38emantv.js → studio-a5h5j33a.js} +19 -5
- package/dist/chunks/{studio-4r14yzyy.js → studio-a65w20ex.js} +2 -2
- package/dist/chunks/{studio-c07s5ckd.js → studio-ab87phj1.js} +3 -3
- package/dist/chunks/{studio-hhskq2mr.js → studio-b66rsh78.js} +12 -12
- package/dist/chunks/{studio-en82c639.js → studio-b7xacrwe.js} +4 -4
- package/dist/chunks/{studio-15f3kxqe.js → studio-c0qgc3tn.js} +10 -10
- package/dist/chunks/{studio-y4f9fdge.js → studio-cgprcdxn.js} +6 -6
- package/dist/chunks/{studio-p6pmwkvh.js → studio-d1n2bykn.js} +32 -5
- package/dist/chunks/{studio-4xmjcrg3.js → studio-dd1qqkry.js} +4 -4
- package/dist/chunks/{studio-etvx9s2g.js → studio-drg8qj9k.js} +6 -6
- package/dist/chunks/{studio-kcgff4r0.js → studio-fgmfksz8.js} +2 -2
- package/dist/chunks/{studio-mpzwze9x.js → studio-fzjx96t2.js} +4 -4
- package/dist/chunks/{studio-66arbpc2.js → studio-g33v4fe9.js} +15 -14
- package/dist/chunks/{studio-ez11dmdh.js → studio-gk2twyh4.js} +2 -2
- package/dist/chunks/{studio-5nz00bhh.js → studio-gtfq1dkz.js} +3 -3
- package/dist/chunks/{studio-5t7ena5z.js → studio-j0w60gft.js} +5 -5
- package/dist/chunks/{studio-qa7fzxan.js → studio-jt27ta6x.js} +3 -3
- package/dist/chunks/{studio-x10fp4tp.js → studio-ke09tn9k.js} +11 -11
- package/dist/chunks/{studio-d0300636.js → studio-ks2zzth2.js} +17 -2
- package/dist/chunks/{studio-wjpgxywa.js → studio-msc1ee1m.js} +3 -3
- package/dist/chunks/{studio-2mdbbc76.js → studio-nfx354t1.js} +7 -7
- package/dist/chunks/{studio-9j6sfeba.js → studio-nsgw7dbk.js} +4 -4
- package/dist/chunks/{studio-hdc2jt3z.js → studio-rtskkm43.js} +3 -3
- package/dist/chunks/{studio-vt0tj32j.js → studio-xajk70w0.js} +16 -16
- package/dist/chunks/{studio-a1pswps3.js → studio-ye9drbk8.js} +24 -23
- package/dist/chunks/{studio-sar8g54p.js → studio-ze46gzjh.js} +7 -7
- package/dist/chunks/{workspace-bnw54cha.js → workspace-np10mayv.js} +3 -3
- package/dist/iframe-entry.js +3 -3
- package/dist/studio.js +116 -79
- package/package.json +5 -5
- package/src/panels/chat-panel.ts +40 -5
- package/src/services/module-preloads.ts +67 -0
- package/src/studio.ts +6 -0
- package/src/surfaces/formula-palette.json +8 -7
- package/src/surfaces/formula-palette.ts +10 -6
- package/src/surfaces/palette.json +8 -7
- package/src/ui/panel-resize.ts +3 -23
- package/src/ui/virtual-window.ts +57 -4
- package/dist/chunks/layers-panel-fksbdg4q.js +0 -40
- package/dist/chunks/quick-search-e9rp74m8.js +0 -46
package/src/panels/chat-panel.ts
CHANGED
|
@@ -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
|
|
16
|
-
* project but no open document, and with a document open. The
|
|
17
|
-
* and never tears down on tab switches — the assistant's module state
|
|
18
|
-
* scroll position)
|
|
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
|
-
|
|
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
|
-
"
|
|
101
|
-
|
|
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
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
|
289
|
-
*
|
|
290
|
-
*
|
|
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
|
-
"
|
|
137
|
-
|
|
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
|
}
|
package/src/ui/panel-resize.ts
CHANGED
|
@@ -43,8 +43,7 @@ import {
|
|
|
43
43
|
} from "../shell";
|
|
44
44
|
import type { DockId } from "../shell";
|
|
45
45
|
import { effect } from "../reactivity";
|
|
46
|
-
import {
|
|
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
|
}
|
package/src/ui/virtual-window.ts
CHANGED
|
@@ -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 ?
|
|
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 ?
|
|
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
|
-
*
|
|
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 =
|
|
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
|