@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
@@ -0,0 +1,131 @@
1
+ /**
2
+ * What Studio calls an HTML element, in the reader's language: "Heading 1" for `h1`, "Bulleted
3
+ * list" for `ul`, and the names of the Insert palette's categories (§5.3).
4
+ *
5
+ * **No web-platform database carries these names.** `@webref/elements` lists each element's tag and
6
+ * interface and nothing a person reads, and MDN's translated pages are prose, not data. So the
7
+ * names are Studio's own, kept as one catalog per language under `data/locales/<language>/`.
8
+ * English (`data/locales/en/elements.json`) is the source: it names every tag the palette offers,
9
+ * which `tests/locale-element-names.test.ts` holds it to, and a translation is a copy of that file
10
+ * with its values translated and its keys left alone, registered in {@link CATALOGS}.
11
+ *
12
+ * **The language is the reader's, and Studio has no setting for it yet.** {@link studioLocale}
13
+ * takes the first of the browser's preferred languages that has a catalog, by exact tag and then by
14
+ * base language (`fr-CA` reads `fr`), and falls back to English. A name the chosen catalog lacks
15
+ * falls back to the English one, and a tag no catalog knows is shown as itself, so a translation
16
+ * can land incomplete and an element a future `@webref` release adds is never nameless.
17
+ *
18
+ * Three surfaces read this module, so they cannot disagree about a name: the Insert palette's
19
+ * cards, the slash menu, and the block action bar's convert menu.
20
+ *
21
+ * @docs studio/design/elements
22
+ */
23
+
24
+ import en from "../../data/locales/en/elements.json";
25
+
26
+ /** One element's entry: the name a card shows, and the line that says what it is for. */
27
+ export interface ElementEntry {
28
+ name: string;
29
+ description: string;
30
+ }
31
+
32
+ /** One language's catalog: category names by key, and element entries by tag. */
33
+ export interface ElementCatalog {
34
+ categories: Readonly<Record<string, string>>;
35
+ elements: Readonly<Record<string, ElementEntry>>;
36
+ }
37
+
38
+ /** The language every lookup falls back to, and the catalog that is complete by contract. */
39
+ export const SOURCE_LOCALE = "en";
40
+
41
+ /** The source catalog, read through the open shape every other catalog has. */
42
+ const SOURCE: ElementCatalog = en;
43
+
44
+ /**
45
+ * Every catalog Studio ships, keyed by BCP 47 language tag. Adding a language is a new
46
+ * `data/locales/<tag>/elements.json` and one entry here.
47
+ */
48
+ const CATALOGS = new Map([[SOURCE_LOCALE, SOURCE]]);
49
+
50
+ /**
51
+ * The best catalog for a list of preferred languages, most preferred first.
52
+ *
53
+ * Each preference is tried whole and then by its base language before the next one is, because a
54
+ * reader who asked for Canadian French first wants French before their second choice. The match is
55
+ * case-insensitive, as BCP 47 tags are.
56
+ *
57
+ * @param {readonly string[]} preferred - Language tags, most preferred first
58
+ * @param {readonly string[]} available - The tags a catalog exists for
59
+ * @returns {string} One of `available`, or {@link SOURCE_LOCALE}
60
+ */
61
+ export function resolveLocale(
62
+ preferred: readonly string[],
63
+ available: readonly string[] = [...CATALOGS.keys()],
64
+ ): string {
65
+ const byLower = new Map(available.map((tag) => [tag.toLowerCase(), tag]));
66
+ for (const tag of preferred) {
67
+ const lower = tag.toLowerCase();
68
+ const match = byLower.get(lower) ?? byLower.get(lower.split("-")[0] ?? "");
69
+ if (match) {
70
+ return match;
71
+ }
72
+ }
73
+ return SOURCE_LOCALE;
74
+ }
75
+
76
+ /**
77
+ * The browser's preferred languages, or none where there is no `navigator` to ask. Anything that is
78
+ * not a non-empty string is dropped: an embedder or a privacy setting can blank the answer, and a
79
+ * reader with no stated language gets English rather than an exception.
80
+ */
81
+ function preferredLanguages(): readonly string[] {
82
+ if (typeof navigator === "undefined") {
83
+ return [];
84
+ }
85
+ /* SAFETY: the DOM typings promise both members, but a navigator outside a browser window (Bun's
86
+ own, a worker's in some engines) lacks `languages`, so each is read as possibly absent and
87
+ checked before use. */
88
+ const { language, languages } = navigator as Partial<Pick<Navigator, "language" | "languages">>;
89
+ const listed: readonly unknown[] = languages && languages.length > 0 ? languages : [language];
90
+ return listed.filter((tag): tag is string => typeof tag === "string" && tag !== "");
91
+ }
92
+
93
+ /** The language Studio names elements in for this reader. */
94
+ export function studioLocale(): string {
95
+ return resolveLocale(preferredLanguages());
96
+ }
97
+
98
+ /** The catalog for `locale`, or the source catalog when there is none. */
99
+ function catalog(locale: string): ElementCatalog {
100
+ return CATALOGS.get(locale) ?? SOURCE;
101
+ }
102
+
103
+ /**
104
+ * The name of the element `tag`: the catalog's, else the English one, else the tag itself.
105
+ *
106
+ * @param {string} tag
107
+ * @param {string} [locale]
108
+ */
109
+ export function elementName(tag: string, locale: string = studioLocale()): string {
110
+ return catalog(locale).elements[tag]?.name ?? SOURCE.elements[tag]?.name ?? tag;
111
+ }
112
+
113
+ /**
114
+ * What the element `tag` is for, in one line, or `""` when no catalog says.
115
+ *
116
+ * @param {string} tag
117
+ * @param {string} [locale]
118
+ */
119
+ export function elementDescription(tag: string, locale: string = studioLocale()): string {
120
+ return catalog(locale).elements[tag]?.description ?? SOURCE.elements[tag]?.description ?? "";
121
+ }
122
+
123
+ /**
124
+ * The name of the palette category `key` (§5.3), else the English one, else the key.
125
+ *
126
+ * @param {string} key
127
+ * @param {string} [locale]
128
+ */
129
+ export function categoryName(key: string, locale: string = studioLocale()): string {
130
+ return catalog(locale).categories[key] ?? SOURCE.categories[key] ?? key;
131
+ }
package/src/panels/dnd.ts CHANGED
@@ -41,6 +41,7 @@ import type { JxMutableNode } from "@jxsuite/schema/types";
41
41
  import type { ComponentEntry } from "../files/components.js";
42
42
  import type { ElementDragPayload } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
43
43
  import type { DropTargetRecord } from "@atlaskit/pragmatic-drag-and-drop/types";
44
+ import { elementName } from "../locale/element-names";
44
45
 
45
46
  interface DragCanDragArgs {
46
47
  element: HTMLElement;
@@ -314,7 +315,8 @@ export function registerElementsDnD() {
314
315
  const preview = row.querySelector(".element-card-preview");
315
316
  if (preview && !preview.firstChild) {
316
317
  const el = document.createElement(unsafeTags.has(tag) ? "span" : tag);
317
- el.textContent = tag;
318
+ // The specimen reads the card's own name, so a heading previews as "Heading 1", not "h1".
319
+ el.textContent = elementName(tag);
318
320
  preview.append(el);
319
321
  }
320
322
  const def = defaultDef(tag);
@@ -1,6 +1,9 @@
1
1
  /// <reference lib="dom" />
2
2
  /**
3
- * Elements panel — the Insert palette: the blocks and components a page is built out of.
3
+ * Elements panel — the Insert palette: the blocks and components a page is built out of, on two
4
+ * tabs, Elements and Components (§5.3). Each element card is named in the reader's language
5
+ * (`locale/element-names.ts`), with its tag beside the name, and the categories lead with what a
6
+ * first page is made of: text, lists, media and layout before forms, tables and inline formatting.
4
7
  *
5
8
  * **The body is a Jx document** (`surfaces/panel-elements.json`, mounted by
6
9
  * `surfaces/panel-elements.ts`), so `render` draws nothing and `afterRender` mounts. What stays
@@ -30,6 +33,7 @@ import { mountElementsSurface } from "../surfaces/panel-elements";
30
33
  import { mutateInsertNode, transactDoc } from "../tabs/transact";
31
34
  import { primarySelection } from "../tabs/selection";
32
35
  import { registerPanel } from "./panel-registry";
36
+ import { categoryName, elementDescription, elementName } from "../locale/element-names";
33
37
  import { activeTab } from "../workspace/workspace";
34
38
  import { view } from "../view";
35
39
 
@@ -38,8 +42,10 @@ import type {
38
42
  ComponentCardView,
39
43
  ElementCategoryView,
40
44
  ElementsActions,
45
+ ElementsEmptyState,
41
46
  ElementsSurfaceHandle,
42
47
  ElementsValues,
48
+ InsertTab,
43
49
  } from "../surfaces/panel-elements";
44
50
  import type { JxElement, JxMutableNode } from "@jxsuite/schema/types";
45
51
  import type { NavigatorPanelContext, NavigatorPanelDeps, PanelBody } from "./panel-registry";
@@ -49,8 +55,13 @@ interface ElementsWebdata {
49
55
  elements?: Record<string, { tag: string }[]>;
50
56
  }
51
57
 
52
- /** The Components section's name — its accordion label AND its key in `view.elementsCollapsed`. */
53
- const COMPONENTS = "Components";
58
+ /**
59
+ * The categories that start closed: inline formatting and the advanced remainder (§5.3). Both are
60
+ * long, and neither is where somebody building their first page looks, so a newcomer sees the
61
+ * everyday blocks first and opens these when they want them. A filter opens them too, unless the
62
+ * reader has closed one by hand.
63
+ */
64
+ const CLOSED_AT_FIRST: ReadonlySet<string> = new Set(["inline", "advanced"]);
54
65
 
55
66
  /**
56
67
  * What the palette is built from, held at module scope rather than closed over at mount.
@@ -76,18 +87,37 @@ function needle(): string {
76
87
  return view.elementsFilter.toLowerCase();
77
88
  }
78
89
 
79
- /** The categories that still have something in them, with the filter applied. */
90
+ /** Whether a section is drawn open: the reader's own choice, else its default. */
91
+ function sectionOpen(key: string, filter: string): boolean {
92
+ return view.elementsOpen.get(key) ?? (filter !== "" || !CLOSED_AT_FIRST.has(key));
93
+ }
94
+
95
+ /**
96
+ * The categories that still have something in them, with the filter applied, in the order
97
+ * `webdata.elements` declares them (§5.3). A card matches on its name in the reader's language or
98
+ * on its tag, so "image" and "img" both find the same card.
99
+ */
80
100
  function categoryViews(webdata: ElementsWebdata, filter: string): ElementCategoryView[] {
81
101
  const sections: ElementCategoryView[] = [];
82
- for (const [name, elements] of Object.entries(webdata.elements ?? {})) {
83
- const matched = filter ? elements.filter((entry) => entry.tag.includes(filter)) : elements;
102
+ for (const [key, elements] of Object.entries(webdata.elements ?? {})) {
103
+ const cards = elements.map(({ tag }) => ({
104
+ description: elementDescription(tag),
105
+ name: elementName(tag),
106
+ tag,
107
+ }));
108
+ const matched = filter
109
+ ? cards.filter(
110
+ (card) => card.tag.includes(filter) || card.name.toLowerCase().includes(filter),
111
+ )
112
+ : cards;
84
113
  if (matched.length === 0) {
85
114
  continue;
86
115
  }
87
116
  sections.push({
88
- elements: matched.map(({ tag }) => ({ tag })),
89
- name,
90
- open: !view.elementsCollapsed.has(name),
117
+ elements: matched,
118
+ key,
119
+ name: categoryName(key),
120
+ open: sectionOpen(key, filter),
91
121
  });
92
122
  }
93
123
  return sections;
@@ -145,23 +175,51 @@ function componentViews(filter: string): ComponentCardView[] {
145
175
  }));
146
176
  }
147
177
 
178
+ /** Which body a tab shows: its cards, or why it has none. */
179
+ function emptyState(count: number, filter: string): ElementsEmptyState {
180
+ if (count > 0) {
181
+ return "none";
182
+ }
183
+ return filter ? "filtered" : "empty";
184
+ }
185
+
186
+ /** The other tab, which a filtered-out tab offers to switch to. */
187
+ function otherTab(tab: InsertTab): InsertTab {
188
+ return tab === "elements" ? "components" : "elements";
189
+ }
190
+
191
+ /** "Show 1 matching component", "Show 3 matching elements": the other tab's offer. */
192
+ function otherMatchesLabel(tab: InsertTab, count: number): string {
193
+ const noun = tab === "elements" ? "element" : "component";
194
+ return `Show ${count} matching ${noun}${count === 1 ? "" : "s"}`;
195
+ }
196
+
148
197
  /** Everything the document draws, as one value. */
149
198
  function panelValues(source: NavigatorPanelDeps): ElementsValues {
150
199
  const filter = needle();
200
+ const tab = view.insertTab;
151
201
  /* SAFETY: `webdata` is wired in studio.ts from the bundled data/webdata.json, whose `elements`
152
- maps each category name to an array of `{ tag }` entries. */
202
+ maps each category key to an array of `{ tag }` entries. */
153
203
  const categories = categoryViews(source.webdata as ElementsWebdata, filter);
154
204
  const components = componentViews(filter);
155
- const bare = categories.length === 0 && components.length === 0;
205
+ const elementCount = categories.reduce((sum, section) => sum + section.elements.length, 0);
206
+ const counts: Record<InsertTab, number> = {
207
+ components: components.length,
208
+ elements: elementCount,
209
+ };
210
+ const other = otherTab(tab);
211
+ const showOffer = filter !== "" && counts[tab] === 0 && counts[other] > 0;
156
212
  return {
157
213
  categories,
158
214
  components,
159
- componentsOpen: !view.elementsCollapsed.has(COMPONENTS),
160
- emptyState: bare ? (filter ? "filtered" : "empty") : "none",
215
+ componentsEmpty: emptyState(components.length, filter),
216
+ elementsEmpty: emptyState(elementCount, filter),
161
217
  /* The reader's own text, not `filter`: the field is bound to this, and a scope holding the
162
218
  lower-cased form would rewrite what was typed on every keystroke. */
163
219
  filter: view.elementsFilter,
164
- hasComponents: components.length > 0,
220
+ filterLabel: tab === "elements" ? "Filter elements" : "Filter components",
221
+ otherMatches: showOffer ? otherMatchesLabel(other, counts[other]) : "",
222
+ tab,
165
223
  };
166
224
  }
167
225
 
@@ -207,6 +265,17 @@ function refresh(): void {
207
265
 
208
266
  /** What the document's controls do. Every one of them is a decision this module owns. */
209
267
  const ACTIONS: ElementsActions = {
268
+ /* The strip announces any value its tabs carry; only the two this panel draws are taken. */
269
+ selectTab: (tab) => {
270
+ if (tab === "elements" || tab === "components") {
271
+ view.insertTab = tab;
272
+ refresh();
273
+ }
274
+ },
275
+ showOtherTab: () => {
276
+ view.insertTab = otherTab(view.insertTab);
277
+ refresh();
278
+ },
210
279
  clearFilter: () => {
211
280
  view.elementsFilter = "";
212
281
  refresh();
@@ -223,31 +292,20 @@ const ACTIONS: ElementsActions = {
223
292
  insertAtSelection(panelDeps.defaultDef(tag) as JxMutableNode);
224
293
  }
225
294
  },
226
- setComponentsOpen: (open) => {
227
- setCollapsed(COMPONENTS, !open);
228
- refresh();
229
- },
230
295
  /* The echo: what the field now holds goes into the state the palette is filtered by, before
231
296
  anything is decided about it. */
232
297
  setFilter: (value) => {
233
298
  view.elementsFilter = value;
234
299
  refresh();
235
300
  },
236
- setSectionOpen: (name, open) => {
237
- setCollapsed(name, !open);
301
+ /* Remembered across repaints and panel switches, and from then on it outranks the section's
302
+ default, the filter's included. */
303
+ setSectionOpen: (key, open) => {
304
+ view.elementsOpen.set(key, open);
238
305
  refresh();
239
306
  },
240
307
  };
241
308
 
242
- /** Remember a section's disclosure across repaints and across panel switches. */
243
- function setCollapsed(name: string, collapsed: boolean): void {
244
- if (collapsed) {
245
- view.elementsCollapsed.add(name);
246
- } else {
247
- view.elementsCollapsed.delete(name);
248
- }
249
- }
250
-
251
309
  /**
252
310
  * Draw the panel body — mounting the document the first time, updating it every time after.
253
311
  *
@@ -277,13 +335,13 @@ export function mountElementsPanel(ctx: NavigatorPanelContext, host: HTMLElement
277
335
  }
278
336
 
279
337
  /**
280
- * Contribute the Insert panel — **off the rail** (`rail: false`).
338
+ * Contribute the Insert panel — the first button of the rail's DOCUMENT group (§5.1).
281
339
  *
282
- * §5.1 keeps Insert off the Navigator rail because it is not a view of anything: it is an insert
283
- * palette, and a palette belongs at the caret (slash menu), on the canvas (`+`) and behind ⌘⇧A, all
284
- * of which are the Insert command family. The record survives so the surface stays reachable —
285
- * `view.setActivity {tab:"insert"}`, the palette, and the screenshot pipeline all still address it
286
- * — and giving up its rail slot is what keeps the DOCUMENT group at four.
340
+ * It is the place an author picks an element or component from and drags it onto the canvas, so it
341
+ * stays on screen while they work: a card is a choice to look at, compare and drag, and a palette
342
+ * that has to be summoned by name before it can be seen is one only a reader of the command list
343
+ * finds. The slash menu and the canvas `+` stay the inserts at the caret; this is the one an author
344
+ * browses.
287
345
  *
288
346
  * `level: "document"`, per §13.2's own worked example: it READS the project's component registry
289
347
  * and WRITES the document tree.
@@ -295,7 +353,6 @@ export function registerInsertPanel(): void {
295
353
  level: "document",
296
354
  dock: "navigator",
297
355
  icon: "grid-four",
298
- rail: false,
299
356
  // The body is a document, so lit draws nothing and the mount happens against the painted DOM.
300
357
  // `afterRender` runs on every repaint; {@link mountElementsPanel} is idempotent.
301
358
  render: (): PanelBody => nothing,
@@ -50,6 +50,7 @@ import type {
50
50
  } from "../surfaces/panel-imports";
51
51
  import type { JxMutableNode } from "@jxsuite/schema/types";
52
52
  import type { SurfaceOption } from "../ui/surface";
53
+ import { isProjectConfigPath } from "@jxsuite/schema/project-file";
53
54
 
54
55
  /** What the panel is drawn against. `left-panel.ts`'s context, reduced to what this reads. */
55
56
  export interface ImportsContext {
@@ -204,7 +205,7 @@ function documentView(ctx: ImportsContext): ImportsView {
204
205
 
205
206
  /** What the panel shows right now. `project.json` is the one document with the other mood. */
206
207
  function view(ctx: ImportsContext): ImportsView {
207
- return ctx.documentPath?.endsWith("project.json") ? siteView() : documentView(ctx);
208
+ return isProjectConfigPath(ctx.documentPath) ? siteView() : documentView(ctx);
208
209
  }
209
210
 
210
211
  /** Push the current projection into the standing surface, in this turn. */
@@ -295,7 +296,7 @@ async function onToggleComponent(id: string, checked: boolean): Promise<void> {
295
296
  if (!ctx || !comp) {
296
297
  return;
297
298
  }
298
- if (ctx.documentPath?.endsWith("project.json")) {
299
+ if (isProjectConfigPath(ctx.documentPath)) {
299
300
  const before = siteElements();
300
301
  const updated = checked
301
302
  ? enableElement(before, comp, null)
@@ -8,9 +8,9 @@
8
8
  * the second definition site the registry exists to prevent.
9
9
  *
10
10
  * **Registration order is rail order** within a level group: PROJECT (Site, then Files with the
11
- * developer panels on) above the divider, DOCUMENT (Outline · Data, then Packages with the
12
- * developer panels on) below it (§5.1). Search, Languages and Insert are registered rail-less:
13
- * reachable by `view.setActivity` and the palette, with no button.
11
+ * developer panels on) above the divider, DOCUMENT (Insert · Outline · Data, then Packages with the
12
+ * developer panels on) below it (§5.1). Search and Languages are registered rail-less: reachable by
13
+ * `view.setActivity` and the palette, with no button.
14
14
  *
15
15
  * **Every rail button opens the Navigator.** Problems used to hold the fourth PROJECT slot while
16
16
  * its body was drawn in the Bottom dock, and paying for that took a per-dock branch in three
@@ -84,13 +84,13 @@ export function registerNavigatorPanels(): void {
84
84
  registerI18nPanel();
85
85
 
86
86
  // ── DOCUMENT group ──────────────────────────────────────────────────────────
87
+ /* Insert leads the group: it is where a page's elements and components are picked from and
88
+ dragged onto the canvas, the verb a Design session is mostly made of (§5.1, §5.3). */
89
+ registerInsertPanel();
87
90
  registerLayersPanel();
88
91
  registerDataPanel();
89
92
  registerPackagesPanel();
90
93
 
91
- // ── Off-rail: reachable by command and palette, no rail button ──────────────
92
- registerInsertPanel();
93
-
94
94
  // ── The Bottom dock's tabs (§16.3): Problems · Logic · Activity ──────────────
95
95
  // Composed here, in the one place panel records are gathered, so the shell has ONE composition
96
96
  // Site rather than one per dock. Importing the module is also what attaches the dock to the
@@ -122,10 +122,10 @@ export function navigatorPanelSet(): readonly PanelRecord[] {
122
122
  * The roster `panel.focus.*` is generated from: rail order first, then the rail-less Navigator
123
123
  * panels.
124
124
  *
125
- * Not `railPanelSet()`: Insert and Languages have no rail button and no chord, and they still need
126
- * a `Show …` command and a palette row, which is the whole point of `rail: false` (a surface
127
- * reachable by name, not by number). State used to be the third of them and is gone: its editor is
128
- * the Data panel's now, so there is no second record to roster.
125
+ * Not `railPanelSet()`: Languages has no rail button and no chord, and it still needs a `Show …`
126
+ * command and a palette row, which is the whole point of `rail: false` (a surface reachable by
127
+ * name, not by number). State and Insert were rail-less beside it once: State's editor is the Data
128
+ * panel's now, and Insert is back on the rail (§5.1).
129
129
  *
130
130
  * The Bottom dock's three tabs are deliberately absent — Problems as of this change, and Logic and
131
131
  * Activity all along. Each is `rail: false` AND hosted there, so each is addressed by
@@ -153,9 +153,9 @@ export interface PanelRecord {
153
153
  *
154
154
  * Not a hiding mechanism — a rail-less panel is still reachable by `view.setActivity`, by the
155
155
  * palette and by another surface linking to it. It is how a record survives the interval between
156
- * losing its rail slot and gaining its real home (Insert, reachable from the canvas and palette;
157
- * State, folded into Data — §5.1). Declaring it `false` also removes the panel's `rail/<level>`
158
- * placement, which is what keeps the rail inside its four-per-group budget.
156
+ * losing its rail slot and gaining its real home (State, folded into Data — §5.1). Declaring it
157
+ * `false` also removes the panel's `rail/<level>` placement, which is what keeps the rail inside
158
+ * its four-per-group budget.
159
159
  *
160
160
  * `"developer"` is a rail button only while **Show developer panels** is on (§5.1,
161
161
  * `settings/developer-panels.ts`): Files and Packages, which a site author does not need on the
@@ -71,6 +71,7 @@ import type {
71
71
  SiteSectionState,
72
72
  } from "../surfaces/site-panel";
73
73
  import type { SiteComponentRow, SiteMediaRow, SitePageFact } from "./site-model";
74
+ import { isProjectConfigPath } from "@jxsuite/schema/project-file";
74
75
 
75
76
  // ─── The project index ───────────────────────────────────────────────────────
76
77
 
@@ -213,7 +214,7 @@ export function mayDefineComponent(path: string): boolean {
213
214
  }
214
215
  return (
215
216
  path.endsWith(".json") &&
216
- path !== "project.json" &&
217
+ !isProjectConfigPath(path) &&
217
218
  !NON_COMPONENT_DIRS.some((dir) => path.startsWith(dir))
218
219
  );
219
220
  }
@@ -54,6 +54,7 @@ import {
54
54
  isNumber,
55
55
  isString,
56
56
  } from "@jxsuite/schema/guards";
57
+ import { PROJECT_FILE_NAME } from "@jxsuite/schema/project-file";
57
58
  import { effect, effectScope, reactive } from "../reactivity";
58
59
  import {
59
60
  cancelStyleDebounce,
@@ -374,7 +375,7 @@ function stylebookTagOf(selector: string | null): string | null {
374
375
  * elements of its own for "in this document" to be about.
375
376
  */
376
377
  function isProjectStylesheet(path: string): boolean {
377
- return /(?:^|\/)project\.json$/.test(path);
378
+ return path.split("/").pop() === PROJECT_FILE_NAME;
378
379
  }
379
380
 
380
381
  /**
@@ -34,7 +34,7 @@ import { effect, effectScope } from "../reactivity";
34
34
  import { projectState } from "../store";
35
35
  import { setDockCollapsed, shell } from "../shell";
36
36
  import { workspace } from "../workspace/workspace";
37
- import { PROJECT_CONFIG_PATH } from "../tabs/tab";
37
+ import { projectConfigPath } from "../tabs/tab";
38
38
  import { componentRegistry } from "../files/components";
39
39
  import { runActiveReported } from "../commands/run-reported";
40
40
  import { showSettingsDocument } from "../settings/settings-document";
@@ -88,7 +88,7 @@ export interface StylesPanelInput {
88
88
  * opened with.
89
89
  */
90
90
  export function stylesConfig(): StylesPanelInput["config"] {
91
- const tab = workspace.tabs.get(PROJECT_CONFIG_PATH);
91
+ const tab = workspace.tabs.get(projectConfigPath());
92
92
  return (tab ? tab.doc.document : projectState?.projectConfig) ?? null;
93
93
  }
94
94
 
@@ -20,6 +20,7 @@ import type { WsCollabConnection } from "@jxsuite/collab/client";
20
20
  import { isBoolean, isJsonObject, isNonNullObject, isString } from "@jxsuite/schema/guards";
21
21
  import type { JsonObject, JsonValue, JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
22
22
  import { componentMetaFrom } from "@jxsuite/schema/component-meta";
23
+ import { isJxnPath, isNativeDocumentPath, parseJxn } from "@jxsuite/schema/jxn";
23
24
  import { streamImport } from "../services/import-client";
24
25
  import type {
25
26
  AccountEntry,
@@ -1403,7 +1404,7 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
1403
1404
  if (!SKIP.has(entry.name) && !entry.name.startsWith(".")) {
1404
1405
  dirs.push(entry.path);
1405
1406
  }
1406
- } else if (entry.name.endsWith(".json") && jsonPaths.length < MAX_FILES) {
1407
+ } else if (isNativeDocumentPath(entry.name) && jsonPaths.length < MAX_FILES) {
1407
1408
  jsonPaths.push(entry.path);
1408
1409
  }
1409
1410
  }
@@ -1414,7 +1415,9 @@ export function createCloudPlatform(project: CloudProject | null): StudioPlatfor
1414
1415
  const metas = await Promise.all(
1415
1416
  jsonPaths.map(async (path) => {
1416
1417
  try {
1417
- return componentMetaFrom(parseJson(await platform.readFile(path)), path);
1418
+ const text = await platform.readFile(path);
1419
+ const document = isJxnPath(path) ? parseJxn(text) : parseJson(text);
1420
+ return componentMetaFrom(isJsonObject(document) ? document : null, path);
1418
1421
  } catch {
1419
1422
  return null; // Not JSON, not readable, or not a component — all the same answer here.
1420
1423
  }
@@ -84,6 +84,7 @@ import type { CommandContext } from "../commands/context";
84
84
  import type { AiToolReport, AnyCommand, CommandRegistry } from "../commands/registry";
85
85
  import type { Tab } from "../tabs/tab";
86
86
  import { runAsActor } from "../tabs/transact";
87
+ import { projectConfigPath } from "../tabs/tab";
87
88
 
88
89
  /**
89
90
  * The record a selection-level projection composes through.
@@ -328,7 +329,7 @@ function defaultPaths(command: AnyCommand, tab: Tab | null): string[] {
328
329
  return [tab?.documentPath ?? "(untitled)"];
329
330
  }
330
331
  if (command.undo === "project") {
331
- return ["project.json"];
332
+ return [projectConfigPath()];
332
333
  }
333
334
  return ["(unknown file)"];
334
335
  }
@@ -17,6 +17,7 @@
17
17
  import { JX_TOOL_FACTS } from "@jxsuite/ai/jx-tools";
18
18
  import { createToolDefinition } from "@jxsuite/ai/tools";
19
19
  import { isJsonObject, isNumber } from "@jxsuite/schema/guards";
20
+ import { classifyJxn, isJxnPath, isNativeDocumentPath, parseJxn } from "@jxsuite/schema/jxn";
20
21
  import type { CreateProjectDestination, StudioPlatform } from "../types";
21
22
  import type { ToolRegistry } from "@jxsuite/ai/tools";
22
23
  import type { JsonValue, JxMutableNode, ProjectConfig } from "@jxsuite/schema/types";
@@ -30,6 +31,7 @@ import { validateDoc, validateProjectConfig } from "./jx-validate";
30
31
  import { flagHardcodedTokens, formatTokenHints } from "./token-lint";
31
32
  import type { CandidateDocument, RenderProbe } from "./ai-write-report";
32
33
  import { invalidPathResult, normalizeRelPath } from "./project-path";
34
+ import { isProjectConfigPath } from "@jxsuite/schema/project-file";
33
35
 
34
36
  /** Directories the file tools never descend into or report. */
35
37
  const EXCLUDED_DIRS = new Set(["node_modules", "dist", ".git", ".jx-cache"]);
@@ -49,7 +51,7 @@ const NOT_UNDOABLE = "(saved to disk; not undoable with Cmd+Z)";
49
51
  * {@link looksLikeJxDoc} sniff.
50
52
  */
51
53
  function isJxDocPath(path: string): boolean {
52
- return /^(pages|layouts|components|elements)\//.test(path) && path.endsWith(".json");
54
+ return /^(pages|layouts|components|elements)\//.test(path) && isNativeDocumentPath(path);
53
55
  }
54
56
 
55
57
  /** Structural sniff for Jx-document-shaped JSON values. */
@@ -299,8 +301,19 @@ export function registerProjectTools(
299
301
 
300
302
  // Pre-validate Jx documents — disk writes have no undo to lean on.
301
303
  let parsed: JsonValue | undefined;
302
- const isJson = relPath.endsWith(".json") && relPath !== "project.json";
303
- if (isJson) {
304
+ const isJxn = isJxnPath(relPath) && classifyJxn(content) !== "project";
305
+ const isJson = relPath.endsWith(".json") && !isProjectConfigPath(relPath);
306
+ if (isJxn) {
307
+ try {
308
+ const document = parseJxn(content);
309
+ parsed = isJsonObject(document) ? document : null;
310
+ } catch (error) {
311
+ return {
312
+ success: false,
313
+ error: `Content is not valid Jx Notation: ${error instanceof Error ? error.message : String(error)}`,
314
+ };
315
+ }
316
+ } else if (isJson) {
304
317
  try {
305
318
  // SAFETY: `JSON.parse` without a reviver yields nothing but JSON values.
306
319
  parsed = JSON.parse(content) as JsonValue;
@@ -312,7 +325,7 @@ export function registerProjectTools(
312
325
  }
313
326
  }
314
327
  let tokenHints = "";
315
- // The parse above sets `parsed` exactly when `isJson` holds: it never yields `undefined`.
328
+ // The parse above sets `parsed` exactly when one of those holds: it never yields `undefined`.
316
329
  if (parsed !== undefined && (isJxDocPath(relPath) || looksLikeJxDoc(parsed))) {
317
330
  const errors = await validate(parsed);
318
331
  if (errors.length > 0) {
@@ -345,7 +358,7 @@ export function registerProjectTools(
345
358
  /* Parsed as `unknown` because that is what it is until the schema says otherwise — this is
346
359
  model-authored text, and the gate below is the whole reason we do not trust its shape. */
347
360
  let projectConfig: ProjectConfig | undefined;
348
- if (relPath === "project.json") {
361
+ if (isProjectConfigPath(relPath)) {
349
362
  let parsedConfig: JsonValue;
350
363
  try {
351
364
  // SAFETY: `JSON.parse` without a reviver yields nothing but JSON values.