@jxsuite/studio 10.5.0 → 10.6.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 (279) hide show
  1. package/data/locales/en/elements.json +434 -0
  2. package/data/webdata.json +116 -139
  3. package/dist/chunks/{2020-7sx2vexa.js → 2020-rqm7qrje.js} +3 -3
  4. package/dist/chunks/adapter-cyy8vbcx.js +4 -0
  5. package/dist/chunks/add-new-flow-8cgtejv2.js +4 -0
  6. package/dist/chunks/{cf-account-picker-ajh60k54.js → cf-account-picker-v3qd2dhw.js} +3 -3
  7. package/dist/chunks/cf-settings-1fnw874z.js +4 -0
  8. package/dist/chunks/{color-area-gbcftzh3.js → color-area-frd9y9q6.js} +2 -2
  9. package/dist/chunks/{color-field-85136598.js → color-field-rebpbe33.js} +3 -3
  10. package/dist/chunks/{color-slider-w1s3ndd8.js → color-slider-6bzmk3s7.js} +2 -2
  11. package/dist/chunks/color-t2bnnj8y.js +4 -0
  12. package/dist/chunks/{combobox-dk98wc5x.js → combobox-c49x7gf1.js} +2 -2
  13. package/dist/chunks/components-y8yhdd7d.js +4 -0
  14. package/dist/chunks/content-source-9e7qvmsf.js +4 -0
  15. package/dist/chunks/convert-to-repeater-pd2w4z21.js +4 -0
  16. package/dist/chunks/editor-mb9jzdrw.js +4 -0
  17. package/dist/chunks/entry-commands-at0wnx3f.js +4 -0
  18. package/dist/chunks/extension-sections-4z9qq0gh.js +4 -0
  19. package/dist/chunks/file-ops-dtdvjmza.js +4 -0
  20. package/dist/chunks/files-8pf70854.js +4 -0
  21. package/dist/chunks/format-host-fd3m2y2a.js +4 -0
  22. package/dist/chunks/fs-events-hrk29vcv.js +4 -0
  23. package/dist/chunks/grid-open-bh74v82z.js +4 -0
  24. package/dist/chunks/{grid-view-0v4hjspb.js → grid-view-a51mgzmq.js} +10 -10
  25. package/dist/chunks/{icons-j9eksgew.js → icons-qp8fn1k8.js} +2 -2
  26. package/dist/chunks/{iframe-entry-4zqtn3ry.js → iframe-entry-mpwe5pwk.js} +4 -4
  27. package/dist/chunks/{iframe-entry-2x9bn5ng.js → iframe-entry-msd43x9h.js} +4 -4
  28. package/dist/chunks/iframe-entry-rt8jz5t8.js +5 -0
  29. package/dist/chunks/{iframe-entry-54kwwhmg.js → iframe-entry-v40cr813.js} +2 -2
  30. package/dist/chunks/iframe-host-q0s0wwy3.js +4 -0
  31. package/dist/chunks/{image-bake-m8gzaan9.js → image-bake-q9ajdjsj.js} +3 -3
  32. package/dist/chunks/{image-editor-0afyws7c.js → image-editor-ke9std9b.js} +2 -2
  33. package/dist/chunks/index-57ang7ma.js +4 -0
  34. package/dist/chunks/index-eytr7av4.js +17 -0
  35. package/dist/chunks/{index-2q88g57p.js → index-x495reff.js} +3 -3
  36. package/dist/chunks/{javascript-5sxtr1g7.js → javascript-qe34bvaz.js} +3 -3
  37. package/dist/chunks/{jsonMode-m6sf47hq.js → jsonMode-m8rseh31.js} +8 -8
  38. package/dist/chunks/jx-validate-bbd1mary.js +4 -0
  39. package/dist/chunks/layers-panel-wcjtdqsa.js +4 -0
  40. package/dist/chunks/layers-yscpys6q.js +4 -0
  41. package/dist/chunks/{listbox-k5p41049.js → listbox-tktzq5xj.js} +2 -2
  42. package/dist/chunks/lit-html-qm5wbhqz.js +4 -0
  43. package/dist/chunks/media-library-2hfff88g.js +4 -0
  44. package/dist/chunks/media-open-4pb05cne.js +4 -0
  45. package/dist/chunks/media-picker-bzktmmwr.js +4 -0
  46. package/dist/chunks/{menu-26nez152.js → menu-xc3cbgsd.js} +2 -2
  47. package/dist/chunks/{monaco-binding-1s2b8snq.js → monaco-binding-7t38arh9.js} +3 -3
  48. package/dist/chunks/monaco-buffer-3vz94c9f.js +4 -0
  49. package/dist/chunks/monaco-setup-fstdew02.js +4 -0
  50. package/dist/chunks/notify-gbdcckrh.js +4 -0
  51. package/dist/chunks/parse-fpdzgk4c.js +4 -0
  52. package/dist/chunks/platform-9g72gdqr.js +4 -0
  53. package/dist/chunks/{popover-fhdnt4be.js → popover-cnmsav6b.js} +2 -2
  54. package/dist/chunks/publish-panel-91x1cb5d.js +4 -0
  55. package/dist/chunks/publish-quick-e2dmf7eg.js +4 -0
  56. package/dist/chunks/publish-workspace-1x8mnzrj.js +4 -0
  57. package/dist/chunks/quick-search-2m4y6p1x.js +4 -0
  58. package/dist/chunks/references-yg1g9rwx.js +4 -0
  59. package/dist/chunks/right-panel-7mfyhzrk.js +4 -0
  60. package/dist/chunks/source-control-rrc4jgp5.js +4 -0
  61. package/dist/chunks/{split-qh21j2b0.js → split-d15yencd.js} +2 -2
  62. package/dist/chunks/{studio-r5g7fx7q.js → studio-0jhhznxy.js} +6 -6
  63. package/dist/chunks/studio-0nb57pjg.js +7 -0
  64. package/dist/chunks/{studio-bpzwhn88.js → studio-0wa4wv4s.js} +4 -4
  65. package/dist/chunks/studio-0z8kp4xk.js +5 -0
  66. package/dist/chunks/{studio-jqvke4ac.js → studio-18gc5t3v.js} +4 -4
  67. package/dist/chunks/{studio-xh6rf05g.js → studio-1b79rpcb.js} +4 -4
  68. package/dist/chunks/studio-211q5j7m.js +4 -0
  69. package/dist/chunks/studio-213pyfxh.js +16 -0
  70. package/dist/chunks/studio-2b1smjvp.js +5 -0
  71. package/dist/chunks/{studio-dst8y8vh.js → studio-2v6tc9jp.js} +4 -4
  72. package/dist/chunks/studio-30j0wz99.js +5 -0
  73. package/dist/chunks/studio-3kmqhwtt.js +5 -0
  74. package/dist/chunks/studio-44drsmhw.js +5 -0
  75. package/dist/chunks/{studio-d5f7sskn.js → studio-495b6f2r.js} +4 -4
  76. package/dist/chunks/{studio-g65t6svn.js → studio-4gthb2fd.js} +4 -4
  77. package/dist/chunks/{studio-bmwgj8ez.js → studio-4xadhxj1.js} +4 -4
  78. package/dist/chunks/{studio-zav9hgb3.js → studio-52mzbxqm.js} +4 -4
  79. package/dist/chunks/{studio-thgexj3w.js → studio-56mmnsw8.js} +7 -7
  80. package/dist/chunks/studio-59hz8f4r.js +5 -0
  81. package/dist/chunks/{studio-bk0anq3b.js → studio-5k9mfafh.js} +3 -3
  82. package/dist/chunks/{studio-qarg1xf9.js → studio-5qez462c.js} +5 -5
  83. package/dist/chunks/studio-6tnpg0vx.js +5 -0
  84. package/dist/chunks/studio-6w8mt734.js +6 -0
  85. package/dist/chunks/studio-7hm04x5z.js +5 -0
  86. package/dist/chunks/studio-8atmfjjm.js +7 -0
  87. package/dist/chunks/{studio-69g6a0jn.js → studio-8n99z71s.js} +4 -4
  88. package/dist/chunks/{studio-ep1384t9.js → studio-8vmad6x2.js} +4 -4
  89. package/dist/chunks/{studio-6sxnqqw8.js → studio-93p5k4r4.js} +4 -4
  90. package/dist/chunks/{studio-hy936516.js → studio-9gb0m81w.js} +4 -4
  91. package/dist/chunks/{studio-ek4fb4eb.js → studio-a4ymg4vd.js} +4 -4
  92. package/dist/chunks/studio-anc8067x.js +5 -0
  93. package/dist/chunks/{studio-ketrgtph.js → studio-dbdt67kr.js} +5 -5
  94. package/dist/chunks/studio-dbex091t.js +5 -0
  95. package/dist/chunks/{studio-nngc45ct.js → studio-dj8ctxey.js} +4 -4
  96. package/dist/chunks/{studio-pgze9p34.js → studio-ed9bz68j.js} +5 -5
  97. package/dist/chunks/{studio-4ncxzw5m.js → studio-fe2d1fp5.js} +4 -4
  98. package/dist/chunks/{studio-m8hesdrw.js → studio-fgp4cyy3.js} +4 -4
  99. package/dist/chunks/{studio-151kgn69.js → studio-ft706q02.js} +4 -4
  100. package/dist/chunks/{studio-pv1ts08p.js → studio-g79s1dyg.js} +5 -5
  101. package/dist/chunks/{studio-zkkt13qd.js → studio-gahswxp4.js} +4 -4
  102. package/dist/chunks/{studio-x5ffyssw.js → studio-gbqc12bk.js} +4 -4
  103. package/dist/chunks/{studio-4jy967rj.js → studio-gxphbetb.js} +5 -5
  104. package/dist/chunks/studio-hdf01j7q.js +7 -0
  105. package/dist/chunks/studio-hf259xzd.js +48 -0
  106. package/dist/chunks/studio-jbva4wxw.js +5 -0
  107. package/dist/chunks/{studio-td3h1644.js → studio-jj9gf1qc.js} +4 -4
  108. package/dist/chunks/studio-jsjg3mjv.js +5 -0
  109. package/dist/chunks/studio-jtvb8hd7.js +5 -0
  110. package/dist/chunks/{studio-c72v1kwc.js → studio-kp4vzrct.js} +4 -4
  111. package/dist/chunks/{studio-s4k1v7dt.js → studio-kpx02gaa.js} +6 -6
  112. package/dist/chunks/{studio-3yr1zdp9.js → studio-kv1728ex.js} +8 -8
  113. package/dist/chunks/studio-m25ry0mf.js +5 -0
  114. package/dist/chunks/studio-mcbwmkyh.js +5 -0
  115. package/dist/chunks/studio-meajkr51.js +5 -0
  116. package/dist/chunks/studio-mkjrjpr2.js +6 -0
  117. package/dist/chunks/{studio-nv5eq0f7.js → studio-mr084886.js} +181 -181
  118. package/dist/chunks/{studio-43pynnhz.js → studio-mrb10hv3.js} +4 -4
  119. package/dist/chunks/{studio-7qqb7hb8.js → studio-nhpg405j.js} +4 -4
  120. package/dist/chunks/{studio-n271vdsg.js → studio-nsec0hxm.js} +41 -41
  121. package/dist/chunks/{studio-ztzf8mce.js → studio-nv0mh0p5.js} +4 -4
  122. package/dist/chunks/studio-pafmb06w.js +5 -0
  123. package/dist/chunks/{studio-4s5bka8t.js → studio-pgftd79b.js} +4 -4
  124. package/dist/chunks/{studio-wn2cmf9q.js → studio-pk961xgz.js} +4 -4
  125. package/dist/chunks/studio-pzbr7g05.js +5 -0
  126. package/dist/chunks/{studio-15mfah02.js → studio-qcc5hsw2.js} +6 -6
  127. package/dist/chunks/studio-qfq58c8n.js +5 -0
  128. package/dist/chunks/{studio-j3bq30yz.js → studio-qvb2jr83.js} +4 -4
  129. package/dist/chunks/studio-r85zw6ay.js +5 -0
  130. package/dist/chunks/{studio-f69wcajk.js → studio-s0kwce5w.js} +5 -5
  131. package/dist/chunks/studio-s6ymkf3q.js +14 -0
  132. package/dist/chunks/{studio-8ncm9zwx.js → studio-s7p5ccxa.js} +4 -4
  133. package/dist/chunks/{studio-zzgp5yka.js → studio-s9jjap3x.js} +4 -4
  134. package/dist/chunks/{studio-ber1dgte.js → studio-t5hde1ws.js} +4 -4
  135. package/dist/chunks/studio-vj9yv897.js +5 -0
  136. package/dist/chunks/{studio-ajfq4fmr.js → studio-wmak8xjw.js} +4 -4
  137. package/dist/chunks/{studio-k05g7efh.js → studio-wqsmvtqj.js} +4 -4
  138. package/dist/chunks/studio-x3z3rhej.js +5 -0
  139. package/dist/chunks/{studio-7qr8k0v2.js → studio-xb55kpmy.js} +4 -4
  140. package/dist/chunks/{studio-f3awr5nc.js → studio-xxtdajmv.js} +7 -7
  141. package/dist/chunks/{studio-1a7zshnv.js → studio-xxttcym1.js} +4 -4
  142. package/dist/chunks/{studio-czpmzs9r.js → studio-y2zsnzye.js} +7 -7
  143. package/dist/chunks/{studio-9f5vcb4w.js → studio-y3tjh2xb.js} +3 -3
  144. package/dist/chunks/studio-y4f1rdsj.js +5 -0
  145. package/dist/chunks/studio-ybkd5w8k.js +5 -0
  146. package/dist/chunks/{studio-cv6gnjqd.js → studio-yetr4gg1.js} +4 -4
  147. package/dist/chunks/{studio-ehj3csbm.js → studio-z295anhs.js} +5 -5
  148. package/dist/chunks/{studio-pte5w7rf.js → studio-z89e6wpg.js} +8 -8
  149. package/dist/chunks/{studio-4z0cdh16.js → studio-zbwr6prc.js} +3 -3
  150. package/dist/chunks/{studio-w3aafbpk.js → studio-zdbsv5q2.js} +3 -3
  151. package/dist/chunks/{studio-eq5vqn1q.js → studio-zvthj8hg.js} +4 -4
  152. package/dist/chunks/toast-2kn2vrn4.js +4 -0
  153. package/dist/chunks/{toast-host-g3r2s0kn.js → toast-host-3rxeyj3d.js} +3 -3
  154. package/dist/chunks/{tooltip-gtx40bjg.js → tooltip-ghkxkasp.js} +2 -2
  155. package/dist/chunks/{tree-a19s1c05.js → tree-vw4nc63s.js} +2 -2
  156. package/dist/chunks/{tsMode-3f116hth.js → tsMode-7gngmamr.js} +7 -7
  157. package/dist/chunks/workspace-gv0hxgsm.js +4 -0
  158. package/dist/chunks/workspace-home-j7e0mjy5.js +4 -0
  159. package/dist/chunks/{ws-client-cb49m87s.js → ws-client-vs11sxc5.js} +3 -3
  160. package/dist/iframe-entry.js +14 -14
  161. package/dist/manifest.json +84 -83
  162. package/dist/studio.js +80 -80
  163. package/package.json +12 -12
  164. package/src/browse/preview-style.ts +2 -1
  165. package/src/canvas/canvas-render.ts +6 -1
  166. package/src/canvas/iframe-drop.ts +84 -1
  167. package/src/canvas/iframe-entry.ts +9 -6
  168. package/src/canvas/iframe-render.ts +13 -0
  169. package/src/collab/collab-session.ts +2 -2
  170. package/src/commands/levels.ts +1 -1
  171. package/src/create/add-new-flow.ts +2 -1
  172. package/src/editor/convert-targets.ts +2 -23
  173. package/src/editor/slash-menu.ts +35 -17
  174. package/src/files/files.ts +18 -0
  175. package/src/format/format-choices.ts +5 -5
  176. package/src/format/format-host.ts +35 -3
  177. package/src/grid/redirects-grid.ts +4 -4
  178. package/src/locale/element-names.ts +131 -0
  179. package/src/panels/dnd.ts +3 -1
  180. package/src/panels/elements-panel.ts +93 -36
  181. package/src/panels/imports-panel.ts +3 -2
  182. package/src/panels/navigator-panels.ts +10 -10
  183. package/src/panels/panel-registry.ts +3 -3
  184. package/src/panels/site-panel.ts +2 -1
  185. package/src/panels/style-panel.ts +2 -1
  186. package/src/panels/styles-panel.ts +2 -2
  187. package/src/platforms/cloud.ts +5 -2
  188. package/src/services/ai-command-tools.ts +2 -1
  189. package/src/services/ai-project-tools.ts +18 -5
  190. package/src/services/jxn-language.ts +110 -0
  191. package/src/services/model-uri.ts +3 -0
  192. package/src/services/monaco-setup.ts +6 -2
  193. package/src/settings/contributed-section.ts +2 -1
  194. package/src/settings/extension-commands.ts +2 -1
  195. package/src/settings/general-settings.ts +2 -2
  196. package/src/settings/settings-document.ts +5 -5
  197. package/src/shell.ts +1 -1
  198. package/src/studio.ts +28 -11
  199. package/src/style/variables-view.ts +2 -2
  200. package/src/surfaces/panel-elements.json +466 -206
  201. package/src/surfaces/panel-elements.ts +61 -25
  202. package/src/tabs/project-config.ts +13 -13
  203. package/src/tabs/tab.ts +7 -2
  204. package/src/types.ts +11 -2
  205. package/src/view.ts +9 -2
  206. package/src/workspace/tab-workspace.ts +2 -2
  207. package/src/workspace/workspace-back.ts +2 -2
  208. package/dist/chunks/adapter-12dxr3z7.js +0 -4
  209. package/dist/chunks/add-new-flow-any4pw2q.js +0 -4
  210. package/dist/chunks/cf-settings-hbj16kh3.js +0 -4
  211. package/dist/chunks/color-thzpppyc.js +0 -4
  212. package/dist/chunks/components-a98hzqw9.js +0 -4
  213. package/dist/chunks/content-source-jq3fjs3w.js +0 -4
  214. package/dist/chunks/convert-to-repeater-g66r6qp1.js +0 -4
  215. package/dist/chunks/editor-rth1rzj8.js +0 -4
  216. package/dist/chunks/entry-commands-02j0s19w.js +0 -4
  217. package/dist/chunks/extension-sections-rqrs7dr0.js +0 -4
  218. package/dist/chunks/file-ops-6a49er4c.js +0 -4
  219. package/dist/chunks/files-b2p9y0yz.js +0 -4
  220. package/dist/chunks/format-host-7xe57k1s.js +0 -4
  221. package/dist/chunks/fs-events-0xsy168g.js +0 -4
  222. package/dist/chunks/grid-open-h3q4fjy5.js +0 -4
  223. package/dist/chunks/iframe-entry-m24fza30.js +0 -5
  224. package/dist/chunks/iframe-host-8jcmaktg.js +0 -4
  225. package/dist/chunks/index-7nj2375a.js +0 -4
  226. package/dist/chunks/jx-validate-808zgbgy.js +0 -4
  227. package/dist/chunks/layers-0ffdtqjn.js +0 -4
  228. package/dist/chunks/layers-panel-rw62wtvf.js +0 -4
  229. package/dist/chunks/lit-html-9bz9rcfv.js +0 -4
  230. package/dist/chunks/media-library-2a5hdbek.js +0 -4
  231. package/dist/chunks/media-open-069kzhj4.js +0 -4
  232. package/dist/chunks/media-picker-4p4pd28y.js +0 -4
  233. package/dist/chunks/monaco-buffer-m9fd2dka.js +0 -4
  234. package/dist/chunks/monaco-setup-e0crh3v6.js +0 -4
  235. package/dist/chunks/notify-re3js157.js +0 -4
  236. package/dist/chunks/parse-hee7ne9b.js +0 -4
  237. package/dist/chunks/platform-rywy3tkg.js +0 -4
  238. package/dist/chunks/publish-panel-czkpg8m4.js +0 -4
  239. package/dist/chunks/publish-quick-b9wbhqj6.js +0 -4
  240. package/dist/chunks/publish-workspace-yy6d23b7.js +0 -4
  241. package/dist/chunks/quick-search-tp7pjnmv.js +0 -4
  242. package/dist/chunks/references-wd2yj8s1.js +0 -4
  243. package/dist/chunks/right-panel-x971k0zc.js +0 -4
  244. package/dist/chunks/source-control-k4p00hpe.js +0 -4
  245. package/dist/chunks/studio-1d8m5s38.js +0 -16
  246. package/dist/chunks/studio-35pmfjtx.js +0 -5
  247. package/dist/chunks/studio-44xqe0b7.js +0 -4
  248. package/dist/chunks/studio-48yzqzqr.js +0 -5
  249. package/dist/chunks/studio-5rq3e4q5.js +0 -5
  250. package/dist/chunks/studio-6hcfm9q3.js +0 -5
  251. package/dist/chunks/studio-6mf31e16.js +0 -5
  252. package/dist/chunks/studio-8w5pa88p.js +0 -7
  253. package/dist/chunks/studio-9c6cbq2c.js +0 -4
  254. package/dist/chunks/studio-aaw7wxv8.js +0 -5
  255. package/dist/chunks/studio-afx9brsc.js +0 -5
  256. package/dist/chunks/studio-dmgh2rrj.js +0 -5
  257. package/dist/chunks/studio-erjccagy.js +0 -5
  258. package/dist/chunks/studio-ex1644g2.js +0 -5
  259. package/dist/chunks/studio-gw83z798.js +0 -7
  260. package/dist/chunks/studio-j52ks8q7.js +0 -5
  261. package/dist/chunks/studio-kefs4sz4.js +0 -5
  262. package/dist/chunks/studio-m3jrjfhy.js +0 -14
  263. package/dist/chunks/studio-n4pnf0sw.js +0 -5
  264. package/dist/chunks/studio-ncwx602y.js +0 -5
  265. package/dist/chunks/studio-nzp0sqjg.js +0 -5
  266. package/dist/chunks/studio-ph17an19.js +0 -5
  267. package/dist/chunks/studio-q509sqt8.js +0 -5
  268. package/dist/chunks/studio-s49y6m5b.js +0 -5
  269. package/dist/chunks/studio-t46rt6gx.js +0 -5
  270. package/dist/chunks/studio-veh91hjv.js +0 -5
  271. package/dist/chunks/studio-vswmz40d.js +0 -6
  272. package/dist/chunks/studio-w4mfaaam.js +0 -5
  273. package/dist/chunks/studio-x5py815g.js +0 -5
  274. package/dist/chunks/studio-z6s56dzz.js +0 -5
  275. package/dist/chunks/studio-ze1sm147.js +0 -7
  276. package/dist/chunks/studio-zn7ysyzb.js +0 -5
  277. package/dist/chunks/toast-w11qpkyz.js +0 -4
  278. package/dist/chunks/workspace-home-61n1jz3q.js +0 -4
  279. package/dist/chunks/workspace-sheek6zw.js +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "10.5.0",
3
+ "version": "10.6.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -47,7 +47,7 @@
47
47
  "scripts": {
48
48
  "build": "bun run scripts/build.ts",
49
49
  "build:metafile": "bun build ./src/studio.ts --outdir dist-metafile --target browser --sourcemap=linked --metafile=metafile.json",
50
- "gen:webdata": "bun run scripts/gen-webdata.js",
50
+ "gen:webdata": "bun run scripts/gen-webdata.ts",
51
51
  "eval": "bun run evals/cli.ts",
52
52
  "eval:headless": "bun run tests/harness/run-eval.ts",
53
53
  "lint:styles": "bun run scripts/check-styles.ts",
@@ -60,16 +60,16 @@
60
60
  "dependencies": {
61
61
  "@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
62
62
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^3.0.0",
63
- "@jxsuite/ai": "^0.43.0",
64
- "@jxsuite/collab": "^0.11.6",
65
- "@jxsuite/create": "^1.5.4",
66
- "@jxsuite/formulas": "^0.0.28",
67
- "@jxsuite/markup": "^1.0.4",
68
- "@jxsuite/protocol": "^6.2.2",
69
- "@jxsuite/runtime": "^6.1.0",
70
- "@jxsuite/schema": "^5.2.0",
71
- "@jxsuite/site": "^2.3.1",
72
- "@jxsuite/ui": "^0.10.1",
63
+ "@jxsuite/ai": "^0.43.1",
64
+ "@jxsuite/collab": "^0.11.7",
65
+ "@jxsuite/create": "^1.5.5",
66
+ "@jxsuite/formulas": "^0.0.29",
67
+ "@jxsuite/markup": "^1.0.5",
68
+ "@jxsuite/protocol": "^6.2.3",
69
+ "@jxsuite/runtime": "^6.2.0",
70
+ "@jxsuite/schema": "^5.3.0",
71
+ "@jxsuite/site": "^2.4.0",
72
+ "@jxsuite/ui": "^0.10.2",
73
73
  "@vue/reactivity": "3.5.44",
74
74
  "ajv": "^8.20.0",
75
75
  "ajv-formats": "^3.0.1",
@@ -27,6 +27,7 @@ import { isFunction, isNonNullObject, isString } from "@jxsuite/schema/guards";
27
27
  import { previewAssetSrcFor } from "../canvas/asset-refs";
28
28
  import { projectState } from "../store";
29
29
  import type { JxHeadEntry, ProjectConfig } from "@jxsuite/schema/types";
30
+ import { PROJECT_FILE_NAME } from "@jxsuite/schema/project-file";
30
31
 
31
32
  /** The preview box, as a project sheet names the page's root and body inside a shadow root. */
32
33
  const HOST_ROOTS = { body: ":host", root: ":host" };
@@ -93,7 +94,7 @@ export function headStylesheetHrefs(head: readonly JxHeadEntry[] | undefined): s
93
94
 
94
95
  /** One linked stylesheet as a preview sheet, or null when it cannot be read. */
95
96
  async function linkedSheet(href: string): Promise<CSSStyleSheet | null> {
96
- const url = previewAssetSrcFor(href, "project.json");
97
+ const url = previewAssetSrcFor(href, PROJECT_FILE_NAME);
97
98
  try {
98
99
  const response = await fetch(url);
99
100
  if (!response.ok) {
@@ -175,8 +175,13 @@ export function initCanvasRender(ctx: CanvasRenderCtx) {
175
175
  */
176
176
  function sourceLang(tab: Tab) {
177
177
  const format = formatByName(tab.doc.sourceFormat);
178
+ if (!format) {
179
+ return "json";
180
+ }
178
181
  // The essence: splitting the declared type would yield `markdown; variant=GFM` as a language id.
179
- return format ? (mediaTypeEssence(format.mediaType)?.split("/").pop() ?? "plaintext") : "json";
182
+ const fromType = mediaTypeEssence(format.mediaType)?.split("/").pop();
183
+ // `.jxn` has no media type to read a language from, and its path says everything it needs to.
184
+ return fromType ?? monacoLangForPath(tab.documentPath ?? "");
180
185
  }
181
186
 
182
187
  /**
@@ -16,7 +16,7 @@
16
16
  * preview is display-only and never the source of truth (patch-mid-drag safety).
17
17
  */
18
18
 
19
- import { parseJxPath } from "./path-mapping";
19
+ import { parseJxPath, serializeJxPath } from "./path-mapping";
20
20
  import { displayTagName, isNonNullObject } from "@jxsuite/schema/guards";
21
21
  import { rectOf, elementAtPoint } from "../utils/geometry";
22
22
  import { getNodeAtPath, isAncestor, pathsEqual } from "../state";
@@ -161,6 +161,89 @@ export function resolveDropTarget(x: number, y: number, doc: Document): HTMLElem
161
161
  return nearestPathEl(hit);
162
162
  }
163
163
 
164
+ /**
165
+ * The drop for a cursor over blank canvas: a point under no addressable node and outside a layout's
166
+ * chrome.
167
+ *
168
+ * **The canvas around the content is part of the page** (§5.3). A short page fills the top of a
169
+ * frame whose remainder is the body, or the unstamped layout node its blocks are drawn into, and
170
+ * that blank space is exactly where an author drags a new block. The drop goes to the nearest edge
171
+ * of the page's own top-level blocks, so below the content it lands after the last one. Layout
172
+ * chrome (`data-jx-layout-region`, the site header and footer a page is drawn inside) is not the
173
+ * page, so a drop over it resolves to nothing, as a click there edits nothing.
174
+ *
175
+ * Two shapes reach here. Without a layout the root is an element (`data-jx-path="[]"`) and the root
176
+ * branch of {@link computeDropInstruction} already answers. Inside a layout the page root has no
177
+ * element of its own (its children are drawn into the layout's slot), so its blocks are found by
178
+ * their stamped paths, and each keeps its own path as the target, which a text child of the root
179
+ * would otherwise shift.
180
+ */
181
+ export function resolveBlankCanvasDrop(
182
+ x: number,
183
+ y: number,
184
+ doc: Document,
185
+ shadowDoc: JxMutableNode,
186
+ src: DragSrcKind,
187
+ ): DropPreview | null {
188
+ const hit = elementAtPoint(x, y, doc);
189
+ if (!hit || nearestPathEl(hit) || hit.closest("[data-jx-layout-region]")) {
190
+ return null;
191
+ }
192
+ const root = doc.querySelector<HTMLElement>(`[data-jx-path="${serializeJxPath([])}"]`);
193
+ if (root) {
194
+ return computeDropInstruction(root, y, shadowDoc, src);
195
+ }
196
+ const blocks = [...doc.querySelectorAll<HTMLElement>("[data-jx-path]")].filter(
197
+ (el) => parseJxPath(el.dataset.jxPath ?? "").length === 2,
198
+ );
199
+ const [first, ...rest] = blocks;
200
+ if (!first) {
201
+ return canDrop(src, [])
202
+ ? { edge: "inside", instruction: "make-child", referenceRect: rectFor(hit), targetPath: [] }
203
+ : null;
204
+ }
205
+ return nearestBlockEdge(first, rest, y, src);
206
+ }
207
+
208
+ /** How far `cursorY` is from `block`, and whether its bottom edge is the nearer one. */
209
+ function blockEdge(block: HTMLElement, cursorY: number) {
210
+ const rect = rectFor(block);
211
+ const above = Math.abs(cursorY - rect.y);
212
+ const below = Math.abs(cursorY - (rect.y + rect.height));
213
+ return { below: below <= above, block, distance: Math.min(above, below) };
214
+ }
215
+
216
+ /**
217
+ * {@link nearestChildEdge} over blocks that carry their own paths, for a root with no element. The
218
+ * first block is passed apart from the rest because an empty page is the caller's to answer.
219
+ */
220
+ function nearestBlockEdge(
221
+ first: HTMLElement,
222
+ rest: readonly HTMLElement[],
223
+ cursorY: number,
224
+ src: DragSrcKind,
225
+ ): DropPreview | null {
226
+ let best = blockEdge(first, cursorY);
227
+ for (const block of rest) {
228
+ const edge = blockEdge(block, cursorY);
229
+ if (edge.distance < best.distance) {
230
+ best = edge;
231
+ }
232
+ }
233
+ const targetPath = parseJxPath(best.block.dataset.jxPath ?? "");
234
+ if (!canDrop(src, targetPath)) {
235
+ return null;
236
+ }
237
+ return best.below
238
+ ? {
239
+ edge: "bottom",
240
+ instruction: "reorder-below",
241
+ referenceRect: rectFor(best.block),
242
+ targetPath,
243
+ }
244
+ : { edge: "top", instruction: "reorder-above", referenceRect: rectFor(best.block), targetPath };
245
+ }
246
+
164
247
  /**
165
248
  * Compute the structural drop placement for a cursor over `targetEl`, PURE against iframe-realm
166
249
  * rects.
@@ -24,6 +24,7 @@ import { parseJxPath, serializeJxPath } from "./path-mapping";
24
24
  import {
25
25
  AUTO_SCROLL_STEP,
26
26
  computeDropInstruction,
27
+ resolveBlankCanvasDrop,
27
28
  rectFor,
28
29
  resolveDropTarget,
29
30
  scrollDirection,
@@ -107,8 +108,9 @@ setNpmModuleResolver((specifier) => npmModuleUrl(specifier, location.href));
107
108
 
108
109
  /**
109
110
  * Resolve the drop placement for a forwarded cursor: point hit-test → nearest `[data-jx-path]` →
110
- * pure {@link computeDropInstruction}. Returns null when the cursor resolves to no droppable target.
111
- * Shared by the `dragMove` (display-only preview) and `drop` (fresh, authoritative) handlers.
111
+ * pure {@link computeDropInstruction}, or, over blank canvas, {@link resolveBlankCanvasDrop}. Returns
112
+ * null when the cursor resolves to no droppable target (a layout's chrome). Shared by the
113
+ * `dragMove` (display-only preview) and `drop` (fresh, authoritative) handlers.
112
114
  */
113
115
  function previewAt(
114
116
  cursor: { x: number; y: number },
@@ -118,7 +120,7 @@ function previewAt(
118
120
  ) {
119
121
  const targetEl = resolveDropTarget(cursor.x, cursor.y, doc);
120
122
  if (!targetEl) {
121
- return null;
123
+ return resolveBlankCanvasDrop(cursor.x, cursor.y, doc, shadowDoc, src);
122
124
  }
123
125
  return computeDropInstruction(targetEl, cursor.y, shadowDoc, src);
124
126
  }
@@ -703,10 +705,11 @@ export function startCanvasIframe(opts: {
703
705
  const doc = container.ownerDocument;
704
706
  const targetEl = resolveDropTarget(cursor.x, cursor.y, doc);
705
707
  const hit = targetEl ? fileDropHitFor(targetEl, shadowDoc) : null;
706
- const preview =
707
- shadowDoc && targetEl
708
+ const preview = shadowDoc
709
+ ? targetEl
708
710
  ? computeDropInstruction(targetEl, cursor.y, shadowDoc, FILE_DRAG_SRC)
709
- : null;
711
+ : resolveBlankCanvasDrop(cursor.x, cursor.y, doc, shadowDoc, FILE_DRAG_SRC)
712
+ : null;
710
713
  if (kind === "fileDragOver") {
711
714
  if (e.dataTransfer) {
712
715
  e.dataTransfer.dropEffect = "copy";
@@ -24,6 +24,7 @@ import {
24
24
  setCanvasDelinkCommands,
25
25
  setCanvasDelinkPopovers,
26
26
  setCanvasViewportTranspose,
27
+ setDocumentParser,
27
28
  setRootMedia,
28
29
  setSkipAutoRequests,
29
30
  setSkipServerFunctions,
@@ -43,6 +44,17 @@ import { isNonNullObject, isString } from "@jxsuite/schema/guards";
43
44
  import type { JxDocument, JxElement, JxStyle } from "@jxsuite/schema/types";
44
45
  import type { PathMapCtx } from "./path-mapping";
45
46
 
47
+ /**
48
+ * Read a `.jxn` `$ref`, which the editing server answers as its text.
49
+ *
50
+ * Loaded when one turns up, because the parser is a third of this entry's size budget and most
51
+ * canvases never see a `.jxn` reference.
52
+ */
53
+ async function parseDocumentText(text: string, source: string): Promise<JxDocument> {
54
+ const { parseJxn } = await import("@jxsuite/schema/jxn");
55
+ return parseJxn(text, source);
56
+ }
57
+
46
58
  /**
47
59
  * The retained render context a full render leaves behind so the surgical patcher can re-render an
48
60
  * individual subtree (insert/replace/attr edits) with the SAME scope, doc base, path mapping, and
@@ -1070,6 +1082,7 @@ export async function renderResolvedDocument(opts: {
1070
1082
  const assets = opts.assets ?? null;
1071
1083
  setCanvasAssetResolver(assets ? (value) => resolveAssetRef(value, assets) : null);
1072
1084
  setSkipServerFunctions(opts.mode !== "preview");
1085
+ setDocumentParser(parseDocumentText);
1073
1086
  // Same gate for automatic `$prototype: "Request"` state entries. `buildScope` re-resolves every
1074
1087
  // State entry on each full render, so without this an escalating authoring action (a signals-panel
1075
1088
  // Edit, or Enter inside component-wrapped content) issued an HTTP request per render. Edit/design
@@ -33,7 +33,7 @@ import {
33
33
  setTransactObserver,
34
34
  transactDoc,
35
35
  } from "../tabs/transact";
36
- import { PROJECT_CONFIG_PATH } from "../tabs/tab";
36
+ import { isProjectConfigPath } from "../tabs/tab";
37
37
  import type { TransactOrigin } from "../tabs/transact";
38
38
  import type { TransactionRecord } from "../tabs/patch-ops";
39
39
  import type { Tab } from "../tabs/tab";
@@ -672,7 +672,7 @@ const liveTabs = new Set<Tab>();
672
672
  */
673
673
  export function ensureCollab(tab: Tab): void {
674
674
  const platform = maybePlatform();
675
- if (!platform?.collab || !tab.documentPath || tab.documentPath === PROJECT_CONFIG_PATH) {
675
+ if (!platform?.collab || !tab.documentPath || isProjectConfigPath(tab.documentPath)) {
676
676
  return;
677
677
  }
678
678
  const runtime = runtimeFor(tab);
@@ -180,7 +180,7 @@ export const PANEL_PLACEMENT_MATRIX: Readonly<Record<PanelPlacement, PlacementRu
180
180
  },
181
181
  "rail/document": {
182
182
  admits: ["document"],
183
- note: "the rail's lower group — Outline, Data, and Packages with the developer panels on",
183
+ note: "the rail's lower group — Insert, Outline, Data, and Packages with the developer panels on",
184
184
  },
185
185
  navigator: {
186
186
  admits: ["project", "document"],
@@ -101,6 +101,7 @@ import type {
101
101
  AddNewView,
102
102
  } from "../surfaces/add-new";
103
103
  import type { JsonObject, JxMutableNode } from "@jxsuite/schema/types";
104
+ import { projectConfigPath } from "../tabs/tab";
104
105
 
105
106
  /** The `source` every Problem this flow files carries. */
106
107
  const SOURCE = "Add new";
@@ -385,7 +386,7 @@ async function performCollection(creation: Creation): Promise<AddNewResult | nul
385
386
  if (!committed.ok) {
386
387
  return null;
387
388
  }
388
- const wrote = ["project.json"];
389
+ const wrote = [projectConfigPath()];
389
390
  const missed: string[] = [];
390
391
  for (const write of [{ body: creation.content, path: creation.path }, ...creation.extraWrites]) {
391
392
  try {
@@ -1,32 +1,11 @@
1
1
  import elementsMeta from "../../data/elements-meta.json";
2
2
 
3
+ import { elementName } from "../locale/element-names";
3
4
  import { ELEMENT_DEFS } from "./editable-tags";
4
5
  import type { SlashCommand } from "./inline-edit";
5
6
 
6
7
  export type { SlashCommand } from "./inline-edit";
7
8
 
8
- const TAG_LABELS = new Map<string, string>([
9
- ["article", "Article"],
10
- ["aside", "Aside"],
11
- ["blockquote", "Blockquote"],
12
- ["div", "Div"],
13
- ["footer", "Footer"],
14
- ["h1", "Heading 1"],
15
- ["h2", "Heading 2"],
16
- ["h3", "Heading 3"],
17
- ["h4", "Heading 4"],
18
- ["h5", "Heading 5"],
19
- ["h6", "Heading 6"],
20
- ["header", "Header"],
21
- ["main", "Main"],
22
- ["nav", "Nav"],
23
- ["ol", "Numbered List"],
24
- ["p", "Paragraph"],
25
- ["search", "Search"],
26
- ["section", "Section"],
27
- ["ul", "Bulleted List"],
28
- ]);
29
-
30
9
  const groups: Readonly<Record<string, string[]>> = elementsMeta.$convertGroups;
31
10
 
32
11
  /**
@@ -56,7 +35,7 @@ export function getConvertTargets(currentTag: string, isEmpty: boolean): SlashCo
56
35
 
57
36
  return [...tags].map((tag) => ({
58
37
  description: "",
59
- label: TAG_LABELS.get(tag) || tag.charAt(0).toUpperCase() + tag.slice(1),
38
+ label: elementName(tag),
60
39
  tag,
61
40
  }));
62
41
  }
@@ -28,6 +28,7 @@
28
28
 
29
29
  import { openSlashMenuSurface } from "../surfaces/slash-menu";
30
30
  import { componentSlashCommands } from "./insert-component";
31
+ import { elementName } from "../locale/element-names";
31
32
  import { rectOf } from "../utils/geometry";
32
33
  import type {
33
34
  SlashMenuGroup,
@@ -39,24 +40,41 @@ import type { SlashCommand } from "./inline-edit";
39
40
 
40
41
  // ─── Commands ─────────────────────────────────────────────────────────────────
41
42
 
42
- const SLASH_COMMANDS = [
43
- { description: "Large heading", label: "Heading 1", tag: "h1" },
44
- { description: "Medium heading", label: "Heading 2", tag: "h2" },
45
- { description: "Small heading", label: "Heading 3", tag: "h3" },
46
- { description: "Plain text", label: "Paragraph", tag: "p" },
47
- { description: "Unordered list", label: "Bulleted List", tag: "ul" },
48
- { description: "Numbered list", label: "Numbered List", tag: "ol" },
49
- { description: "Quote block", label: "Blockquote", tag: "blockquote" },
50
- { description: "Insert image", label: "Image", tag: "img" },
51
- { description: "Divider line", label: "Horizontal Rule", tag: "hr" },
52
- { description: "Button element", label: "Button", tag: "button" },
53
- { description: "Anchor link", label: "Link", tag: "a" },
54
- { description: "Preformatted code", label: "Code Block", tag: "pre" },
55
- { description: "Insert table", label: "Table", tag: "table" },
56
- { description: "Container", label: "Div", tag: "div" },
57
- { description: "Section container", label: "Section", tag: "section" },
43
+ /**
44
+ * The HTML elements the slash menu offers, in this order. Each row's label is the element's name in
45
+ * the locale catalog (`locale/element-names.ts`), so the menu, the Insert palette and the convert
46
+ * menu call an element the same thing in every language. A row carries no description: the
47
+ * catalog's are sentences, written for the palette's tooltips, and a menu row draws its description
48
+ * on the label's own line and cuts it off there, while a name like "Bulleted list" already says
49
+ * what the row inserts.
50
+ */
51
+ const SLASH_TAGS = [
52
+ "h1",
53
+ "h2",
54
+ "h3",
55
+ "p",
56
+ "ul",
57
+ "ol",
58
+ "blockquote",
59
+ "img",
60
+ "hr",
61
+ "button",
62
+ "a",
63
+ "pre",
64
+ "table",
65
+ "div",
66
+ "section",
58
67
  ];
59
68
 
69
+ /** The element rows, named in the reader's language at the moment the menu opens. */
70
+ function elementCommands(): SlashCommand[] {
71
+ return SLASH_TAGS.map((tag) => ({
72
+ description: "",
73
+ label: elementName(tag),
74
+ tag,
75
+ }));
76
+ }
77
+
60
78
  // ─── State ────────────────────────────────────────────────────────────────────
61
79
 
62
80
  /** Callbacks a caller passes to {@link showSlashMenu}/{@link showSlashMenuAtRect}. */
@@ -127,7 +145,7 @@ export function showSlashMenuAtRect(
127
145
  * on offer.
128
146
  */
129
147
  function standardCommands(): SlashCommand[] {
130
- return [...SLASH_COMMANDS, ...componentSlashCommands()];
148
+ return [...elementCommands(), ...componentSlashCommands()];
131
149
  }
132
150
 
133
151
  /** Everything on offer right now: what the caller named, or the standard set. */
@@ -286,6 +286,24 @@ export async function loadProject() {
286
286
  }
287
287
  }
288
288
 
289
+ /**
290
+ * Show the documents the operating system asks of this window, now and from here on.
291
+ *
292
+ * Called once the window has finished loading whatever it holds, so a document opens on top of the
293
+ * restored session rather than being replaced by it. Called whether or not there is a project yet,
294
+ * because a welcome window can be given one later and then be asked for a document in it.
295
+ */
296
+ export function followOpenDocuments(): void {
297
+ getPlatform().subscribeOpenDocument?.((path) => {
298
+ // A window whose project did not load has been told why, and has nowhere to put a tab.
299
+ if (!projectState) {
300
+ return;
301
+ }
302
+ // It reports its own failure ("Could not open …"), as it does for every other caller.
303
+ void openFileInTab(path);
304
+ });
305
+ }
306
+
289
307
  // ─── Open Project (PAL-based) ─────────────────────────────────────────────
290
308
 
291
309
  /**
@@ -69,11 +69,11 @@ function declaresDocumentKind(kinds: readonly string[]): boolean {
69
69
  /**
70
70
  * Whether this path is (or is used as) a layout.
71
71
  *
72
- * Layouts are JSON at BOTH readers — `packages/compiler/src/site/layout-resolver.ts` `JSON.parse`s
73
- * and throws, `packages/studio/src/site-context.ts` `JSON.parse`s and silently returns null — and
74
- * there is no `"layout"` document kind for a format to declare, so this clause is hand-written and
75
- * cannot be derived. `layouts/` is excluded by {@link DOCUMENT_ROOTS} already; what remains is a
76
- * project whose `layout` points somewhere inside a document root.
72
+ * A layout is native at both readers — `packages/compiler/src/site/layout-resolver.ts` reads
73
+ * `.json` and `.jxn` and throws, `packages/studio/src/site-context.ts` `JSON.parse`s and silently
74
+ * returns null — and there is no `"layout"` document kind for a format to declare, so this clause
75
+ * is hand-written and cannot be derived. `layouts/` is excluded by {@link DOCUMENT_ROOTS} already;
76
+ * what remains is a project whose `layout` points somewhere inside a document root.
77
77
  */
78
78
  function isLayoutPath(path: string): boolean {
79
79
  const declared = projectState?.projectConfig?.layout;
@@ -4,7 +4,9 @@
4
4
  * Format classes are auto-discovered server-side from the project imports map
5
5
  * (specs/extensions.md); the studio introspects them via the PAL (`listFormats`) and invokes
6
6
  * parse/serialize capabilities via `formatAction` (POST /__studio/format in the dev server, RPC on
7
- * desktop). The studio itself holds zero format knowledge — `.json` is the single native built-in.
7
+ * desktop). The studio holds no other format knowledge: `.json` is native, and `.jxn` is read and
8
+ * written here in the same process with the same code as everywhere else (`spec.md` §22), so a host
9
+ * with no extension runtime has it too.
8
10
  */
9
11
 
10
12
  import { setProjectSchemasForMonaco } from "../services/monaco-lazy";
@@ -17,7 +19,8 @@ import type {
17
19
  StudioPlatform,
18
20
  } from "../types";
19
21
  import { isString } from "@jxsuite/schema/guards";
20
- import type { JxDocumentValue, JxMutableNode } from "@jxsuite/schema/types";
22
+ import { parseJxn, serializeJxn } from "@jxsuite/schema/jxn";
23
+ import type { JxDocument, JxDocumentValue, JxMutableNode } from "@jxsuite/schema/types";
21
24
 
22
25
  export interface StudioFormatHints {
23
26
  icon?: string;
@@ -59,6 +62,24 @@ export interface StudioFormat {
59
62
  capabilities: Record<string, { identifier: string; timing: string[] }>;
60
63
  }
61
64
 
65
+ /**
66
+ * What opening a `.jxn` file is told about its format. It is not in {@link getFormats}, which is
67
+ * what a project chose and what a picker offers: a new document is still `.json`.
68
+ */
69
+ const JXN_FORMAT: StudioFormat = {
70
+ capabilities: {
71
+ parse: { identifier: "parse", timing: ["compiler", "server"] },
72
+ serialize: { identifier: "serialize", timing: ["compiler", "server"] },
73
+ },
74
+ documentKinds: ["page", "component"],
75
+ exportTarget: false,
76
+ extensions: [".jxn"],
77
+ mediaType: null,
78
+ name: "jxn",
79
+ remote: false,
80
+ studio: { documentMode: { default: "component" } },
81
+ };
82
+
62
83
  let _formats: StudioFormat[] = [];
63
84
  let _loaded: Promise<StudioFormat[]> | null = null;
64
85
 
@@ -235,6 +256,9 @@ export function getFormats(): StudioFormat[] {
235
256
  /** Look up the format claiming a file extension (".md" or "md"), optionally by capability. */
236
257
  export function formatByExtension(ext: string, capability?: string): StudioFormat | undefined {
237
258
  const norm = ext.startsWith(".") ? ext.toLowerCase() : `.${ext.toLowerCase()}`;
259
+ if (norm === ".jxn") {
260
+ return JXN_FORMAT;
261
+ }
238
262
  return _formats.find(
239
263
  (f) => f.extensions.includes(norm) && (!capability || f.capabilities[capability]),
240
264
  );
@@ -245,7 +269,7 @@ export function formatByName(name: string | null | undefined): StudioFormat | un
245
269
  if (!name) {
246
270
  return undefined;
247
271
  }
248
- return _formats.find((f) => f.name === name);
272
+ return name === JXN_FORMAT.name ? JXN_FORMAT : _formats.find((f) => f.name === name);
249
273
  }
250
274
 
251
275
  /** Error for opening a non-JSON file when no imported format class claims its extension. */
@@ -330,6 +354,9 @@ export async function formatParse(
330
354
  source: string,
331
355
  options?: FormatActionOptions,
332
356
  ): Promise<JxMutableNode> {
357
+ if (name === JXN_FORMAT.name) {
358
+ return parseJxn(source);
359
+ }
333
360
  /* SAFETY: a format class's `parse` capability returns the Jx document it built from the source —
334
361
  The contract every format class implements (specs/extensions.md) — and nothing here inspects
335
362
  It before `splitFormatDocument` reads its keys. */
@@ -352,6 +379,11 @@ export async function formatSerialize(
352
379
  doc: JxMutableNode,
353
380
  options?: FormatActionOptions,
354
381
  ): Promise<string> {
382
+ if (name === JXN_FORMAT.name) {
383
+ // `source` is what keeps the comments: everything that did not change keeps its exact text.
384
+ // SAFETY: a document in its editing form, and every member of a JxDocument is optional.
385
+ return serializeJxn(doc as JxDocument, options?.source);
386
+ }
355
387
  /* SAFETY: a format class's `serialize` capability returns the source text it wrote — the contract
356
388
  Every format class implements (specs/extensions.md) — so the answer is a string. */
357
389
  return (await formatAction({
@@ -29,7 +29,7 @@ import { projectState } from "../store";
29
29
  import { clearProblems, notify, problemFindings } from "../services/notify";
30
30
  import { showPromptDialog } from "../ui/layers";
31
31
  import { activateTab, openTab, workspace } from "../workspace/workspace";
32
- import { PROJECT_CONFIG_PATH, commitProjectConfig } from "../tabs/project-config";
32
+ import { projectConfigPath, commitProjectConfig } from "../tabs/project-config";
33
33
  import { pageRoute } from "../panels/tab-strip";
34
34
  import { createGridController, getGridController } from "./grid-controller";
35
35
  import {
@@ -230,7 +230,7 @@ export function reportRedirectProblems(
230
230
  action: "redirects.open",
231
231
  detail: problem.detail,
232
232
  key: `redirects.${problem.rule}:${problem.source}`,
233
- path: PROJECT_CONFIG_PATH,
233
+ path: projectConfigPath(),
234
234
  source: REDIRECTS_PROBLEM_SOURCE,
235
235
  tier: "problem",
236
236
  });
@@ -263,7 +263,7 @@ export function createRedirectsSource(): GridSource {
263
263
  const load = () => rulesFromConfig(currentConfig());
264
264
 
265
265
  return {
266
- backingPaths: () => new Map([[PROJECT_CONFIG_PATH, "*"]]),
266
+ backingPaths: () => new Map([[projectConfigPath(), "*"]]),
267
267
  capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
268
268
  columns: () => Promise.resolve(redirectColumns()),
269
269
 
@@ -476,7 +476,7 @@ export function stageRedirectImport(
476
476
  action: "redirects.import",
477
477
  detail: notes.join("\n"),
478
478
  key: "redirects.import",
479
- path: PROJECT_CONFIG_PATH,
479
+ path: projectConfigPath(),
480
480
  source: REDIRECTS_PROBLEM_SOURCE,
481
481
  tier: "problem",
482
482
  },