@jxsuite/studio 2.2.1 → 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 +12 -5
  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
@@ -0,0 +1,211 @@
1
+ /**
2
+ * What `@jxsuite/studio` ships, where it goes, and how a host addresses it.
3
+ *
4
+ * The package's internal cross-references are already correct and self-locating — `dist/studio.css`
5
+ * reaches `./codicon.ttf`, `styles/tokens.css` reaches `../fonts/*.woff2`, the entry reaches
6
+ * `./chunks/*` and `./workers/*`. What it never shipped was a STATEMENT that they are, so four
7
+ * hosts re-derived the file list by hand, two of them flattened the tree, that broke the
8
+ * references, and they rewrote the shipped HTML to compensate. This module is the statement.
9
+ *
10
+ * **Pure by contract.** No `node:`, no DOM, nothing beyond the language — so a wrangler build, a
11
+ * Vite plugin, a Deno host or a Nix derivation can read it. `./hosting` (stage.ts) is the `node:fs`
12
+ * convenience the two Bun consumers share, and it is the only entry that binds a runtime. The build
13
+ * also emits `dist/manifest.json` from {@link STUDIO_ASSETS} for a host that cannot import
14
+ * TypeScript at all.
15
+ *
16
+ * **It names no adapter, no backend and no transport.** `@jxsuite/studio` may contain PAL adapters
17
+ * — `platforms/devserver.ts` and `platforms/cloud.ts` both ship, the latter because it owns the
18
+ * collab client's `Y.Doc` and a second bundled `yjs` breaks cross-module `instanceof` — but it must
19
+ * not depend on a backend PACKAGE. `scripts/check-dep-rules.ts` cannot catch a violation (it
20
+ * forbids only core-to-extension edges, and server and studio are both core), so
21
+ * `scripts/check-studio-package.ts` does.
22
+ */
23
+
24
+ /** What a manifest entry is, which decides how a host must treat it. */
25
+ export type StudioAssetKind =
26
+ | "document"
27
+ | "entry"
28
+ | "style"
29
+ | "chunk"
30
+ | "worker"
31
+ | "font"
32
+ | "asset";
33
+
34
+ export interface StudioAsset {
35
+ /** POSIX, relative to the package root. */
36
+ readonly path: string;
37
+ readonly kind: StudioAssetKind;
38
+ /** A DIRECTORY copied wholesale; the names inside it are part of the contract. */
39
+ readonly dir: boolean;
40
+ /** Absence is a boot failure, not a degradation. */
41
+ readonly required: boolean;
42
+ /** Printed verbatim when it is missing, so the error explains itself. */
43
+ readonly why: string;
44
+ }
45
+
46
+ /**
47
+ * Everything a host serves.
48
+ *
49
+ * `required: false` means a host may leave it out and get a working editor with something switched
50
+ * off; everything else is load-bearing. The `why` is not decoration — it is what a staging error
51
+ * prints, and the difference between "a file is missing" and "the code view will silently have no
52
+ * schema validation".
53
+ */
54
+ export const STUDIO_ASSETS: readonly StudioAsset[] = [
55
+ {
56
+ dir: false,
57
+ kind: "document",
58
+ path: "index.html",
59
+ required: true,
60
+ why: "the editor document. Generated from studioShellHtml(); a host serving the shell at its own path should render its own rather than copy this one.",
61
+ },
62
+ {
63
+ dir: false,
64
+ kind: "document",
65
+ path: "canvas.html",
66
+ required: true,
67
+ why: "the canvas iframe document. Without it every canvas frame 404s at boot.",
68
+ },
69
+ {
70
+ dir: false,
71
+ kind: "entry",
72
+ path: "dist/studio.js",
73
+ required: true,
74
+ why: "the editor bundle. Its path is a contract (studio.md 11.1) and is never hashed, because it anchors every other url the bundle resolves.",
75
+ },
76
+ {
77
+ dir: false,
78
+ kind: "entry",
79
+ path: "dist/iframe-entry.js",
80
+ required: true,
81
+ why: "the canvas bundle, loaded by canvas.html. Same unhashed-path contract.",
82
+ },
83
+ {
84
+ dir: false,
85
+ kind: "style",
86
+ path: "dist/studio.css",
87
+ required: true,
88
+ why: "the bundle's own stylesheet — Monaco's tokens and Tabulator's theme. Distinct from styles/, which is hand-authored.",
89
+ },
90
+ {
91
+ dir: false,
92
+ kind: "asset",
93
+ path: "dist/codicon.ttf",
94
+ required: true,
95
+ why: "Monaco's icon font, referenced by dist/studio.css and by three chunk stylesheets. It shipped in no tarball and no copy list for months, so every host drew tofu where Monaco draws icons.",
96
+ },
97
+ {
98
+ dir: false,
99
+ kind: "asset",
100
+ path: "dist/manifest.json",
101
+ required: false,
102
+ why: "this manifest as data, for a host that cannot import TypeScript — a Nix derivation, a shell script, a build in another language. Not required: a host that CAN import it has the same facts with types on them.",
103
+ },
104
+ {
105
+ dir: true,
106
+ kind: "chunk",
107
+ path: "dist/chunks",
108
+ required: true,
109
+ why: "the split chunks — Monaco, yjs, ajv and every on-demand import. The entry reaches them by url relative to itself, so the directory ships wholesale with its emitted names intact. A bundle that cannot find them dies at boot with a bare module-resolution error.",
110
+ },
111
+ {
112
+ dir: true,
113
+ kind: "worker",
114
+ path: "dist/workers",
115
+ required: true,
116
+ why: "Monaco's pre-bundled web workers. Missing, they do not 404 loudly: the code view simply has no JSON language service, so no schema validation, no completion and no hover, with nothing in the console.",
117
+ },
118
+ {
119
+ dir: true,
120
+ kind: "style",
121
+ path: "styles",
122
+ required: true,
123
+ why: "the hand-authored chrome stylesheet, linked by the document in cascade order. No bundler sees it. Missing, the editor boots with no tokens, no grid and no panel chrome.",
124
+ },
125
+ {
126
+ dir: true,
127
+ kind: "font",
128
+ path: "fonts",
129
+ required: true,
130
+ why: "the vendored JetBrains Mono faces, reached from styles/tokens.css by ../fonts/. They stay a directory rather than being bundled: Bun inlines a woff2 under its size threshold as base64, which would put about 85 KB into a render-blocking stylesheet.",
131
+ },
132
+ ];
133
+
134
+ /** Never staged and never published, whatever the entries above would otherwise match. */
135
+ export const STUDIO_ASSET_EXCLUDE: readonly string[] = ["**/*.map"];
136
+
137
+ /**
138
+ * Published beyond the served tree: the source graph the `exports` map points at, and the data it
139
+ * reads. `data/` is here because six modules import it — `src/studio.ts` among them — so without it
140
+ * the package's own `.` export does not resolve from a tarball.
141
+ */
142
+ export const PUBLISHED_EXTRAS: readonly string[] = ["src", "data"];
143
+
144
+ export const STUDIO_SHELL = "index.html";
145
+ export const STUDIO_CANVAS = "canvas.html";
146
+ export const STUDIO_ENTRY = "dist/studio.js";
147
+ export const STUDIO_IFRAME_ENTRY = "dist/iframe-entry.js";
148
+ export const STUDIO_BUNDLE_CSS = "dist/studio.css";
149
+
150
+ /** Monaco's workers, by the names scripts/build-workers.ts emits and monaco-setup.ts asks for. */
151
+ export const STUDIO_WORKERS: readonly string[] = [
152
+ "editor.worker.js",
153
+ "json.worker.js",
154
+ "ts.worker.js",
155
+ ];
156
+
157
+ /**
158
+ * The chrome stylesheets, IN CASCADE ORDER.
159
+ *
160
+ * Order is behaviour, not presentation: `forced-colors.css` is last because it redraws what Windows
161
+ * High Contrast deletes. This list is what the document's link tags are generated from, so a
162
+ * stylesheet added to `styles/` and not added here is simply never loaded — which is the 2.1.0
163
+ * outage exactly, where the cloud shipped seven dead link tags and the build exited 0.
164
+ * `scripts/check-studio-package.ts` holds it equal to `styles/*.css` on disk.
165
+ */
166
+ export const STUDIO_STYLESHEETS: readonly string[] = [
167
+ "styles/tokens.css",
168
+ "styles/shell.css",
169
+ "styles/canvas.css",
170
+ "styles/panels.css",
171
+ "styles/inspector.css",
172
+ "styles/overlays.css",
173
+ "styles/forced-colors.css",
174
+ ];
175
+
176
+ /** Bump when a host-visible export changes shape. A consumer may assert against it. */
177
+ export const STUDIO_HOST_API = 1;
178
+
179
+ /**
180
+ * How a host lays the tree out.
181
+ *
182
+ * `nested` keeps the package's own shape. `flat` hoists the contents of `dist/` up one level, which
183
+ * is what jx-platform's `public/` has always done.
184
+ */
185
+ export type StudioLayoutMode = "nested" | "flat";
186
+
187
+ export interface AssetBase {
188
+ /** URL prefix the tree is served under, as the BROWSER sees it. Non-empty values end in "/". */
189
+ readonly prefix: string;
190
+ readonly mode: StudioLayoutMode;
191
+ }
192
+
193
+ /**
194
+ * Map a package-relative path to the url a host serving under `base` answers.
195
+ *
196
+ * `flat` strips exactly ONE leading `dist/` segment and nothing else, and that single rule is what
197
+ * makes flattening a contract rather than a rewrite. Every reference that lives inside `dist/` is
198
+ * dist-relative — the entry reaches `./chunks/`, `dist/studio.css` reaches `./codicon.ttf`, a chunk
199
+ * stylesheet reaches `../codicon.ttf` — so stripping one segment uniformly moves all of them
200
+ * together and none of them breaks. `styles/` and `fonts/` are untouched in both modes, which is
201
+ * why `tokens.css`'s `url("../fonts/…")` holds either way.
202
+ *
203
+ * @param base Where the tree is mounted.
204
+ * @param packagePath A path from {@link STUDIO_ASSETS}, or a file inside one of its directories.
205
+ * @returns The url, with `base.prefix` applied.
206
+ */
207
+ export function assetUrl(base: AssetBase, packagePath: string): string {
208
+ const path = packagePath.replace(/^\.?\//, "");
209
+ const laid = base.mode === "flat" ? path.replace(/^dist\//, "") : path;
210
+ return `${base.prefix}${laid}`;
211
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Copy the studio tree somewhere a host can serve it.
3
+ *
4
+ * CONVENIENCE, not contract. Everything a subscriber strictly needs is in `./layout` (the manifest
5
+ * and {@link assetUrl}) and `./document` (the two generators), both of which are pure and run
6
+ * anywhere. This module exists so the two Bun consumers — the desktop's asset staging and
7
+ * jx-platform's `public/` build — do not each write the same copier, and it is the ONLY module
8
+ * under `src/` that imports `node:`. A host in another runtime reads the manifest and moves the
9
+ * bytes however it likes.
10
+ *
11
+ * @docs extending/embedding/hosting
12
+ */
13
+
14
+ import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
15
+ import { existsSync } from "node:fs";
16
+ import { dirname, join, resolve } from "node:path";
17
+ import { Glob } from "bun";
18
+ import { STUDIO_ASSETS, STUDIO_CANVAS, STUDIO_SHELL } from "./layout";
19
+ import type { AssetBase, StudioAsset, StudioAssetKind, StudioLayoutMode } from "./layout";
20
+ import { canvasShellHtml } from "./document";
21
+ import type { DocumentOptions } from "./document";
22
+
23
+ /** The installed package root, resolved off this module rather than off a caller's cwd. */
24
+ export const STUDIO_PACKAGE_DIR = resolve(import.meta.dir, "..", "..");
25
+
26
+ export interface StageOptions {
27
+ /** Package root to read from — an installed copy, or a `--link` checkout. */
28
+ readonly from?: string | undefined;
29
+ /** Kinds to leave out. The cloud passes `["document"]`; see the note on {@link stageStudioAssets}. */
30
+ readonly exclude?: readonly StudioAssetKind[] | undefined;
31
+ /** Copy `*.map` too. Default false: the chunk maps alone are about 24 MB. */
32
+ readonly sourceMaps?: boolean | undefined;
33
+ /**
34
+ * Remove the manifest's own paths under `destDir` first. Default true.
35
+ *
36
+ * Only those paths, never the whole directory. `packages/desktop/scripts/pre-build.ts` writes its
37
+ * launcher's PAL-init bundle to `assets/studio/dist/init.js` BEFORE staging, so a blanket wipe
38
+ * deletes it and the packaged app boots with no platform registered. Not cleaning at all is
39
+ * equally wrong: content-hashed chunks would accumulate in a staged tree forever.
40
+ */
41
+ readonly clean?: boolean | undefined;
42
+ /** Layout to report in {@link StageResult.base}. Default `"nested"`. */
43
+ readonly layout?: StudioLayoutMode | undefined;
44
+ /** Url prefix to report in {@link StageResult.base}. Default `"./"`. */
45
+ readonly prefix?: string | undefined;
46
+ }
47
+
48
+ export interface StageResult {
49
+ /**
50
+ * The base the tree was staged at — hand it straight to {@link studioShellHtml}, so the stager and
51
+ * the document cannot disagree about where the files went.
52
+ */
53
+ readonly base: AssetBase;
54
+ /** Package-relative paths written, in manifest order. */
55
+ readonly written: readonly string[];
56
+ readonly bytes: number;
57
+ }
58
+
59
+ /** Manifest entries absent from `root`. Empty means the tree is complete. */
60
+ export function missingStudioAssets(root: string = STUDIO_PACKAGE_DIR): readonly StudioAsset[] {
61
+ return STUDIO_ASSETS.filter((a) => a.required && !existsSync(join(root, a.path)));
62
+ }
63
+
64
+ /** Where a package-relative path lands under `destDir`, honouring the layout. */
65
+ function destOf(destDir: string, path: string, layout: StudioLayoutMode): string {
66
+ return join(destDir, layout === "flat" ? path.replace(/^dist\//, "") : path);
67
+ }
68
+
69
+ /**
70
+ * Copy the tree into `destDir`.
71
+ *
72
+ * @throws {Error} Naming the entry AND its `why` when a required one is missing — a staging failure
73
+ * should say what the reader will lose, not just which file was absent.
74
+ */
75
+ export async function stageStudioAssets(
76
+ destDir: string,
77
+ options: StageOptions = {},
78
+ ): Promise<StageResult> {
79
+ const from = options.from ?? STUDIO_PACKAGE_DIR;
80
+ const layout = options.layout ?? "nested";
81
+ const exclude = new Set(options.exclude);
82
+ const entries = STUDIO_ASSETS.filter((a) => !exclude.has(a.kind));
83
+
84
+ const absent = entries.filter((a) => a.required && !existsSync(join(from, a.path)));
85
+ if (absent.length > 0) {
86
+ const listed = absent.map((a) => ` ${a.path} — ${a.why}`).join("\n");
87
+ throw new Error(
88
+ `@jxsuite/studio is missing ${absent.length} required asset(s) at ${from}:\n${listed}\n` +
89
+ `Run \`bun run build\` in packages/studio, or check the package's published files list.`,
90
+ );
91
+ }
92
+
93
+ if (options.clean !== false) {
94
+ for (const a of entries) {
95
+ await rm(destOf(destDir, a.path, layout), { force: true, recursive: true });
96
+ }
97
+ }
98
+
99
+ const written: string[] = [];
100
+ let bytes = 0;
101
+ const skipMap = (p: string) => !options.sourceMaps && p.endsWith(".map");
102
+
103
+ for (const a of entries) {
104
+ const src = join(from, a.path);
105
+ if (!existsSync(src)) {
106
+ continue;
107
+ }
108
+ const dest = destOf(destDir, a.path, layout);
109
+ if (a.dir) {
110
+ await mkdir(dest, { recursive: true });
111
+ for (const rel of new Glob("**/*").scanSync(src)) {
112
+ const posix = rel.replaceAll("\\", "/");
113
+ if (skipMap(posix)) {
114
+ continue;
115
+ }
116
+ const file = join(src, rel);
117
+ const to = join(dest, rel);
118
+ await mkdir(dirname(to), { recursive: true });
119
+ await cp(file, to);
120
+ written.push(`${a.path}/${posix}`);
121
+ const info = await stat(file);
122
+ bytes += info.size;
123
+ }
124
+ } else {
125
+ if (skipMap(a.path)) {
126
+ continue;
127
+ }
128
+ await mkdir(dirname(dest), { recursive: true });
129
+ await cp(src, dest);
130
+ written.push(a.path);
131
+ const info = await stat(src);
132
+ bytes += info.size;
133
+ }
134
+ }
135
+
136
+ return { base: { mode: layout, prefix: options.prefix ?? "./" }, bytes, written };
137
+ }
138
+
139
+ export interface ReadDocumentOptions extends DocumentOptions {
140
+ /** Package root to read `canvas.html` from. */
141
+ readonly from?: string | undefined;
142
+ }
143
+
144
+ /* There is no shellDocument() here. The editor document needs no file read, so it would be a
145
+ re-export of studioShellHtml under a second name — a host imports `./hosting/document` for it,
146
+ which is also the entry that stays pure. */
147
+
148
+ /** The canvas document, read from the package and rebased. */
149
+ export async function canvasDocument(options: ReadDocumentOptions = {}): Promise<string> {
150
+ const from = options.from ?? STUDIO_PACKAGE_DIR;
151
+ const html = await readFile(join(from, STUDIO_CANVAS), "utf8");
152
+ return canvasShellHtml(html, options.base);
153
+ }
154
+
155
+ /**
156
+ * Write `dist/manifest.json` — {@link STUDIO_ASSETS} as data, for a host that cannot import
157
+ * TypeScript. Called by the build; gated against the manifest by check-studio-dist.ts.
158
+ */
159
+ export async function writeAssetManifest(root: string = STUDIO_PACKAGE_DIR): Promise<void> {
160
+ const out = join(root, "dist", "manifest.json");
161
+ await mkdir(dirname(out), { recursive: true });
162
+ await writeFile(
163
+ out,
164
+ `${JSON.stringify({ assets: STUDIO_ASSETS, shell: STUDIO_SHELL }, null, 2)}\n`,
165
+ );
166
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "//": "The hosting contract is compiled under a CONSUMER's flags, not this repo's.",
3
+ "//why": "These three modules are published for other repositories to import, and jx-platform — the first real subscriber — sets exactOptionalPropertyTypes, verbatimModuleSyntax and isolatedModules, none of which the jx root tsconfig sets. Two type errors were found by consuming the API rather than by building it: `from: string | undefined` did not satisfy `from?: string` under exactOptionalPropertyTypes. Compiling this directory under the stricter set means the next one is found here instead of after a release. `types: []` and a bare lib are the other half: layout.ts and document.ts must not depend on node or DOM globals, because a Worker build, a Vite plugin or a Deno host has to be able to read them.",
4
+ "compilerOptions": {
5
+ "strict": true,
6
+ "exactOptionalPropertyTypes": true,
7
+ "verbatimModuleSyntax": true,
8
+ "isolatedModules": true,
9
+ "noEmit": true,
10
+ "skipLibCheck": true,
11
+ "module": "preserve",
12
+ "moduleResolution": "bundler",
13
+ "target": "ES2023",
14
+ "lib": ["ES2023"],
15
+ "types": ["bun"]
16
+ },
17
+ "include": ["layout.ts", "document.ts"]
18
+ }
@@ -32,6 +32,7 @@
32
32
  */
33
33
 
34
34
  import { html, nothing } from "lit-html";
35
+ import { repeat } from "lit-html/directives/repeat.js";
35
36
  import type { TemplateResult } from "lit-html";
36
37
  import { ref } from "lit-html/directives/ref.js";
37
38
  import type { Message, ToolCallRecord } from "@jxsuite/ai/chat-state";
@@ -443,21 +444,31 @@ export function renderMessageList(opts: MessageListOptions): TemplateResult {
443
444
  `
444
445
  : nothing
445
446
  }
446
- ${messages.map((msg, i) => {
447
- if (msg.role === "user") {
448
- return renderUserMessage(msg);
449
- }
450
- if (msg.role === "tool") {
451
- return renderToolMessage(msg);
452
- }
453
- if (msg.role === "assistant") {
454
- if (status === "streaming" && i === lastIdx) {
455
- return renderStreamingTail(msg);
447
+ ${repeat(
448
+ messages,
449
+ /* Keyed on msg.id, for two reasons a reader can see. The last assistant row swaps between
450
+ renderStreamingTail and renderAssistantMessage the moment a stream completes, so an
451
+ unkeyed list tears down and rebuilds the longest node in the transcript every time one
452
+ finishes. And an assistant row holds the reader's OWN open/closed state on its
453
+ ai-msg-changes details element, which position-based reuse hands to a different
454
+ message. */
455
+ (msg) => msg.id,
456
+ (msg, i) => {
457
+ if (msg.role === "user") {
458
+ return renderUserMessage(msg);
456
459
  }
457
- return renderAssistantMessage(msg, opts.onRestore);
458
- }
459
- return nothing;
460
- })}
460
+ if (msg.role === "tool") {
461
+ return renderToolMessage(msg);
462
+ }
463
+ if (msg.role === "assistant") {
464
+ if (status === "streaming" && i === lastIdx) {
465
+ return renderStreamingTail(msg);
466
+ }
467
+ return renderAssistantMessage(msg, opts.onRestore);
468
+ }
469
+ return nothing;
470
+ },
471
+ )}
461
472
  ${
462
473
  status !== "streaming" && opts.error
463
474
  ? html`
@@ -13,7 +13,7 @@ import { repeat } from "lit-html/directives/repeat.js";
13
13
  import { getPlatform } from "../platform";
14
14
  import { now } from "../services/clock";
15
15
  import { formatForPath } from "../format/format-host";
16
- import { projectState } from "../store";
16
+ import { projectState, renderOnly } from "../store";
17
17
  import { activeTab } from "../workspace/workspace";
18
18
  import type { Tab } from "../tabs/tab";
19
19
  import { shell } from "../shell";
@@ -245,6 +245,16 @@ async function doPull() {
245
245
  */
246
246
  let _pollTimer = null as ReturnType<typeof setInterval> | null;
247
247
 
248
+ /**
249
+ * Whether the commit button's split menu is showing.
250
+ *
251
+ * State rather than a `hidden` attribute toggled on a node found by selector. The two were fighting
252
+ * by construction: the template declared the menu `hidden` unconditionally, so lit had committed
253
+ * that attribute, and every repaint of the panel — the 30-second git poll among them — would leave
254
+ * whatever the handler had last done to it standing, with no way for the template to take it back.
255
+ */
256
+ let _splitMenuOpen = false;
257
+
248
258
  async function fetchGitLog() {
249
259
  const plat = getPlatform();
250
260
  try {
@@ -583,25 +593,18 @@ export function renderGitPanel(ctx: {
583
593
  <sp-action-button
584
594
  class="git-split-trigger"
585
595
  size="s"
586
- @click=${(e: Event) => {
587
- const menu = (
588
- (e.currentTarget as HTMLElement).parentElement as HTMLElement
589
- ).querySelector(".git-split-menu");
590
- if (menu) {
591
- menu.toggleAttribute("hidden");
592
- }
596
+ @click=${() => {
597
+ _splitMenuOpen = !_splitMenuOpen;
598
+ renderOnly("leftPanel");
593
599
  }}
594
600
  >
595
601
  <sp-icon-chevron-down slot="icon" size="xs"></sp-icon-chevron-down>
596
602
  </sp-action-button>
597
- <div class="git-split-menu" hidden>
603
+ <div class="git-split-menu" ?hidden=${!_splitMenuOpen}>
598
604
  <button
599
605
  class="git-split-menu-item"
600
- @click=${(e: Event) => {
601
- ((e.currentTarget as HTMLElement).parentElement as HTMLElement).setAttribute(
602
- "hidden",
603
- "",
604
- );
606
+ @click=${() => {
607
+ _splitMenuOpen = false;
605
608
  void doCommit();
606
609
  }}
607
610
  >
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import { html, nothing } from "lit-html";
10
+ import { createRef, ref } from "lit-html/directives/ref.js";
10
11
  import { renderFieldRow } from "../ui/field-row";
11
12
  import { spTextArea, spTextField } from "../ui/field-input";
12
13
  import { renderMediaPicker } from "../ui/media-picker";
@@ -28,6 +29,18 @@ import type { JxHeadEntry, JxMutableNode } from "@jxsuite/schema/types";
28
29
  import type { Tab } from "../tabs/tab";
29
30
  import type { TemplateResult } from "lit-html";
30
31
 
32
+ /**
33
+ * The add-a-tag draft fields.
34
+ *
35
+ * Uncontrolled by design — the draft is not state anything else reads, and rendering it back on
36
+ * every keystroke would fight the typist. Held as refs rather than re-found by selector: the row is
37
+ * rebuilt on every Head repaint, so a node resolved once is detached by the time it is used, and
38
+ * with a second pane open a document-wide query can return the other pane's field.
39
+ */
40
+ const _addTag = createRef<HTMLInputElement>();
41
+ const _addAttr = createRef<HTMLInputElement>();
42
+ const _addVal = createRef<HTMLInputElement>();
43
+
31
44
  interface MetaField {
32
45
  label: string;
33
46
  attr: "name" | "property";
@@ -982,7 +995,7 @@ export function renderHeadTemplate({
982
995
 
983
996
  <!-- Add custom tag form -->
984
997
  <div class="head-add-form">
985
- <sp-picker size="s" label="Tag" class="head-add-tag" value="meta">
998
+ <sp-picker size="s" label="Tag" class="head-add-tag" ${ref(_addTag)}>
986
999
  <sp-menu-item value="meta">meta</sp-menu-item>
987
1000
  <sp-menu-item value="link">link</sp-menu-item>
988
1001
  <sp-menu-item value="script">script</sp-menu-item>
@@ -991,28 +1004,33 @@ export function renderHeadTemplate({
991
1004
  placeholder="Attribute (e.g. name)"
992
1005
  size="s"
993
1006
  class="head-add-attr"
1007
+ ${ref(_addAttr)}
1008
+ ></sp-textfield>
1009
+ <sp-textfield
1010
+ placeholder="Value"
1011
+ size="s"
1012
+ class="head-add-val"
1013
+ ${ref(_addVal)}
994
1014
  ></sp-textfield>
995
- <sp-textfield placeholder="Value" size="s" class="head-add-val"></sp-textfield>
996
1015
  <sp-action-button
997
1016
  quiet
998
1017
  size="xs"
999
1018
  title="Add tag"
1000
- @click=${(e: Event) => {
1001
- const form = (e.target as HTMLElement).closest(".head-add-form");
1002
- const tagPicker = form?.querySelector(".head-add-tag") as HTMLInputElement | null;
1003
- const attrField = form?.querySelector(".head-add-attr") as HTMLInputElement | null;
1004
- const valField = form?.querySelector(".head-add-val") as HTMLInputElement | null;
1005
- const tagName = tagPicker?.value || "meta";
1006
- const attrKey = attrField?.value?.trim();
1007
- const attrVal = valField?.value?.trim();
1019
+ @click=${() => {
1020
+ /* Handles, not a closest() walk back up to a form this same template drew three lines
1021
+ above. The fields are uncontrolled on purpose — the draft is not state anything
1022
+ else reads — so a ref is exactly what reading and clearing them needs. */
1023
+ const tagName = _addTag.value?.value || "meta";
1024
+ const attrKey = _addAttr.value?.value?.trim();
1025
+ const attrVal = _addVal.value?.value?.trim();
1008
1026
  if (!attrKey || !attrVal) {
1009
1027
  return;
1010
1028
  }
1011
- if (attrField) {
1012
- attrField.value = "";
1029
+ if (_addAttr.value) {
1030
+ _addAttr.value.value = "";
1013
1031
  }
1014
- if (valField) {
1015
- valField.value = "";
1032
+ if (_addVal.value) {
1033
+ _addVal.value.value = "";
1016
1034
  }
1017
1035
 
1018
1036
  const entry: JxHeadEntry = { attributes: {}, tagName };
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { html, nothing } from "lit-html";
12
+ import { createRef, ref } from "lit-html/directives/ref.js";
12
13
  import { componentRegistry } from "../files/components";
13
14
  import {
14
15
  disableElement,
@@ -30,6 +31,10 @@ import type { ComponentEntry } from "../files/components";
30
31
  import type { ElementsEntry } from "../files/elements";
31
32
  import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
32
33
 
34
+ /** The add-an-import draft fields — uncontrolled, held rather than re-found. See head-panel.ts. */
35
+ const _addName = createRef<HTMLInputElement>();
36
+ const _addPath = createRef<HTMLInputElement>();
37
+
33
38
  interface ImportsContext {
34
39
  renderLeftPanel: () => void;
35
40
  documentPath: string | null;
@@ -142,23 +147,37 @@ function renderSiteLevelImports(renderLeftPanel: () => void) {
142
147
  })
143
148
  }
144
149
  <div class="import-add-form">
145
- <sp-textfield placeholder="Name" size="s" class="import-add-name"></sp-textfield>
146
- <sp-textfield placeholder="Path" size="s" class="import-add-path"></sp-textfield>
150
+ <sp-textfield
151
+ placeholder="Name"
152
+ size="s"
153
+ class="import-add-name"
154
+ ${ref(_addName)}
155
+ ></sp-textfield>
156
+ <sp-textfield
157
+ placeholder="Path"
158
+ size="s"
159
+ class="import-add-path"
160
+ ${ref(_addPath)}
161
+ ></sp-textfield>
147
162
  <sp-action-button
148
163
  quiet
149
164
  size="xs"
150
165
  title="Add import"
151
- @click=${async (e: Event) => {
152
- const form = (e.target as HTMLElement).closest(".import-add-form");
153
- const nameField = form?.querySelector(".import-add-name") as HTMLInputElement;
154
- const pathField = form?.querySelector(".import-add-path") as HTMLInputElement;
155
- const name = nameField?.value?.trim();
156
- const path = pathField?.value?.trim();
166
+ @click=${async () => {
167
+ /* Handles rather than a closest() walk back to a form drawn six lines above. The
168
+ fields are an uncontrolled draft, so a ref is what reading and clearing them
169
+ wants — and the panel is rebuilt on every repaint, so a node found once is gone. */
170
+ const name = _addName.value?.value?.trim();
171
+ const path = _addPath.value?.value?.trim();
157
172
  if (!name || !path) {
158
173
  return;
159
174
  }
160
- nameField.value = "";
161
- pathField.value = "";
175
+ if (_addName.value) {
176
+ _addName.value.value = "";
177
+ }
178
+ if (_addPath.value) {
179
+ _addPath.value.value = "";
180
+ }
162
181
  const updated = { ...siteImports, [name]: path };
163
182
  await updateSiteConfig({ imports: updated });
164
183
  renderLeftPanel();
@@ -326,10 +345,10 @@ function renderDocumentLevelImports({
326
345
 
327
346
  const packageGroups = groupByPackage();
328
347
 
329
- /** @param {string} ref */
330
- const removeRef = (ref: string) => {
348
+ /** @param {string} elementRef The element reference to drop. Not `ref` — that is lit's directive. */
349
+ const removeRef = (elementRef: string) => {
331
350
  applyMutation((doc: JxMutableNode) => {
332
- doc.$elements = removeElementRef((doc.$elements ?? []) as ElementsEntry[], ref) as (
351
+ doc.$elements = removeElementRef((doc.$elements ?? []) as ElementsEntry[], elementRef) as (
333
352
  | string
334
353
  | JxElement
335
354
  )[];