@jxsuite/studio 2.2.2 → 2.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 (182) hide show
  1. package/data/css-meta.json +1487 -0
  2. package/data/elements-meta.json +370 -0
  3. package/data/html-meta.json +366 -0
  4. package/data/stylebook-meta.json +200 -0
  5. package/data/webdata.json +1621 -0
  6. package/dist/chunks/{Overlay.dev-ymy3et3y.js → Overlay.dev-d2ajy6sj.js} +7 -7
  7. package/dist/chunks/{content-source-r2n7v682.js → content-source-56xpvx83.js} +13 -12
  8. package/dist/chunks/{convert-to-repeater-cnpyvj74.js → convert-to-repeater-0xh9wwek.js} +12 -11
  9. package/dist/chunks/{extension-sections-vpcm4jvx.js → extension-sections-g34239vm.js} +17 -16
  10. package/dist/chunks/{files-zeg88k2d.js → files-gkz202dp.js} +18 -19
  11. package/dist/chunks/{format-host-taymsw55.js → format-host-pej3srnw.js} +2 -2
  12. package/dist/chunks/{git-panel-qp4rrwr9.js → git-panel-4j6xbh8g.js} +12 -11
  13. package/dist/chunks/{layers-panel-kj93w2zg.js → layers-panel-8mvn8sgd.js} +15 -14
  14. package/dist/chunks/{monaco-setup-xy3bs05d.js → monaco-setup-a5wkmbgh.js} +6 -3
  15. package/dist/chunks/{publish-panel-6qz6kp41.js → publish-panel-ptddc3kg.js} +6 -6
  16. package/dist/chunks/{quick-search-9wwa09hh.js → quick-search-j88azfs2.js} +19 -18
  17. package/dist/chunks/right-panel-egkaxy89.js +38 -0
  18. package/dist/chunks/{sp-overlay.dev-h6kdx076.js → sp-overlay.dev-s18hymn9.js} +8 -8
  19. package/dist/chunks/sp-progress-circle.dev-0zxcyz3r.js +15 -0
  20. package/dist/chunks/sp-tooltip.dev-5c6d1kjz.js +15 -0
  21. package/dist/chunks/{studio-753znj4c.js → studio-0e4h32g6.js} +5 -5
  22. package/dist/chunks/{studio-yz9s88s0.js → studio-10s469da.js} +25 -9
  23. package/dist/chunks/{studio-6f0am8f2.js → studio-2w11grav.js} +2 -2
  24. package/dist/chunks/{studio-zqv3w24n.js → studio-3q65xvzv.js} +5 -5
  25. package/dist/chunks/{studio-qhh8r5fz.js → studio-4058pg09.js} +4 -4
  26. package/dist/chunks/{studio-hekaagev.js → studio-4enps87v.js} +17 -17
  27. package/dist/chunks/{studio-tqemhphj.js → studio-50eed4c9.js} +4 -4
  28. package/dist/chunks/studio-53n8bp04.js +16 -0
  29. package/dist/chunks/{studio-nqq8jk8f.js → studio-5amdrjm4.js} +14 -14
  30. package/dist/chunks/{studio-6zzbyx17.js → studio-5ne8mdx9.js} +2 -2
  31. package/dist/chunks/{studio-3z8jjm2p.js → studio-7ejemhej.js} +3 -3
  32. package/dist/chunks/{studio-1kgfcv7s.js → studio-a11d3a1b.js} +12 -11
  33. package/dist/chunks/{studio-rgwcpast.js → studio-ay8wcr2b.js} +2 -2
  34. package/dist/chunks/{studio-fys6z73y.js → studio-bak2cyjr.js} +7 -7
  35. package/dist/chunks/{studio-xmvbhg31.js → studio-c31yb4t6.js} +2 -2
  36. package/dist/chunks/{studio-8sdyjfd6.js → studio-cf4rk7kv.js} +2 -2
  37. package/dist/chunks/{studio-yphy4z9m.js → studio-cv606rs1.js} +5 -5
  38. package/dist/chunks/{studio-5kt960pk.js → studio-hk523hby.js} +133 -147
  39. package/dist/chunks/{studio-x0et2z9g.js → studio-hs8x23fv.js} +15 -15
  40. package/dist/chunks/{studio-p471gg5n.js → studio-kvfx9z2c.js} +7 -3
  41. package/dist/chunks/{studio-22xr4q8q.js → studio-ncnet0ba.js} +18 -29
  42. package/dist/chunks/{studio-ckwz4d3x.js → studio-twsafr1e.js} +2 -2
  43. package/dist/chunks/{studio-qrsfh1w7.js → studio-tzx3xmvx.js} +5 -5
  44. package/dist/chunks/{studio-g8t7g0gp.js → studio-vdpcpada.js} +3 -3
  45. package/dist/chunks/{studio-6a0h7sne.js → studio-vhk5fmfv.js} +2 -2
  46. package/dist/chunks/{studio-hrajjyt2.js → studio-wdt9y92a.js} +2 -2
  47. package/dist/chunks/{studio-r8vx7bkz.js → studio-x3etxs0s.js} +14 -13
  48. package/dist/chunks/{studio-pv8m0h4a.js → studio-yxzgjq8q.js} +28 -23
  49. package/dist/chunks/{studio-300zsz1k.js → studio-zdkgwhkv.js} +6 -6
  50. package/dist/codicon.ttf +0 -0
  51. package/dist/iframe-entry.js +8 -1
  52. package/dist/manifest.json +82 -0
  53. package/dist/studio.js +232 -103
  54. package/index.html +0 -50
  55. package/package.json +11 -4
  56. package/src/canvas/iframe-entry.ts +5 -0
  57. package/src/canvas/iframe-host.ts +45 -4
  58. package/src/content/entry-editor.ts +2 -1
  59. package/src/editor/context-menu.ts +13 -2
  60. package/src/editor/slash-menu.ts +26 -11
  61. package/src/files/files.ts +11 -23
  62. package/src/grid/grid-panel.ts +27 -6
  63. package/src/hosting/document.ts +109 -0
  64. package/src/hosting/layout.ts +211 -0
  65. package/src/hosting/stage.ts +166 -0
  66. package/src/hosting/tsconfig.json +18 -0
  67. package/src/panels/ai-chat/chat-view.ts +25 -14
  68. package/src/panels/git-panel.ts +17 -14
  69. package/src/panels/head-panel.ts +32 -14
  70. package/src/panels/imports-panel.ts +32 -13
  71. package/src/panels/layers-panel.ts +9 -1
  72. package/src/panels/panel-registry.ts +0 -1
  73. package/src/panels/properties-panel.ts +3 -3
  74. package/src/panels/settings-pane.ts +1 -1
  75. package/src/panels/style-panel.ts +3 -3
  76. package/src/panels/stylebook-panel.ts +10 -1
  77. package/src/panels/toolbar.ts +1 -1
  78. package/src/services/bundle-base.ts +69 -0
  79. package/src/services/monaco-setup.ts +19 -13
  80. package/src/shell/tree.ts +155 -0
  81. package/src/shell.ts +0 -2
  82. package/src/studio.ts +22 -2
  83. package/src/types.ts +15 -0
  84. package/src/ui/regions.ts +10 -45
  85. package/styles/overlays.css +35 -0
  86. package/dist/chunks/2020-xp8vnwma.js.map +0 -24
  87. package/dist/chunks/Overlay.dev-ymy3et3y.js.map +0 -9
  88. package/dist/chunks/adapter-jkcyhgtr.js.map +0 -9
  89. package/dist/chunks/cf-settings-g5fbf2zp.js.map +0 -9
  90. package/dist/chunks/components-z6cnkyy1.js.map +0 -9
  91. package/dist/chunks/content-source-r2n7v682.js.map +0 -9
  92. package/dist/chunks/convert-to-repeater-cnpyvj74.js.map +0 -10
  93. package/dist/chunks/editor-cptq5jp1.js.map +0 -9
  94. package/dist/chunks/extension-sections-vpcm4jvx.js.map +0 -11
  95. package/dist/chunks/files-zeg88k2d.js.map +0 -9
  96. package/dist/chunks/flow-qbfgrpeh.js.map +0 -12
  97. package/dist/chunks/focus-trap.esm-ggawf1hy.js.map +0 -11
  98. package/dist/chunks/focus-visible-6eg70z18.js.map +0 -10
  99. package/dist/chunks/format-host-taymsw55.js.map +0 -9
  100. package/dist/chunks/git-panel-qp4rrwr9.js.map +0 -9
  101. package/dist/chunks/index-nj44jpzj.js.map +0 -14
  102. package/dist/chunks/index-zqxmqxmh.js.map +0 -9
  103. package/dist/chunks/javascript-q8kpmfah.js.map +0 -11
  104. package/dist/chunks/jsonMode-dkr040ga.js.map +0 -18
  105. package/dist/chunks/jx-validate-jj9rnt4n.js.map +0 -9
  106. package/dist/chunks/layers-panel-kj93w2zg.js.map +0 -9
  107. package/dist/chunks/monaco-binding-rj6xpx9y.js.map +0 -10
  108. package/dist/chunks/monaco-setup-xy3bs05d.js.map +0 -15
  109. package/dist/chunks/publish-panel-6qz6kp41.js.map +0 -9
  110. package/dist/chunks/quick-search-9wwa09hh.js.map +0 -9
  111. package/dist/chunks/right-panel-faf816ch.js +0 -37
  112. package/dist/chunks/right-panel-faf816ch.js.map +0 -9
  113. package/dist/chunks/section-registry-sx1ykvkw.js.map +0 -9
  114. package/dist/chunks/sp-overlay.dev-h6kdx076.js.map +0 -9
  115. package/dist/chunks/sp-popover.dev-dj9x7e7a.js.map +0 -9
  116. package/dist/chunks/sp-progress-circle.dev-0fpvhm52.js +0 -15
  117. package/dist/chunks/sp-progress-circle.dev-0fpvhm52.js.map +0 -9
  118. package/dist/chunks/sp-tooltip.dev-rvepkw4r.js +0 -15
  119. package/dist/chunks/sp-tooltip.dev-rvepkw4r.js.map +0 -9
  120. package/dist/chunks/sp-tray.dev-64v4pxw8.js.map +0 -12
  121. package/dist/chunks/studio-0a5qrh01.js.map +0 -9
  122. package/dist/chunks/studio-0rqqhrdy.js.map +0 -12
  123. package/dist/chunks/studio-18j3bmcz.js.map +0 -10
  124. package/dist/chunks/studio-1kgfcv7s.js.map +0 -13
  125. package/dist/chunks/studio-22xr4q8q.js.map +0 -13
  126. package/dist/chunks/studio-300zsz1k.js.map +0 -39
  127. package/dist/chunks/studio-3fk1ac9z.js.map +0 -12
  128. package/dist/chunks/studio-3n4ndrzh.js.map +0 -41
  129. package/dist/chunks/studio-3z8jjm2p.js.map +0 -13
  130. package/dist/chunks/studio-4mqb9a0j.js.map +0 -14
  131. package/dist/chunks/studio-4q0wznzf.js.map +0 -73
  132. package/dist/chunks/studio-5ehhackm.js.map +0 -10
  133. package/dist/chunks/studio-5kt960pk.js.map +0 -38
  134. package/dist/chunks/studio-5x5774ak.js.map +0 -10
  135. package/dist/chunks/studio-685s0ytq.js.map +0 -76
  136. package/dist/chunks/studio-6a0h7sne.js.map +0 -15
  137. package/dist/chunks/studio-6f0am8f2.js.map +0 -10
  138. package/dist/chunks/studio-6kv3apa9.js.map +0 -10
  139. package/dist/chunks/studio-6zzbyx17.js.map +0 -12
  140. package/dist/chunks/studio-753znj4c.js.map +0 -16
  141. package/dist/chunks/studio-7afd21nw.js.map +0 -10
  142. package/dist/chunks/studio-8jpzqk5s.js.map +0 -10
  143. package/dist/chunks/studio-8q517dfr.js.map +0 -10
  144. package/dist/chunks/studio-8sdyjfd6.js.map +0 -10
  145. package/dist/chunks/studio-a0tswy0w.js.map +0 -9
  146. package/dist/chunks/studio-a7pzveq1.js.map +0 -11
  147. package/dist/chunks/studio-ahnzd9rg.js.map +0 -10
  148. package/dist/chunks/studio-b9b6t1dg.js.map +0 -651
  149. package/dist/chunks/studio-bw5dz6km.js.map +0 -21
  150. package/dist/chunks/studio-cjm7fdjf.js.map +0 -14
  151. package/dist/chunks/studio-ckwz4d3x.js.map +0 -12
  152. package/dist/chunks/studio-d1ks410h.js.map +0 -10
  153. package/dist/chunks/studio-dg7zyt5x.js.map +0 -10
  154. package/dist/chunks/studio-fys6z73y.js.map +0 -10
  155. package/dist/chunks/studio-g1wdche0.js.map +0 -56
  156. package/dist/chunks/studio-g8t7g0gp.js.map +0 -10
  157. package/dist/chunks/studio-h4rxscfc.js.map +0 -10
  158. package/dist/chunks/studio-hekaagev.js.map +0 -18
  159. package/dist/chunks/studio-hrajjyt2.js.map +0 -10
  160. package/dist/chunks/studio-nqq8jk8f.js.map +0 -14
  161. package/dist/chunks/studio-p471gg5n.js.map +0 -14
  162. package/dist/chunks/studio-pv8m0h4a.js.map +0 -98
  163. package/dist/chunks/studio-qhh8r5fz.js.map +0 -13
  164. package/dist/chunks/studio-qrsfh1w7.js.map +0 -41
  165. package/dist/chunks/studio-r8vx7bkz.js.map +0 -235
  166. package/dist/chunks/studio-rgwcpast.js.map +0 -12
  167. package/dist/chunks/studio-s519zc99.js.map +0 -10
  168. package/dist/chunks/studio-sas7mt3k.js.map +0 -10
  169. package/dist/chunks/studio-t5wqwzdh.js.map +0 -10
  170. package/dist/chunks/studio-tqemhphj.js.map +0 -11
  171. package/dist/chunks/studio-tr20m8g8.js.map +0 -9
  172. package/dist/chunks/studio-tvgyw9xw.js.map +0 -14
  173. package/dist/chunks/studio-x0et2z9g.js.map +0 -32
  174. package/dist/chunks/studio-xm3dbxvn.js.map +0 -19
  175. package/dist/chunks/studio-xmvbhg31.js.map +0 -10
  176. package/dist/chunks/studio-ynxwkrzd.js.map +0 -246
  177. package/dist/chunks/studio-yphy4z9m.js.map +0 -10
  178. package/dist/chunks/studio-yz9s88s0.js.map +0 -54
  179. package/dist/chunks/studio-z6nw2ek2.js.map +0 -10
  180. package/dist/chunks/studio-zqv3w24n.js.map +0 -11
  181. package/dist/chunks/tsMode-76kr53q0.js.map +0 -13
  182. package/dist/chunks/ws-client-6shc5rze.js.map +0 -12
package/index.html CHANGED
@@ -11,59 +11,9 @@
11
11
  <link rel="stylesheet" href="./styles/panels.css" />
12
12
  <link rel="stylesheet" href="./styles/inspector.css" />
13
13
  <link rel="stylesheet" href="./styles/overlays.css" />
14
- <!-- Last: forced colours redraws what Windows High Contrast deletes. -->
15
14
  <link rel="stylesheet" href="./styles/forced-colors.css" />
16
15
  </head>
17
16
  <body>
18
- <sp-theme color="dark" scale="medium" system="spectrum">
19
- <div id="app">
20
- <div id="toolbar"></div>
21
- <!-- The PANE GRID (spec §18.1). One cell per pane, each holding that pane's own strip,
22
- jump bar, chrome layer and stage — the four surfaces that used to be flat siblings of
23
- THIS grid, which is to say application rows that only ever described one pane. There
24
- is nothing to see here in markup: `panels/pane-grid.ts` reconciles the cells against
25
- `workspace.panes`, because how many there are is a fact about the workspace and a
26
- `<div id>` can only ever be one of them. -->
27
- <div id="pane-grid"></div>
28
- <div id="activity-bar"></div>
29
- <div id="left-panel"></div>
30
- <div id="resize-left" class="resize-handle"></div>
31
- <!-- ⑪ The Bottom dock (⌘J). It sits in the PANE's column, under the stage and above the
32
- status bar, so opening it never narrows the Navigator or the Inspector (spec §12
33
- region ⑪). Its handle resizes on the other axis; `panels/bottom-dock.ts` stamps
34
- `dock.bottom` on this host while it is open, and nothing while it is closed. -->
35
- <div id="resize-bottom" class="resize-handle resize-handle-row"></div>
36
- <div id="bottom-dock"></div>
37
- <div id="resize-right" class="resize-handle"></div>
38
- <!-- The Inspector dock. Four tabs — Content · Style · Logic · Assistant (spec §12 region
39
- ⑨) — all inside this one host. The assistant's fifth grid column is gone. -->
40
- <div id="right-panel"></div>
41
- <!-- The app's only status channel: announce what lands in it (spec §12 region ⑫). -->
42
- <div id="statusbar" role="status" aria-live="polite"></div>
43
- </div>
44
- <div
45
- id="layer-popover"
46
- style="position: fixed; inset: 0; pointer-events: none; z-index: 1000"
47
- ></div>
48
- <div
49
- id="layer-modal"
50
- style="position: fixed; inset: 0; pointer-events: none; z-index: 2000"
51
- ></div>
52
- <div
53
- id="layer-dialog"
54
- style="position: fixed; inset: 0; pointer-events: none; z-index: 3000"
55
- ></div>
56
- <!-- The toast host: the fourth overlay layer (spec §12 region ④, §7.1). Above the dialog
57
- layer, because an operation started from a dialog reports its outcome to the person
58
- still looking at the dialog. `role="status"` sits on the HOST, so a stack of toasts is
59
- announced as one live region rather than as one region per notification. -->
60
- <div
61
- id="layer-toast"
62
- role="status"
63
- aria-live="polite"
64
- style="position: fixed; inset: 0; pointer-events: none; z-index: 4000"
65
- ></div>
66
- </sp-theme>
67
17
  <script type="module" src="./dist/studio.js"></script>
68
18
  </body>
69
19
  </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "2.2.2",
3
+ "version": "2.3.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -9,12 +9,15 @@
9
9
  "directory": "packages/studio"
10
10
  },
11
11
  "files": [
12
- "*.js",
13
12
  "src",
13
+ "data",
14
14
  "dist/studio.js",
15
15
  "dist/studio.css",
16
16
  "dist/iframe-entry.js",
17
- "dist/chunks",
17
+ "dist/codicon.ttf",
18
+ "dist/manifest.json",
19
+ "dist/chunks/*.js",
20
+ "dist/chunks/*.css",
18
21
  "dist/workers",
19
22
  "index.html",
20
23
  "canvas.html",
@@ -30,6 +33,10 @@
30
33
  "./directory-picker": "./src/services/directory-picker.ts",
31
34
  "./import-client": "./src/services/import-client.ts",
32
35
  "./github-auth": "./src/github/github-auth.ts",
36
+ "./hosting": "./src/hosting/stage.ts",
37
+ "./hosting/layout": "./src/hosting/layout.ts",
38
+ "./hosting/document": "./src/hosting/document.ts",
39
+ "./package.json": "./package.json",
33
40
  "./platforms/cloud": "./src/platforms/cloud.ts",
34
41
  "./preview-navigate": "./src/canvas/preview-navigate.ts"
35
42
  },
@@ -38,7 +45,7 @@
38
45
  },
39
46
  "scripts": {
40
47
  "build": "bun run scripts/build.ts",
41
- "build:metafile": "bun build ./src/studio.ts --outdir dist --target browser --sourcemap=linked --metafile=metafile.json",
48
+ "build:metafile": "bun build ./src/studio.ts --outdir dist-metafile --target browser --sourcemap=linked --metafile=metafile.json",
42
49
  "gen:webdata": "bun run scripts/gen-webdata.js",
43
50
  "eval": "bun run evals/cli.ts",
44
51
  "eval:headless": "bun run tests/harness/run-eval.ts",
@@ -56,6 +56,11 @@ import type {
56
56
  } from "./iframe-protocol";
57
57
  import type { JxDocument, JxMutableNode, JxStyle } from "@jxsuite/schema/types";
58
58
  import type { IframeRenderCtx, RenderHandle } from "./iframe-render";
59
+ import { setBundleBase } from "../services/bundle-base";
60
+
61
+ /* Anchor shipped-asset URLs to this ENTRY's directory — see src/studio.ts for why only an
62
+ entry may, and tests/entry-anchors.test.ts for the guard. */
63
+ setBundleBase(import.meta.url);
59
64
 
60
65
  /**
61
66
  * Resolve the drop placement for a forwarded cursor: point hit-test → nearest `[data-jx-path]` →
@@ -67,6 +67,7 @@ import type { OverlayLayer, OverlayPlacement } from "./iframe-overlay";
67
67
  import type { SlashCommand } from "../editor/inline-edit";
68
68
  import type { Tab } from "../tabs/tab";
69
69
  import type { JxExpressionNode, JxMutableNode } from "@jxsuite/schema/types";
70
+ import { bundleUrl } from "../services/bundle-base";
70
71
 
71
72
  /** A rect in PARENT coordinates (overlay-local from {@link canvasRectToParent}, same field shape). */
72
73
  type ParentRect = OverlayPlacement;
@@ -1473,10 +1474,27 @@ function requestStylebookSelection(host: HostState): void {
1473
1474
  }
1474
1475
 
1475
1476
  /**
1476
- * The default iframe document URL (a static shell that boots the slim canvas bundle). Used when the
1477
- * platform does not provide its own canvasUrl — i.e. the dev server and electrobun keep this path.
1477
+ * The canvas document, beside the bundle that asked for it.
1478
+ *
1479
+ * This was the literal `/packages/studio/canvas.html` — a repo dev-server path baked into a browser
1480
+ * bundle, correct on exactly one host out of four. It happened to work because every other host
1481
+ * overrides `canvasUrl`: the chromium launcher builds one from its token, electrobun fetches one
1482
+ * over RPC, and the cloud adapter hard-codes its own. So the default was never the default; it was
1483
+ * the dev server's URL with nothing saying so.
1484
+ *
1485
+ * `bundleUrl` resolves it against the ENTRY's directory, which is the one fact every host agrees on
1486
+ * (see services/bundle-base.ts), so the fallback is now correct everywhere rather than accidentally
1487
+ * unused.
1478
1488
  */
1479
- const DEFAULT_CANVAS_URL = "/packages/studio/canvas.html";
1489
+ function defaultCanvasUrl(): string {
1490
+ return bundleUrl("../canvas.html");
1491
+ }
1492
+
1493
+ /**
1494
+ * Loads nothing, deliberately: what a platform that resolves its canvasUrl LATER gets in the
1495
+ * meantime. See {@link StudioPlatform.canvasUrlDeferred}.
1496
+ */
1497
+ const DEFERRED_CANVAS_URL = "about:blank";
1480
1498
 
1481
1499
  /**
1482
1500
  * Release one host: its channel, its overlay, its frame, and everything awaiting a reply from it.
@@ -1523,6 +1541,29 @@ function releaseHost(host: HostState): void {
1523
1541
  liveHosts.delete(host);
1524
1542
  }
1525
1543
 
1544
+ /**
1545
+ * Which document this pane's canvas iframe should load.
1546
+ *
1547
+ * The `canvasUrlDeferred` branch is the one that needs saying. Electrobun resolves its canvasUrl
1548
+ * asynchronously — it is this window's loopback port, fetched over RPC inside `activate()` — and
1549
+ * until the fix above, the bundle-relative default resolved to nothing servable under `views://`,
1550
+ * so an early frame simply failed and `ensureHost` rebuilt against the real URL when it landed. Now
1551
+ * that default RESOLVES: `views://studio/canvas.html` is a document electrobun really does stage.
1552
+ * An early frame would therefore boot the whole canvas bundle inside the SHELL's app-privileged
1553
+ * origin, in a CEF instance running `disable-site-isolation-trials` — and the cross-origin loopback
1554
+ * canvas exists precisely so that never happens.
1555
+ */
1556
+ function resolveCanvasUrl(): string {
1557
+ if (!hasPlatform()) {
1558
+ return defaultCanvasUrl();
1559
+ }
1560
+ const platform = getPlatform();
1561
+ if (platform.canvasUrl) {
1562
+ return platform.canvasUrl;
1563
+ }
1564
+ return platform.canvasUrlDeferred ? DEFERRED_CANVAS_URL : defaultCanvasUrl();
1565
+ }
1566
+
1526
1567
  /**
1527
1568
  * Release every canvas host mounted under `root`, and say how many there were.
1528
1569
  *
@@ -1554,7 +1595,7 @@ export function releaseCanvasHosts(root: HTMLElement): number {
1554
1595
  function ensureHost(canvasEl: HTMLElement): HostState {
1555
1596
  // Read the platform's canvasUrl when one is registered; otherwise fall back to the default. The
1556
1597
  // Dev server leaves it unset, and some tests mount without a platform registered.
1557
- const canvasUrl = (hasPlatform() ? getPlatform().canvasUrl : undefined) ?? DEFAULT_CANVAS_URL;
1598
+ const canvasUrl = resolveCanvasUrl();
1558
1599
  const existing = hosts.get(canvasEl);
1559
1600
  if (existing) {
1560
1601
  if (existing.canvasUrl === canvasUrl) {
@@ -34,6 +34,7 @@
34
34
  */
35
35
 
36
36
  import { html, nothing, render as litRender } from "lit-html";
37
+ import { live } from "lit-html/directives/live.js";
37
38
  import { activeRegistry } from "../commands/active-registry";
38
39
  import { effect, effectScope } from "../reactivity";
39
40
  import { projectState } from "../store";
@@ -182,7 +183,7 @@ function headerTpl(tab: Tab, collection: EntryCollection) {
182
183
  <sp-switch
183
184
  size="s"
184
185
  class="entry-draft-switch"
185
- ?checked=${draft}
186
+ .checked=${live(draft)}
186
187
  title=${DRAFT_MEANING}
187
188
  @change=${(e: Event) => setEntryDraft(tab, (e.target as HTMLInputElement).checked)}
188
189
  >Draft</sp-switch
@@ -849,7 +849,17 @@ function itemElements(): HTMLElement[] {
849
849
  : [];
850
850
  }
851
851
 
852
- /** Move the roving tabindex (and the caret) to `index`, wrapping at both ends. */
852
+ /**
853
+ * Move the roving tabindex (and the caret) to `index`, wrapping at both ends.
854
+ *
855
+ * Still imperative, deliberately and not happily. The menu's template is inline in the
856
+ * `renderPopover` call and its `ref` closure MUTATES the `x`/`y` it was rendered with to clamp the
857
+ * popover into the viewport, so re-rendering to move focus would also re-run the placement from
858
+ * stale coordinates. The template's own `tabindex`/`?focused` now read `_activeIdx` rather than
859
+ * hard-coding row 0, so first paint and this function at least agree on where focus starts; making
860
+ * the binding the only writer needs the template hoisted out and the placement state with it.
861
+ * Carried in check-lit-conventions' allow-list with that reason.
862
+ */
853
863
  function focusItem(index: number): void {
854
864
  const items = itemElements();
855
865
  if (items.length === 0) {
@@ -978,7 +988,8 @@ function rowTemplate(row: MenuRow, index: number) {
978
988
  }<sp-menu-item
979
989
  role="menuitem"
980
990
  data-command-id=${row.command.id}
981
- tabindex=${index === 0 ? 0 : -1}
991
+ tabindex=${index === _activeIdx ? 0 : -1}
992
+ ?focused=${index === _activeIdx}
982
993
  ?disabled=${!row.enabled}
983
994
  aria-disabled=${row.enabled ? "false" : "true"}
984
995
  style=${row.command.destructive && row.enabled ? "color: var(--danger)" : ""}
@@ -58,6 +58,9 @@ export interface SlashMenuAnchorRect {
58
58
 
59
59
  let callbacks: SlashMenuCallbacks | null = null;
60
60
  let activeIdx = 0;
61
+
62
+ /** What the current open() asked for, so a repaint driven by the keyboard reproduces it. */
63
+ let showFilter = false;
61
64
  let filteredItems: SlashCommand[] = [];
62
65
  let open = false;
63
66
  let _anchorRect: SlashMenuAnchorRect | null = null;
@@ -123,7 +126,8 @@ function showAt(rect: SlashMenuAnchorRect, filter: string, cbs: SlashMenuCallbac
123
126
 
124
127
  activeIdx = 0;
125
128
 
126
- render(cbs.showFilter || false);
129
+ showFilter = cbs.showFilter || false;
130
+ render();
127
131
 
128
132
  if (!open) {
129
133
  open = true;
@@ -163,8 +167,9 @@ export function dismissSlashMenu() {
163
167
 
164
168
  // ─── Internal ─────────────────────────────────────────────────────────────────
165
169
 
166
- /** @param {boolean} showFilter */
167
- function render(showFilter: boolean) {
170
+ /* No parameter: `showFilter` is module state now, so there is one answer to what this
171
+ menu is, and a keyboard repaint cannot disagree with the open() that created it. */
172
+ function render() {
168
173
  const rect = _anchorRect;
169
174
  if (!rect) {
170
175
  return;
@@ -203,7 +208,7 @@ function render(showFilter: boolean) {
203
208
  ? filteredItems.map(
204
209
  (cmd, i) => html`
205
210
  <sp-menu-item
206
- ?focused=${i === 0}
211
+ ?focused=${i === activeIdx}
207
212
  @click=${(e: Event) => {
208
213
  e.preventDefault();
209
214
  e.stopPropagation();
@@ -255,7 +260,7 @@ function onFilterInput(e: Event) {
255
260
  : source;
256
261
 
257
262
  activeIdx = 0;
258
- render(true);
263
+ render();
259
264
 
260
265
  // Re-focus input after re-render
261
266
  requestAnimationFrame(() => {
@@ -267,6 +272,18 @@ function onFilterInput(e: Event) {
267
272
  });
268
273
  }
269
274
 
275
+ /**
276
+ * Scroll the active row into view after a repaint.
277
+ *
278
+ * The only imperative half of the keyboard model, and it stays imperative on purpose: where a row
279
+ * sits in a scroller is a measurement, not something a template can express. Everything else —
280
+ * WHICH row is active — is now a binding, because it has to be.
281
+ */
282
+ function revealActive(): void {
283
+ const rows = getHost().querySelectorAll("sp-menu-item:not([disabled])");
284
+ rows[activeIdx]?.scrollIntoView({ block: "nearest" });
285
+ }
286
+
270
287
  /**
271
288
  * Drive the open menu with a navigation key. The canvas-iframe bridge calls this DIRECTLY (the key
272
289
  * was pressed in the iframe realm — a synthetic keydown redispatch on this document would lose the
@@ -285,18 +302,16 @@ export function handleSlashMenuKey(key: string): void {
285
302
  if (items.length === 0) {
286
303
  return;
287
304
  }
288
- items[activeIdx]?.removeAttribute("focused");
289
305
  activeIdx = (activeIdx + 1) % items.length;
290
- items[activeIdx]?.setAttribute("focused", "");
291
- items[activeIdx]?.scrollIntoView({ block: "nearest" });
306
+ render();
307
+ revealActive();
292
308
  } else if (key === "ArrowUp") {
293
309
  if (items.length === 0) {
294
310
  return;
295
311
  }
296
- items[activeIdx]?.removeAttribute("focused");
297
312
  activeIdx = (activeIdx - 1 + items.length) % items.length;
298
- items[activeIdx]?.setAttribute("focused", "");
299
- items[activeIdx]?.scrollIntoView({ block: "nearest" });
313
+ render();
314
+ revealActive();
300
315
  } else if (key === "Enter") {
301
316
  const cmd = filteredItems[activeIdx];
302
317
  if (cmd) {
@@ -488,6 +488,9 @@ export function renderFilesTemplate({
488
488
  class="file-tree"
489
489
  role="tree"
490
490
  aria-label="Project files"
491
+ @keydown=${(e: KeyboardEvent) => {
492
+ onFileTreeKeydown(e, e.currentTarget as HTMLElement);
493
+ }}
491
494
  ${ref((el) => {
492
495
  if (el) {
493
496
  afterFileTreeRender(el as HTMLElement);
@@ -814,26 +817,15 @@ async function toggleTreeDirectory(path: string): Promise<void> {
814
817
  }
815
818
  }
816
819
 
817
- /** Tree elements this module has already given a keydown listener. */
818
- const _keyboardTrees = new WeakSet<HTMLElement>();
819
-
820
- /**
821
- * Give the tree its keyboard, once per tree ELEMENT.
820
+ /*
821
+ * There is no setupTreeKeyboard here any more, and no WeakSet of trees that already have one.
822
822
  *
823
- * The panel's `afterRender` calls this after every repaint and lit re-uses the `.file-tree` element
824
- * across all of them, so the unguarded `addEventListener` it replaces accumulated one listener per
825
- * render — after ten repaints a single Down keystroke walked ten rows. The WeakSet still lets a
826
- * remounted panel's new element bind its own.
823
+ * The panel's afterRender called it after every repaint and lit re-uses the `.file-tree` element
824
+ * across all of them, so the unguarded `addEventListener` it replaced accumulated one listener per
825
+ * render — after ten repaints a single Down keystroke walked ten rows. The WeakSet made that safe;
826
+ * `@keydown` on the element the template already renders makes it impossible, because lit owns the
827
+ * binding and swaps it rather than stacking it.
827
828
  */
828
- export function setupTreeKeyboard(tree: HTMLElement) {
829
- if (_keyboardTrees.has(tree)) {
830
- return;
831
- }
832
- _keyboardTrees.add(tree);
833
- tree.addEventListener("keydown", (e: KeyboardEvent) => {
834
- onFileTreeKeydown(e, tree);
835
- });
836
- }
837
829
 
838
830
  /**
839
831
  * The tree's keyboard model: ↑↓ walk the rows, → expands, ← collapses, Enter opens.
@@ -1875,11 +1867,7 @@ export function registerFilesPanel(): void {
1875
1867
  dock: "navigator",
1876
1868
  icon: "sp-icon-folder",
1877
1869
  render: (ctx) => ctx.deps.renderFilesTemplate(),
1878
- afterRender: (ctx, host) => {
1879
- const tree = host.querySelector(".file-tree") as HTMLElement | null;
1880
- if (tree) {
1881
- ctx.deps.setupTreeKeyboard(tree);
1882
- }
1870
+ afterRender: (ctx) => {
1883
1871
  ctx.deps.registerFileTreeDnD({ renderLeftPanel: ctx.rerender });
1884
1872
  },
1885
1873
  });
@@ -17,6 +17,8 @@
17
17
  * controls that each half-remember what the others did.
18
18
  */
19
19
  import { html, render as litRender, nothing } from "lit-html";
20
+ import { guard } from "lit-html/directives/guard.js";
21
+ import { live } from "lit-html/directives/live.js";
20
22
  import { ref } from "lit-html/directives/ref.js";
21
23
  import { effect, effectScope, reactive } from "../reactivity";
22
24
  import { renderPopover, showConfirmDialog, showPromptDialog } from "../ui/layers";
@@ -193,7 +195,7 @@ function toolbarTpl(controller: GridController, panel: ActiveGridPanel) {
193
195
  size="s"
194
196
  quiet
195
197
  placeholder="Filter rows"
196
- .value=${layout?.filter ?? ""}
198
+ .value=${live(layout?.filter ?? "")}
197
199
  @input=${(e: Event) => {
198
200
  const term = (e.target as HTMLInputElement).value;
199
201
  // The filter is part of the view, so it persists with the rest of it rather than
@@ -575,7 +577,23 @@ function shellTpl(
575
577
  return html`
576
578
  <div class="jx-grid">
577
579
  ${toolbarTpl(controller, panel)}
578
- <div class="jx-grid-host" ${ref((el) => onHost(el as HTMLElement | undefined))}></div>
580
+ ${guard(
581
+ [controller.source.id],
582
+ /* Tabulator OWNS this node's children once createGridView hands it over, so a toolbar tick
583
+ must not re-render it. Today that holds by ACCIDENT: shellTpl returns the same template
584
+ shape every pass, so lit keeps the element and patches only the toolbar above it. Put
585
+ this div behind a conditional, or inside a map, and the next tick silently destroys the
586
+ whole table. The guard says the guarantee out loud.
587
+ Keyed on the SOURCE, not on nothing: an empty dependency list also suppresses the
588
+ re-commit a tab switch needs. `hostEl` lives in the panel closure, so switching tabs
589
+ builds a fresh one and relies on `ref` firing again with the new callback to hand the
590
+ node over; memoising past that leaves the new panel with a null host and no engine. */
591
+ () =>
592
+ html`<div
593
+ class="jx-grid-host"
594
+ ${ref((el) => onHost(el as HTMLElement | undefined))}
595
+ ></div>`,
596
+ )}
579
597
  </div>
580
598
  `;
581
599
  }
@@ -610,13 +628,16 @@ export function renderGridMode(surface: CanvasSurface, tab: Tab) {
610
628
  // `localStorage` is not reactive and neither is the engine, so the toolbar needs something that
611
629
  // Is: a saved-view edit bumps this and the panel's one effect re-renders like any other change.
612
630
  const local = reactive({ views: 0 });
613
- const live = controller;
631
+ /* Named `controller` everywhere else in this file; this alias exists so the closures below
632
+ capture it by a short name. NOT `live` — that is lit's directive, imported above and used
633
+ in shellTpl, and a local of the same name shadows it. */
634
+ const engine = controller;
614
635
  let hostEl: HTMLElement | null = null;
615
636
 
616
637
  const panel: ActiveGridPanel = {
617
638
  applyLayout(layout) {
618
- void live.setSort(layout?.sort ?? null);
619
- live.setGrouping(layout?.groupBy ?? null);
639
+ void engine.setSort(layout?.sort ?? null);
640
+ engine.setGrouping(layout?.groupBy ?? null);
620
641
  panel.remount();
621
642
  panel.view?.setSearch(layout?.filter ?? "");
622
643
  panel.bump();
@@ -626,7 +647,7 @@ export function renderGridMode(surface: CanvasSurface, tab: Tab) {
626
647
  },
627
648
  remount() {
628
649
  panel.view?.destroy();
629
- panel.view = hostEl ? createGridView(hostEl, live) : null;
650
+ panel.view = hostEl ? createGridView(hostEl, engine) : null;
630
651
  },
631
652
  paneId,
632
653
  scope,
@@ -0,0 +1,109 @@
1
+ /**
2
+ * The two documents `@jxsuite/studio` ships, generated for whatever base a host mounts the tree at.
3
+ *
4
+ * They used to be hand-written files that every host rewrote by hand. jx-platform's asset build
5
+ * carried three `replaceAll` calls, an exact-string surgery on the entry's script tag, and — added
6
+ * after the fact — an assertion that no relative reference had survived, because studio 2.1.0 split
7
+ * the chrome into `./styles/*.css` and the rewrite list missed it. The cloud editor shipped seven
8
+ * dead stylesheet links and rendered unstyled while the build exited 0. A rewrite list is a fact
9
+ * about a document someone else owns; generating the document removes the category.
10
+ *
11
+ * Pure: no `node:`, no DOM, no I/O. The caller supplies the base and the boot modules.
12
+ */
13
+
14
+ import {
15
+ assetUrl,
16
+ STUDIO_BUNDLE_CSS,
17
+ STUDIO_ENTRY,
18
+ STUDIO_IFRAME_ENTRY,
19
+ STUDIO_STYLESHEETS,
20
+ } from "./layout";
21
+ import type { AssetBase } from "./layout";
22
+
23
+ /** The default base: the tree served at the document's own directory, in the package's own shape. */
24
+ export const IN_PLACE: AssetBase = { mode: "nested", prefix: "./" };
25
+
26
+ export interface DocumentOptions {
27
+ /** Where the asset tree is mounted. Defaults to {@link IN_PLACE}. */
28
+ readonly base?: AssetBase | undefined;
29
+ /**
30
+ * Module urls evaluated BEFORE the studio entry — the host's PAL registration, in order.
31
+ *
32
+ * This is the declared seam. Both hosts used to obtain it by string-replacing the entry's script
33
+ * tag, and only one of them checked that the replace had matched; the other silently shipped an
34
+ * app with no platform registered, which then self-registered the dev-server adapter and fetched
35
+ * `/__studio/*` against a `views://` origin.
36
+ *
37
+ * The runtime half of the handshake is unchanged: a boot module sets `globalThis.__jxPlatform`,
38
+ * or publishes the `__jxCloud` signal for the studio entry to build the adapter from. It must do
39
+ * so SYNCHRONOUSLY — a module script with top-level await does not block a later script tag, and
40
+ * the entry reads the global as it evaluates.
41
+ */
42
+ readonly boot?: readonly string[] | undefined;
43
+ /** Document title. Defaults to "Jx Studio". */
44
+ readonly title?: string | undefined;
45
+ }
46
+
47
+ function scriptTag(url: string): string {
48
+ return `<script type="module" src="${url}"></script>`;
49
+ }
50
+
51
+ /**
52
+ * The editor document.
53
+ *
54
+ * The chrome stylesheets are LINKED rather than bundled, and the order is
55
+ * {@link STUDIO_STYLESHEETS}'s. Bundling them was tried and rejected on measurement: Bun inlines a
56
+ * woff2 below its size threshold as a base64 data uri, which would put roughly 85 KB into a
57
+ * render-blocking stylesheet, and concatenating the chrome with the vendor CSS already in
58
+ * `dist/studio.css` makes the cascade depend on module-graph traversal instead of on link order.
59
+ * Generating the links keeps the one thing that mattered — a single definition of the list.
60
+ */
61
+ export function studioShellHtml(options: DocumentOptions = {}): string {
62
+ const base = options.base ?? IN_PLACE;
63
+ const url = (path: string) => assetUrl(base, path);
64
+ const links = [STUDIO_BUNDLE_CSS, ...STUDIO_STYLESHEETS]
65
+ .map((path) => ` <link rel="stylesheet" href="${url(path)}" />`)
66
+ .join("\n");
67
+ const boot = (options.boot ?? []).map((mod) => ` ${scriptTag(mod)}`).join("\n");
68
+ return `<!doctype html>
69
+ <html lang="en">
70
+ <head>
71
+ <meta charset="utf-8" />
72
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
73
+ <title>${options.title ?? "Jx Studio"}</title>
74
+ ${links}
75
+ </head>
76
+ <body>
77
+ ${boot ? `${boot}\n` : ""} ${scriptTag(url(STUDIO_ENTRY))}
78
+ </body>
79
+ </html>
80
+ `;
81
+ }
82
+
83
+ /**
84
+ * The canvas iframe document.
85
+ *
86
+ * Its `<style>` block is hand-authored and stays that way — it establishes the fixed-size query
87
+ * container the runtime transposes viewport units against, and the preview-mode overrides, both of
88
+ * which have to apply before the iframe's first render. Only the entry url is generated.
89
+ *
90
+ * The reference it replaces is a dynamic `import()` inside a template literal with a cache-buster,
91
+ * which is precisely the shape jx-platform's `(?:src|href)="\./…"` residual-ref guard could not
92
+ * see.
93
+ *
94
+ * @param html The package's own `canvas.html`, read by the caller.
95
+ * @param base Where the asset tree is mounted. Defaults to {@link IN_PLACE}.
96
+ * @returns The document with its entry url rebased.
97
+ * @throws {Error} When the entry reference is absent — a silent miss here is a canvas that 404s at
98
+ * boot.
99
+ */
100
+ export function canvasShellHtml(html: string, base: AssetBase = IN_PLACE): string {
101
+ const from = `./${STUDIO_IFRAME_ENTRY}`;
102
+ if (!html.includes(from)) {
103
+ throw new Error(
104
+ `canvas.html does not reference ${from}. The canvas entry's path is a contract ` +
105
+ `(studio.md 11.1); if it moved, STUDIO_IFRAME_ENTRY moved with it.`,
106
+ );
107
+ }
108
+ return html.replaceAll(from, assetUrl(base, STUDIO_IFRAME_ENTRY));
109
+ }