@jxsuite/studio 2.0.1 → 2.1.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 (419) hide show
  1. package/canvas.html +21 -0
  2. package/dist/chunks/Overlay.dev-qfjzkms8.js +23 -0
  3. package/dist/chunks/{Overlay.dev-cszx7yn5.js.map → Overlay.dev-qfjzkms8.js.map} +1 -1
  4. package/dist/chunks/{adapter-z8fkmbmv.js → adapter-qfnesn5v.js} +2 -2
  5. package/dist/chunks/{cf-settings-mmyv436d.js → cf-settings-73sh5z4p.js} +3 -3
  6. package/dist/chunks/{components-799jpvhm.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-799jpvhm.js.map → components-2bpqp4mq.js.map} +1 -1
  8. package/dist/chunks/content-source-34gnkbnn.js +38 -0
  9. package/dist/chunks/content-source-34gnkbnn.js.map +9 -0
  10. package/dist/chunks/convert-to-repeater-t8kxre5s.js +258 -0
  11. package/dist/chunks/convert-to-repeater-t8kxre5s.js.map +10 -0
  12. package/dist/chunks/extension-sections-6mqydz35.js +301 -0
  13. package/dist/chunks/extension-sections-6mqydz35.js.map +11 -0
  14. package/dist/chunks/files-8kn321h1.js +70 -0
  15. package/dist/chunks/files-8kn321h1.js.map +9 -0
  16. package/dist/chunks/{format-host-8jxwts2p.js → format-host-mkwsmt8d.js} +3 -3
  17. package/dist/chunks/git-panel-tjbr191d.js +51 -0
  18. package/dist/chunks/git-panel-tjbr191d.js.map +9 -0
  19. package/dist/chunks/{index-baeg2x3y.js → index-7pxchcce.js} +4 -3
  20. package/dist/chunks/{index-baeg2x3y.js.map → index-7pxchcce.js.map} +3 -3
  21. package/dist/chunks/{jx-validate-qx8degj7.js → jx-validate-v6qgknrs.js} +3 -3
  22. package/dist/chunks/layers-panel-87pb25km.js +58 -0
  23. package/dist/chunks/layers-panel-87pb25km.js.map +9 -0
  24. package/dist/chunks/{monaco-setup-zjdp9zdw.js → monaco-setup-e0yh25j4.js} +4 -75
  25. package/dist/chunks/{monaco-setup-zjdp9zdw.js.map → monaco-setup-e0yh25j4.js.map} +3 -4
  26. package/dist/chunks/publish-panel-m0fcgdrt.js +21 -0
  27. package/dist/chunks/publish-panel-m0fcgdrt.js.map +9 -0
  28. package/dist/chunks/quick-search-rhs6cbsb.js +53 -0
  29. package/dist/chunks/quick-search-rhs6cbsb.js.map +9 -0
  30. package/dist/chunks/right-panel-zpdxd98t.js +37 -0
  31. package/dist/chunks/right-panel-zpdxd98t.js.map +9 -0
  32. package/dist/chunks/section-registry-zmckaxf4.js +30 -0
  33. package/dist/chunks/section-registry-zmckaxf4.js.map +9 -0
  34. package/dist/chunks/sp-overlay.dev-9ran0sfq.js +18 -0
  35. package/dist/chunks/sp-overlay.dev-9ran0sfq.js.map +9 -0
  36. package/dist/chunks/sp-popover.dev-rebmdncr.js +9 -0
  37. package/dist/chunks/sp-popover.dev-rebmdncr.js.map +9 -0
  38. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js +15 -0
  39. package/dist/chunks/sp-progress-circle.dev-5vkppfkt.js.map +9 -0
  40. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js +15 -0
  41. package/dist/chunks/sp-tooltip.dev-ykqrg8h5.js.map +9 -0
  42. package/dist/chunks/{sp-tray.dev-pb2fsd65.js → sp-tray.dev-ey59vfh7.js} +10 -8
  43. package/dist/chunks/{sp-tray.dev-pb2fsd65.js.map → sp-tray.dev-ey59vfh7.js.map} +2 -2
  44. package/dist/chunks/{studio-9qr6fq7p.js → studio-0rqqhrdy.js} +10 -378
  45. package/dist/chunks/{studio-9qr6fq7p.js.map → studio-0rqqhrdy.js.map} +4 -14
  46. package/dist/chunks/{studio-5x6tqqec.js → studio-2sj1jj3v.js} +10 -8
  47. package/dist/chunks/{studio-5x6tqqec.js.map → studio-2sj1jj3v.js.map} +2 -2
  48. package/dist/chunks/{studio-h3fw0wn8.js → studio-3fk1ac9z.js} +6 -4
  49. package/dist/chunks/{studio-h3fw0wn8.js.map → studio-3fk1ac9z.js.map} +2 -2
  50. package/dist/chunks/studio-41exgzhq.js +6690 -0
  51. package/dist/chunks/studio-41exgzhq.js.map +38 -0
  52. package/dist/chunks/{studio-7v2bpweq.js → studio-4mqb9a0j.js} +6 -4
  53. package/dist/chunks/{studio-7v2bpweq.js.map → studio-4mqb9a0j.js.map} +2 -2
  54. package/dist/chunks/studio-50k77cca.js +1787 -0
  55. package/dist/chunks/studio-50k77cca.js.map +18 -0
  56. package/dist/chunks/{studio-56v3zwk6.js → studio-6a0h7sne.js} +10 -8
  57. package/dist/chunks/{studio-56v3zwk6.js.map → studio-6a0h7sne.js.map} +2 -2
  58. package/dist/chunks/studio-6f0am8f2.js +53 -0
  59. package/dist/chunks/studio-6f0am8f2.js.map +10 -0
  60. package/dist/chunks/studio-6nmjc5qk.js +7002 -0
  61. package/dist/chunks/studio-6nmjc5qk.js.map +32 -0
  62. package/dist/chunks/studio-7afd21nw.js +67 -0
  63. package/dist/chunks/studio-7afd21nw.js.map +10 -0
  64. package/dist/chunks/{studio-xze4zrs1.js → studio-7bmdvzne.js} +2 -2
  65. package/dist/chunks/studio-7bmdvzne.js.map +10 -0
  66. package/dist/chunks/studio-8jpzqk5s.js +128 -0
  67. package/dist/chunks/studio-8jpzqk5s.js.map +10 -0
  68. package/dist/chunks/studio-94m77tgk.js +1482 -0
  69. package/dist/chunks/studio-94m77tgk.js.map +13 -0
  70. package/dist/chunks/studio-9je6w5sq.js +1000 -0
  71. package/dist/chunks/studio-9je6w5sq.js.map +16 -0
  72. package/dist/chunks/{studio-w7c6kvk7.js → studio-a7pzveq1.js} +2 -2
  73. package/dist/chunks/{studio-gs6kqdt1.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-gs6kqdt1.js.map → studio-adn0z1k3.js.map} +3 -3
  75. package/dist/chunks/{studio-kr88ryr7.js → studio-bsxxx3x2.js} +17 -15
  76. package/dist/chunks/{studio-kr88ryr7.js.map → studio-bsxxx3x2.js.map} +7 -6
  77. package/dist/chunks/studio-bw5dz6km.js +608 -0
  78. package/dist/chunks/studio-bw5dz6km.js.map +21 -0
  79. package/dist/chunks/studio-c6s5getc.js +4232 -0
  80. package/dist/chunks/studio-c6s5getc.js.map +39 -0
  81. package/dist/chunks/studio-cjm7fdjf.js +451 -0
  82. package/dist/chunks/studio-cjm7fdjf.js.map +14 -0
  83. package/dist/chunks/{studio-cx91mnh8.js → studio-ckwz4d3x.js} +53 -53
  84. package/dist/chunks/{studio-cx91mnh8.js.map → studio-ckwz4d3x.js.map} +5 -5
  85. package/dist/chunks/studio-dg7zyt5x.js +28 -0
  86. package/dist/chunks/studio-dg7zyt5x.js.map +10 -0
  87. package/dist/chunks/studio-dzcyvvvz.js +166 -0
  88. package/dist/chunks/studio-dzcyvvvz.js.map +10 -0
  89. package/dist/chunks/{studio-7kqe41sa.js → studio-evkr5d3b.js} +15 -7
  90. package/dist/chunks/{studio-7kqe41sa.js.map → studio-evkr5d3b.js.map} +5 -4
  91. package/dist/chunks/studio-f12h6177.js +411 -0
  92. package/dist/chunks/studio-f12h6177.js.map +13 -0
  93. package/dist/chunks/studio-hbp1s8m9.js +1817 -0
  94. package/dist/chunks/studio-hbp1s8m9.js.map +14 -0
  95. package/dist/chunks/studio-k2sxjph5.js +15295 -0
  96. package/dist/chunks/studio-k2sxjph5.js.map +235 -0
  97. package/dist/chunks/studio-myje51t3.js +385 -0
  98. package/dist/chunks/studio-myje51t3.js.map +12 -0
  99. package/dist/chunks/studio-na6rwzhw.js +353 -0
  100. package/dist/chunks/studio-na6rwzhw.js.map +11 -0
  101. package/dist/chunks/{studio-4k0m4fmp.js → studio-p471gg5n.js} +126 -3
  102. package/dist/chunks/studio-p471gg5n.js.map +14 -0
  103. package/dist/chunks/{studio-9bjwf0d4.js → studio-rgwcpast.js} +5 -46
  104. package/dist/chunks/{studio-9bjwf0d4.js.map → studio-rgwcpast.js.map} +4 -5
  105. package/dist/chunks/{studio-esrewxap.js → studio-s519zc99.js} +3 -3
  106. package/dist/chunks/studio-sas7mt3k.js +75 -0
  107. package/dist/chunks/studio-sas7mt3k.js.map +10 -0
  108. package/dist/chunks/studio-t5wqwzdh.js +74 -0
  109. package/dist/chunks/studio-t5wqwzdh.js.map +10 -0
  110. package/dist/chunks/{studio-9qpspwfn.js → studio-temv5bw4.js} +5 -5
  111. package/dist/chunks/{studio-9qpspwfn.js.map → studio-temv5bw4.js.map} +1 -1
  112. package/dist/chunks/{studio-qkvt37c0.js → studio-tr20m8g8.js} +669 -18
  113. package/dist/chunks/studio-tr20m8g8.js.map +9 -0
  114. package/dist/chunks/{studio-v7jkjqng.js → studio-v96qfnww.js} +5514 -4291
  115. package/dist/chunks/studio-v96qfnww.js.map +54 -0
  116. package/dist/chunks/studio-vcetc91k.js +659 -0
  117. package/dist/chunks/studio-vcetc91k.js.map +10 -0
  118. package/dist/chunks/{studio-jm0z592w.js → studio-vmymjefp.js} +2972 -4937
  119. package/dist/chunks/studio-vmymjefp.js.map +98 -0
  120. package/dist/chunks/{studio-bghqfmea.js → studio-wwseqyw0.js} +2 -2
  121. package/dist/chunks/studio-x9hmy5kr.js +390 -0
  122. package/dist/chunks/studio-x9hmy5kr.js.map +10 -0
  123. package/dist/chunks/studio-xa0ce0n7.js +335 -0
  124. package/dist/chunks/studio-xa0ce0n7.js.map +10 -0
  125. package/dist/chunks/studio-xm3dbxvn.js +378 -0
  126. package/dist/chunks/studio-xm3dbxvn.js.map +19 -0
  127. package/dist/chunks/{studio-nqw433kv.js → studio-xmvbhg31.js} +2 -2
  128. package/dist/chunks/studio-zv37yrfa.js +1054 -0
  129. package/dist/chunks/studio-zv37yrfa.js.map +13 -0
  130. package/dist/chunks/{ws-client-q7hkz3j9.js → ws-client-654ts66j.js} +6 -4
  131. package/dist/chunks/ws-client-654ts66j.js.map +11 -0
  132. package/dist/iframe-entry.js +668 -230
  133. package/dist/studio.js +21080 -32585
  134. package/index.html +35 -4685
  135. package/package.json +17 -15
  136. package/src/about/about-modal.ts +42 -11
  137. package/src/browse/library-commands.ts +240 -0
  138. package/src/browse/library-layouts.ts +369 -0
  139. package/src/browse/library-model.ts +396 -0
  140. package/src/browse/library-pane.ts +1049 -0
  141. package/src/browse/library-preview.ts +252 -0
  142. package/src/browse/library-source.ts +150 -0
  143. package/src/canvas/canvas-helpers.ts +33 -9
  144. package/src/canvas/canvas-live-render.ts +45 -29
  145. package/src/canvas/canvas-patcher.ts +151 -31
  146. package/src/canvas/canvas-perf.ts +46 -7
  147. package/src/canvas/canvas-render.ts +1092 -220
  148. package/src/canvas/canvas-surface.ts +240 -0
  149. package/src/canvas/canvas-utils.ts +1166 -124
  150. package/src/canvas/iframe-drop.ts +2 -1
  151. package/src/canvas/iframe-entry.ts +241 -4
  152. package/src/canvas/iframe-host.ts +1226 -143
  153. package/src/canvas/iframe-inline-edit.ts +31 -0
  154. package/src/canvas/iframe-interaction.ts +76 -16
  155. package/src/canvas/iframe-keys.ts +107 -45
  156. package/src/canvas/iframe-overlay.ts +38 -1
  157. package/src/canvas/iframe-protocol.ts +137 -2
  158. package/src/canvas/iframe-render.ts +70 -2
  159. package/src/canvas/iframe-slash.ts +6 -0
  160. package/src/canvas/path-mapping.ts +56 -6
  161. package/src/canvas/surface-registry.ts +292 -0
  162. package/src/collab/collab-commands.ts +244 -0
  163. package/src/collab/collab-session.ts +198 -32
  164. package/src/collab/collab-state.ts +28 -2
  165. package/src/collab/presence-chips.ts +100 -9
  166. package/src/commands/active-registry.ts +42 -0
  167. package/src/commands/app-commands.ts +176 -0
  168. package/src/commands/budget.ts +158 -0
  169. package/src/commands/command-args.ts +253 -0
  170. package/src/commands/context.ts +387 -0
  171. package/src/commands/defaults.ts +580 -0
  172. package/src/commands/keymap.ts +571 -0
  173. package/src/commands/levels.ts +345 -0
  174. package/src/commands/live-context.ts +193 -0
  175. package/src/commands/reference.ts +227 -0
  176. package/src/commands/registry.ts +293 -0
  177. package/src/component-props.ts +3 -2
  178. package/src/content/draft-state.ts +106 -0
  179. package/src/content/entry-commands.ts +282 -0
  180. package/src/content/entry-editor.ts +327 -0
  181. package/src/content/entry-fields.ts +116 -0
  182. package/src/content/entry-model.ts +175 -0
  183. package/src/editor/canvas-context-menu.ts +17 -11
  184. package/src/editor/canvas-slash-bridge.ts +11 -4
  185. package/src/editor/context-menu.ts +804 -204
  186. package/src/editor/convert-to-component.ts +31 -10
  187. package/src/editor/convert-to-repeater.ts +3 -2
  188. package/src/editor/inline-edit-apply.ts +5 -5
  189. package/src/editor/inline-edit.ts +86 -50
  190. package/src/editor/insert-zone-action.ts +1 -1
  191. package/src/editor/shortcuts.ts +1043 -329
  192. package/src/editor/slash-menu.ts +1 -0
  193. package/src/files/elements.ts +166 -0
  194. package/src/files/file-ops.ts +178 -31
  195. package/src/files/files.ts +898 -268
  196. package/src/files/fs-events.ts +21 -0
  197. package/src/files/media-meta.ts +203 -0
  198. package/src/files/media-paths.ts +141 -0
  199. package/src/files/media-upload.ts +7 -3
  200. package/src/files/media-usage.ts +189 -0
  201. package/src/github/github-auth.ts +235 -33
  202. package/src/github/github-publish.ts +80 -21
  203. package/src/grid/grid-controller.ts +152 -12
  204. package/src/grid/grid-layout.ts +225 -20
  205. package/src/grid/grid-open.ts +120 -0
  206. package/src/grid/grid-panel.ts +554 -39
  207. package/src/grid/grid-source.ts +27 -1
  208. package/src/grid/redirects-grid.ts +599 -0
  209. package/src/grid/redirects.ts +398 -0
  210. package/src/grid/sources/content-source.ts +108 -2
  211. package/src/i18n/i18n-commands.ts +322 -0
  212. package/src/new-project/add-repo-modal.ts +8 -10
  213. package/src/new-project/import-tab.ts +7 -0
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +221 -245
  216. package/src/packages/jxsuite-update.ts +97 -48
  217. package/src/packages/pull-package-sync.ts +18 -9
  218. package/src/page-params.ts +2 -1
  219. package/src/panels/activity-bar.ts +152 -91
  220. package/src/panels/activity-panel.ts +481 -0
  221. package/src/panels/ai-chat/chat-markdown.ts +8 -3
  222. package/src/panels/ai-chat/chat-view.ts +245 -18
  223. package/src/panels/ai-chat/composer.ts +31 -5
  224. package/src/panels/ai-chat/sessions-view.ts +17 -10
  225. package/src/panels/ai-panel.ts +279 -53
  226. package/src/panels/block-action-bar.ts +1115 -224
  227. package/src/panels/bottom-dock.ts +337 -0
  228. package/src/panels/canvas-dnd-bridge.ts +37 -8
  229. package/src/panels/chat-panel.ts +29 -12
  230. package/src/panels/data-explorer.ts +254 -112
  231. package/src/panels/data-grid.ts +10 -4
  232. package/src/panels/dnd.ts +28 -51
  233. package/src/panels/editors.ts +434 -107
  234. package/src/panels/elements-panel.ts +66 -2
  235. package/src/panels/empty-state.ts +112 -0
  236. package/src/panels/events-panel.ts +596 -82
  237. package/src/panels/formula-workspace.ts +408 -218
  238. package/src/panels/frontmatter-fields.ts +51 -10
  239. package/src/panels/frontmatter-panel.ts +357 -79
  240. package/src/panels/git-panel.ts +375 -164
  241. package/src/panels/head-panel.ts +791 -85
  242. package/src/panels/i18n-panel.ts +592 -0
  243. package/src/panels/imports-panel.ts +95 -85
  244. package/src/panels/jump-bar.ts +584 -0
  245. package/src/panels/layers-panel.ts +1106 -328
  246. package/src/panels/left-panel.ts +109 -204
  247. package/src/panels/navigator-panels.ts +131 -0
  248. package/src/panels/overlays.ts +7 -2
  249. package/src/panels/pane-context.ts +1477 -0
  250. package/src/panels/pane-grid.ts +406 -0
  251. package/src/panels/panel-registry.ts +340 -0
  252. package/src/panels/panel-scheduler.ts +75 -0
  253. package/src/panels/problems-panel.ts +234 -0
  254. package/src/panels/properties-panel.ts +946 -1139
  255. package/src/panels/provenance.ts +258 -0
  256. package/src/panels/quick-search.ts +791 -122
  257. package/src/panels/right-panel.ts +228 -102
  258. package/src/panels/seo-modal.ts +395 -0
  259. package/src/panels/settings-pane.ts +203 -0
  260. package/src/panels/signals-panel.ts +354 -137
  261. package/src/panels/statement-editor.ts +41 -25
  262. package/src/panels/statusbar.ts +342 -106
  263. package/src/panels/style-panel.ts +974 -463
  264. package/src/panels/style-utils.ts +3 -3
  265. package/src/panels/stylebook-doc.ts +4 -2
  266. package/src/panels/stylebook-layers-panel.ts +3 -2
  267. package/src/panels/stylebook-panel.ts +51 -26
  268. package/src/panels/tab-strip.ts +1058 -78
  269. package/src/panels/target-line.ts +274 -0
  270. package/src/panels/toolbar.ts +631 -484
  271. package/src/panels/welcome-screen.ts +301 -198
  272. package/src/platform-errors.ts +26 -3
  273. package/src/platform.ts +96 -2
  274. package/src/platforms/cloud.ts +71 -8
  275. package/src/platforms/devserver.ts +92 -29
  276. package/src/publish/deploy-checklist.ts +383 -0
  277. package/src/publish/pages-service.ts +10 -1
  278. package/src/publish/publish-commands.ts +218 -0
  279. package/src/publish/publish-panel.ts +110 -16
  280. package/src/recent-projects.ts +3 -2
  281. package/src/services/a11y-report.ts +505 -0
  282. package/src/services/ai-models.ts +30 -1
  283. package/src/services/ai-project-tools.ts +29 -10
  284. package/src/services/ai-system-prompt.ts +53 -14
  285. package/src/services/ai-tools.ts +39 -3
  286. package/src/services/ai-writes.ts +154 -0
  287. package/src/services/announce.ts +97 -0
  288. package/src/services/automation.ts +424 -175
  289. package/src/services/cem-export.ts +9 -4
  290. package/src/services/clock.ts +61 -0
  291. package/src/services/document-assistant.ts +36 -8
  292. package/src/services/idle.ts +179 -0
  293. package/src/services/import-client.ts +26 -0
  294. package/src/services/monaco-buffer.ts +627 -0
  295. package/src/services/monaco-lazy.ts +36 -0
  296. package/src/services/notify.ts +271 -0
  297. package/src/services/profile.ts +155 -0
  298. package/src/services/references.ts +0 -0
  299. package/src/services/token-lint.ts +3 -2
  300. package/src/services/tool-executor.ts +53 -7
  301. package/src/services/trusted-types.ts +123 -0
  302. package/src/settings/contexts-section.ts +427 -0
  303. package/src/settings/contributed-section.ts +149 -11
  304. package/src/settings/css-vars-editor.ts +306 -349
  305. package/src/settings/defs-editor.ts +93 -34
  306. package/src/settings/dependencies-editor.ts +7 -7
  307. package/src/settings/extension-sections.ts +37 -5
  308. package/src/settings/general-settings.ts +204 -121
  309. package/src/settings/locales-section.ts +347 -0
  310. package/src/settings/preferences-accounts.ts +140 -0
  311. package/src/settings/preferences-dialog.ts +620 -0
  312. package/src/settings/preferences-keymap.ts +270 -0
  313. package/src/settings/project-sections.ts +260 -0
  314. package/src/settings/section-registry.ts +169 -0
  315. package/src/settings/settings-document.ts +385 -0
  316. package/src/shell.ts +1549 -0
  317. package/src/site-context.ts +45 -20
  318. package/src/state.ts +14 -275
  319. package/src/store.ts +88 -43
  320. package/src/studio.ts +790 -346
  321. package/src/style/live-preview.ts +36 -0
  322. package/src/style/project-styles.ts +349 -0
  323. package/src/style/token-ref.ts +123 -0
  324. package/src/tabs/patch-ops.ts +16 -4
  325. package/src/tabs/project-config.ts +416 -0
  326. package/src/tabs/selection.ts +323 -0
  327. package/src/tabs/tab.ts +182 -47
  328. package/src/tabs/transact.ts +209 -28
  329. package/src/types.ts +68 -13
  330. package/src/ui/ai-credentials-form.ts +39 -35
  331. package/src/ui/ai-managed-connect.ts +7 -1
  332. package/src/ui/dynamic-slot.ts +224 -81
  333. package/src/ui/expression-editor.ts +71 -63
  334. package/src/ui/field-input.ts +4 -6
  335. package/src/ui/field-row.ts +215 -38
  336. package/src/ui/form-controls.ts +194 -7
  337. package/src/ui/layers.ts +492 -69
  338. package/src/ui/media-picker.ts +115 -25
  339. package/src/ui/panel-resize.ts +109 -91
  340. package/src/ui/progress-modal.ts +131 -50
  341. package/src/ui/regions.ts +462 -0
  342. package/src/ui/schema-form.ts +229 -45
  343. package/src/ui/spectrum.ts +24 -8
  344. package/src/ui/timing.ts +49 -0
  345. package/src/ui/value-selector.ts +20 -0
  346. package/src/ui/value-source.ts +348 -0
  347. package/src/ui/virtual-window.ts +400 -0
  348. package/src/utils/edit-display.ts +3 -2
  349. package/src/utils/inherited-style.ts +67 -37
  350. package/src/view.ts +64 -78
  351. package/src/workspace/pane-derive.ts +1258 -0
  352. package/src/workspace/session.ts +317 -0
  353. package/src/workspace/workspace.ts +1345 -23
  354. package/styles/canvas.css +653 -0
  355. package/styles/forced-colors.css +106 -0
  356. package/styles/inspector.css +1454 -0
  357. package/styles/overlays.css +2704 -0
  358. package/styles/panels.css +1701 -0
  359. package/styles/shell.css +1423 -0
  360. package/styles/tokens.css +151 -0
  361. package/dist/chunks/Overlay.dev-cszx7yn5.js +0 -22
  362. package/dist/chunks/extension-sections-qr3drkkw.js +0 -531
  363. package/dist/chunks/extension-sections-qr3drkkw.js.map +0 -12
  364. package/dist/chunks/files-j8a1t06k.js +0 -50
  365. package/dist/chunks/files-j8a1t06k.js.map +0 -9
  366. package/dist/chunks/git-panel-xtq5z81x.js +0 -28
  367. package/dist/chunks/git-panel-xtq5z81x.js.map +0 -9
  368. package/dist/chunks/layers-panel-ancn2wpq.js +0 -26
  369. package/dist/chunks/layers-panel-ancn2wpq.js.map +0 -9
  370. package/dist/chunks/settings-modal-17ttr96g.js +0 -29
  371. package/dist/chunks/settings-modal-17ttr96g.js.map +0 -9
  372. package/dist/chunks/sp-overlay.dev-earn5as8.js +0 -17
  373. package/dist/chunks/sp-overlay.dev-earn5as8.js.map +0 -9
  374. package/dist/chunks/sp-popover.dev-frxtfejn.js +0 -8
  375. package/dist/chunks/sp-popover.dev-frxtfejn.js.map +0 -9
  376. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js +0 -14
  377. package/dist/chunks/sp-progress-circle.dev-7tvw34h8.js.map +0 -9
  378. package/dist/chunks/sp-tooltip.dev-a9fywesm.js +0 -21
  379. package/dist/chunks/sp-tooltip.dev-a9fywesm.js.map +0 -10
  380. package/dist/chunks/studio-0xgs7xrs.js +0 -429
  381. package/dist/chunks/studio-0xgs7xrs.js.map +0 -13
  382. package/dist/chunks/studio-4d8hzcz5.js +0 -3427
  383. package/dist/chunks/studio-4d8hzcz5.js.map +0 -26
  384. package/dist/chunks/studio-4k0m4fmp.js.map +0 -13
  385. package/dist/chunks/studio-9smfzqhc.js +0 -19
  386. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  387. package/dist/chunks/studio-jm0z592w.js.map +0 -115
  388. package/dist/chunks/studio-m2t4mdg5.js +0 -1854
  389. package/dist/chunks/studio-m2t4mdg5.js.map +0 -18
  390. package/dist/chunks/studio-n4p11dfy.js +0 -853
  391. package/dist/chunks/studio-n4p11dfy.js.map +0 -12
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-sjh1wt1a.js +0 -3744
  394. package/dist/chunks/studio-sjh1wt1a.js.map +0 -22
  395. package/dist/chunks/studio-v7jkjqng.js.map +0 -46
  396. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  397. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  398. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  399. package/dist/chunks/studio-yfcstdzy.js +0 -399
  400. package/dist/chunks/studio-yfcstdzy.js.map +0 -12
  401. package/dist/chunks/studio-zp7jncs8.js +0 -469
  402. package/dist/chunks/studio-zp7jncs8.js.map +0 -14
  403. package/dist/chunks/ws-client-q7hkz3j9.js.map +0 -11
  404. package/src/browse/browse-modal.ts +0 -69
  405. package/src/browse/browse.ts +0 -904
  406. package/src/canvas/canvas-diff.ts +0 -216
  407. package/src/canvas/nested-site-style.ts +0 -58
  408. package/src/new-project/design-fields.ts +0 -264
  409. package/src/new-project/templates.ts +0 -61
  410. package/src/panels/tab-bar.ts +0 -548
  411. package/src/settings/settings-modal.ts +0 -203
  412. /package/dist/chunks/{adapter-z8fkmbmv.js.map → adapter-qfnesn5v.js.map} +0 -0
  413. /package/dist/chunks/{cf-settings-mmyv436d.js.map → cf-settings-73sh5z4p.js.map} +0 -0
  414. /package/dist/chunks/{format-host-8jxwts2p.js.map → format-host-mkwsmt8d.js.map} +0 -0
  415. /package/dist/chunks/{jx-validate-qx8degj7.js.map → jx-validate-v6qgknrs.js.map} +0 -0
  416. /package/dist/chunks/{studio-w7c6kvk7.js.map → studio-a7pzveq1.js.map} +0 -0
  417. /package/dist/chunks/{studio-esrewxap.js.map → studio-s519zc99.js.map} +0 -0
  418. /package/dist/chunks/{studio-bghqfmea.js.map → studio-wwseqyw0.js.map} +0 -0
  419. /package/dist/chunks/{studio-nqw433kv.js.map → studio-xmvbhg31.js.map} +0 -0
@@ -0,0 +1,427 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Contexts — the ONE place a project's rendering contexts are defined (plan §4.2, §2 principle 5).
4
+ *
5
+ * A rendering context is anything the document can be resolved _under_: a size breakpoint, a colour
6
+ * scheme, a feature query. All three are the same thing on disk — one entry in `project.json`'s
7
+ * `$media` map — and until this section existed they were **defined in four places**, none of which
8
+ * said "context" and only one of which said "breakpoint":
9
+ *
10
+ * 1. The New Project wizard's four breakpoint presets, described by media-query direction;
11
+ * 2. Settings › General's "Breakpoints" field;
12
+ * 3. Properties › Media, which renders **only when the document root is selected** — so adding a
13
+ * breakpoint cost you your element selection, the Style tab's scope, and your place;
14
+ * 4. The CSS Variables editor's "Enable dark scheme" button, which appended `'--dark':
15
+ * '(prefers-color-scheme: dark)'` to `$media` **without ever using the word breakpoint**, so the
16
+ * one control that created a colour scheme was filed under variables.
17
+ *
18
+ * All four are gone. §2 principle 5 is why: **definition and selection are different levels.** The
19
+ * pane context bar's Rendering-context control _selects_ among these; its popover's footer is
20
+ * "Manage contexts…", which opens this section. Nothing here selects, and nothing there defines.
21
+ *
22
+ * **This section writes the project, not the document.** Per-document `$media` overlays still merge
23
+ * at render time (`site-context.ts`'s `getEffectiveMedia`) and the on-disk format is unchanged —
24
+ * what moved is the authoring surface, from a panel that required a selection to a settings section
25
+ * that requires only a project.
26
+ *
27
+ * Failures are surfaced, not swallowed: every write runs through {@link persistMedia}, which
28
+ * schema-validates the candidate `project.json` with `jx-validate` first (the human editing this
29
+ * file used to get **no validation at all** — that was wired to the AI's `write_project_config`
30
+ * alone), parks any rejection under the control that caused it, and re-renders. §7.1's third tier:
31
+ * a bad value belongs at its control, not in a toast that expires.
32
+ *
33
+ * @docs studio/projects/pages-layouts-components
34
+ */
35
+
36
+ import { html, render as litRender, nothing } from "lit-html";
37
+ import { live } from "lit-html/directives/live.js";
38
+ import { errorMessage } from "@jxsuite/schema/parse";
39
+ import { projectState } from "../store";
40
+ import { updateSiteConfig } from "../site-context";
41
+ import { validateProjectConfig } from "../services/jx-validate";
42
+ import { isSchemeQuery, schemeOfQuery } from "../utils/canvas-media";
43
+ import { mediaDisplayName } from "../panels/shared";
44
+
45
+ import type { ProjectConfig } from "@jxsuite/schema/types";
46
+
47
+ /** The `$media` key the base width lives under. Not a query — a number of CSS pixels. */
48
+ const BASE_KEY = "--";
49
+
50
+ /** What a new size breakpoint starts as. Narrow-first, because `--` is the widest canvas. */
51
+ const NEW_BREAKPOINT_QUERY = "(max-width: 768px)";
52
+
53
+ /** What a new feature query starts as — a real query, so the row is valid the moment it exists. */
54
+ const NEW_FEATURE_QUERY = "(prefers-reduced-motion: reduce)";
55
+
56
+ /** The three kinds of context, in the order they are defined and rendered. */
57
+ export type ContextKind = "size" | "scheme" | "feature";
58
+
59
+ /** One `$media` entry, classified. */
60
+ export interface ContextEntry {
61
+ key: string;
62
+ query: string;
63
+ kind: ContextKind;
64
+ }
65
+
66
+ /**
67
+ * Which kind of context an entry is.
68
+ *
69
+ * The classification is the same one the canvas makes (`utils/canvas-media.ts`) so a row defined
70
+ * here lands in the group the pane context bar will show it in — a size breakpoint gets a canvas
71
+ * width, a scheme query drives the Auto/Light/Dark segment, and everything else is a plain toggle.
72
+ * Deriving it from the query rather than storing it is what keeps the on-disk format unchanged.
73
+ *
74
+ * @param {string} query
75
+ * @returns {ContextKind}
76
+ */
77
+ export function contextKindOf(query: string): ContextKind {
78
+ if (isSchemeQuery(query)) {
79
+ return "scheme";
80
+ }
81
+ return /(?:min|max)-width:/.test(query) ? "size" : "feature";
82
+ }
83
+
84
+ /**
85
+ * Split a `$media` map into the base width and the three context groups.
86
+ *
87
+ * @param {Record<string, string> | undefined} media
88
+ * @returns {{ base: string; entries: ContextEntry[] }}
89
+ */
90
+ export function splitContexts(media?: Record<string, string> | undefined): {
91
+ base: string;
92
+ entries: ContextEntry[];
93
+ } {
94
+ const source = media ?? {};
95
+ const entries: ContextEntry[] = [];
96
+ for (const [key, raw] of Object.entries(source)) {
97
+ if (key === BASE_KEY) {
98
+ continue;
99
+ }
100
+ const query = String(raw);
101
+ entries.push({ key, kind: contextKindOf(query), query });
102
+ }
103
+ return { base: String(source[BASE_KEY] ?? ""), entries };
104
+ }
105
+
106
+ /**
107
+ * The `$media` key a typed name becomes: `Wide screen` → `--wide-screen`.
108
+ *
109
+ * Typing the leading dashes is allowed but never required — the dashes are a storage detail of the
110
+ * CSS custom-property namespace `$media` shares, and §8.4 says a definition surface may not make
111
+ * the user spell one.
112
+ *
113
+ * @param {string} name
114
+ * @returns {string}
115
+ */
116
+ export function contextKeyOf(name: string): string {
117
+ const slug = name
118
+ .trim()
119
+ .replace(/^-+/, "")
120
+ .toLowerCase()
121
+ .replaceAll(/[^a-z0-9]+/g, "-")
122
+ .replaceAll(/^-+|-+$/g, "");
123
+ return slug ? `--${slug}` : "";
124
+ }
125
+
126
+ // ─── Per-container error state ────────────────────────────────────────────────
127
+
128
+ /**
129
+ * Where an error belongs. A `$media` key addresses the row it came from; `"base"` is the base-width
130
+ * field and `"section"` is a whole-file write failure with no single guilty control.
131
+ */
132
+ type ErrorTarget = string;
133
+
134
+ /**
135
+ * The last failure, per rendered container.
136
+ *
137
+ * Keyed by container so two mounted copies (and two tests) never share a message — the same reason
138
+ * `general-settings.ts` does it. One at a time is deliberate: settings persist on change, so there
139
+ * is exactly one write in flight and exactly one thing that just went wrong.
140
+ */
141
+ const errors = new WeakMap<HTMLElement, { target: ErrorTarget; message: string }>();
142
+
143
+ /** Read the current failure for a container — exported for tests and for re-render helpers. */
144
+ export function contextsError(container: HTMLElement): { target: string; message: string } | null {
145
+ return errors.get(container) ?? null;
146
+ }
147
+
148
+ // ─── Render ───────────────────────────────────────────────────────────────────
149
+
150
+ /**
151
+ * Render Project Settings › Contexts into `container`.
152
+ *
153
+ * @param {HTMLElement} container
154
+ */
155
+ export function renderContextsSection(container: HTMLElement): void {
156
+ const config = (projectState?.projectConfig || {}) as ProjectConfig;
157
+ const media = (config.$media || {}) as Record<string, string>;
158
+ const shown = errors.get(container);
159
+ const { base, entries } = splitContexts(media);
160
+
161
+ /** Park a failure under one control without writing anything. */
162
+ const reject = (target: ErrorTarget, message: string) => {
163
+ errors.set(container, { message, target });
164
+ renderContextsSection(container);
165
+ };
166
+
167
+ /**
168
+ * Write a whole `$media` map, validating it first and surfacing whatever refuses it.
169
+ *
170
+ * Both failure modes are real and both used to be invisible here: the schema can refuse the
171
+ * shape, and the disk can refuse the write. The predecessor surfaces (`Properties › Media` and
172
+ * the CSS-variables "Enable dark scheme") did `void updateSiteConfig(...)` and dropped the
173
+ * rejection, so a failed write read as the field snapping back for no reason.
174
+ */
175
+ const persistMedia = async (next: Record<string, string>, target: ErrorTarget) => {
176
+ const candidate = { ...config, $media: next } as unknown;
177
+ let schemaErrors: string[] = [];
178
+ try {
179
+ schemaErrors = await validateProjectConfig(candidate);
180
+ } catch (error) {
181
+ /* A validator that cannot compile must not block the edit — but it must not be silent
182
+ either, so the message rides the same inline slot the schema errors would have used. */
183
+ reject(target, `Could not validate project.json — ${errorMessage(error)}`);
184
+ return;
185
+ }
186
+ if (schemaErrors.length > 0) {
187
+ reject(target, schemaErrors.join("; "));
188
+ return;
189
+ }
190
+ try {
191
+ await updateSiteConfig({ $media: next });
192
+ errors.delete(container);
193
+ } catch (error) {
194
+ errors.set(container, {
195
+ message: `Could not save project.json — ${errorMessage(error)}`,
196
+ target,
197
+ });
198
+ }
199
+ renderContextsSection(container);
200
+ };
201
+
202
+ /** The error line for one control, when that is where the current failure belongs. */
203
+ const errorFor = (target: ErrorTarget) =>
204
+ shown?.target === target
205
+ ? html`<p class="settings-field-error" role="alert">${shown.message}</p>`
206
+ : nothing;
207
+
208
+ // ─── Base width ────────────────────────────────────────────────────────────
209
+
210
+ const onBaseChange = (e: Event) => {
211
+ const value = (e.target as HTMLInputElement).value.trim();
212
+ if (value && !/^\d+px$/.test(value)) {
213
+ reject("base", "Enter a width in pixels, like 1280px.");
214
+ return;
215
+ }
216
+ const next = { ...media };
217
+ if (value) {
218
+ next[BASE_KEY] = value;
219
+ } else {
220
+ delete next[BASE_KEY];
221
+ }
222
+ void persistMedia(next, "base");
223
+ };
224
+
225
+ // ─── Row editing ───────────────────────────────────────────────────────────
226
+
227
+ const onRename = (oldKey: string) => (e: Event) => {
228
+ const newKey = contextKeyOf((e.target as HTMLInputElement).value);
229
+ if (!newKey) {
230
+ reject(oldKey, "A context needs a name.");
231
+ return;
232
+ }
233
+ if (newKey === oldKey) {
234
+ return;
235
+ }
236
+ if (newKey in media) {
237
+ reject(oldKey, `"${mediaDisplayName(newKey)}" is already defined.`);
238
+ return;
239
+ }
240
+ /* Rebuild in place so renaming never reorders the list — the order is the order the pane
241
+ context bar offers them in, and a rename is not a reordering. */
242
+ const next: Record<string, string> = {};
243
+ for (const [key, value] of Object.entries(media)) {
244
+ next[key === oldKey ? newKey : key] = value;
245
+ }
246
+ void persistMedia(next, newKey);
247
+ };
248
+
249
+ const onQueryChange = (key: string) => (e: Event) => {
250
+ const query = (e.target as HTMLInputElement).value.trim();
251
+ if (!query) {
252
+ reject(key, "A context needs a media query, like (max-width: 768px).");
253
+ return;
254
+ }
255
+ void persistMedia({ ...media, [key]: query }, key);
256
+ };
257
+
258
+ const onRemove = (key: string) => () => {
259
+ const next = { ...media };
260
+ delete next[key];
261
+ void persistMedia(next, "section");
262
+ };
263
+
264
+ /** Add an entry under the first free `--<stem>` / `--<stem>-2` … name. */
265
+ const add = (stem: string, query: string) => () => {
266
+ let key = `--${stem}`;
267
+ let n = 2;
268
+ while (key in media) {
269
+ key = `--${stem}-${n}`;
270
+ n += 1;
271
+ }
272
+ void persistMedia({ ...media, [key]: query }, key);
273
+ };
274
+
275
+ /** The scheme picker writes the canonical query, so a scheme is never mistyped into a feature. */
276
+ const onSchemeChange = (key: string) => (e: Event) => {
277
+ const scheme = (e.target as HTMLInputElement).value;
278
+ void persistMedia({ ...media, [key]: `(prefers-color-scheme: ${scheme})` }, key);
279
+ };
280
+
281
+ // ─── Group templates ───────────────────────────────────────────────────────
282
+
283
+ const removeButton = (key: string) => html`
284
+ <sp-action-button
285
+ size="s"
286
+ quiet
287
+ data-remove=${key}
288
+ title="Remove ${mediaDisplayName(key)}"
289
+ @click=${onRemove(key)}
290
+ >
291
+ ×
292
+ </sp-action-button>
293
+ `;
294
+
295
+ const nameField = (key: string) => html`
296
+ <sp-textfield
297
+ size="s"
298
+ class="settings-media-name"
299
+ .value=${live(key.replace(/^--/, ""))}
300
+ placeholder="name"
301
+ ?invalid=${shown?.target === key}
302
+ @change=${onRename(key)}
303
+ ></sp-textfield>
304
+ `;
305
+
306
+ const queryRow = (entry: ContextEntry) => html`
307
+ <div class="settings-media-row" data-context=${entry.key}>
308
+ ${nameField(entry.key)}
309
+ <sp-textfield
310
+ size="s"
311
+ class="settings-media-value"
312
+ .value=${live(entry.query)}
313
+ placeholder=${NEW_BREAKPOINT_QUERY}
314
+ ?invalid=${shown?.target === entry.key}
315
+ @change=${onQueryChange(entry.key)}
316
+ ></sp-textfield>
317
+ ${removeButton(entry.key)}
318
+ </div>
319
+ ${errorFor(entry.key)}
320
+ `;
321
+
322
+ const schemeRow = (entry: ContextEntry) => html`
323
+ <div class="settings-media-row" data-context=${entry.key}>
324
+ ${nameField(entry.key)}
325
+ <sp-picker
326
+ size="s"
327
+ class="settings-media-value"
328
+ label="Scheme"
329
+ .value=${schemeOfQuery(entry.query) ?? "dark"}
330
+ @change=${onSchemeChange(entry.key)}
331
+ >
332
+ <sp-menu-item value="light">Light</sp-menu-item>
333
+ <sp-menu-item value="dark">Dark</sp-menu-item>
334
+ </sp-picker>
335
+ ${removeButton(entry.key)}
336
+ </div>
337
+ ${errorFor(entry.key)}
338
+ `;
339
+
340
+ const group = (
341
+ kind: ContextKind,
342
+ label: string,
343
+ desc: string,
344
+ row: (entry: ContextEntry) => unknown,
345
+ addLabel: string,
346
+ onAdd: () => void,
347
+ empty: string,
348
+ ) => {
349
+ const rows = entries.filter((entry) => entry.kind === kind);
350
+ return html`
351
+ <div class="settings-field" data-context-group=${kind}>
352
+ <label class="settings-field-label">${label}</label>
353
+ <p class="settings-field-desc">${desc}</p>
354
+ <div class="settings-media-list">
355
+ ${
356
+ rows.length === 0
357
+ ? html`<p class="settings-field-desc">${empty}</p>`
358
+ : rows.map((entry) => row(entry))
359
+ }
360
+ </div>
361
+ <sp-action-button size="s" data-add=${kind} @click=${onAdd}>${addLabel}</sp-action-button>
362
+ </div>
363
+ `;
364
+ };
365
+
366
+ const tpl = html`
367
+ <div class="settings-section">
368
+ <h3 class="settings-section-title">Contexts</h3>
369
+ <p class="settings-field-desc">
370
+ The conditions this project's pages are rendered under. Define them once here; choose
371
+ between them on the pane's context control while you edit.
372
+ </p>
373
+ ${errorFor("section")}
374
+
375
+ <div class="settings-field">
376
+ <label class="settings-field-label">Base width</label>
377
+ <p class="settings-field-desc">
378
+ How wide the canvas is when no other context applies. Styles written here apply
379
+ everywhere.
380
+ </p>
381
+ <div class="settings-media-row">
382
+ <span class="settings-media-name-fixed">Base</span>
383
+ <sp-textfield
384
+ size="s"
385
+ class="settings-media-value"
386
+ data-context="base"
387
+ placeholder="1280px"
388
+ .value=${live(base)}
389
+ ?invalid=${shown?.target === "base"}
390
+ @change=${onBaseChange}
391
+ ></sp-textfield>
392
+ </div>
393
+ ${errorFor("base")}
394
+ </div>
395
+
396
+ ${group(
397
+ "size",
398
+ "Size breakpoints",
399
+ "Screen widths that get their own canvas and their own style values.",
400
+ queryRow,
401
+ "+ Add breakpoint",
402
+ add("breakpoint", NEW_BREAKPOINT_QUERY),
403
+ "No breakpoints yet — every width uses the base styles.",
404
+ )}
405
+ ${group(
406
+ "scheme",
407
+ "Colour schemes",
408
+ "Light and dark renderings. Defining one turns on the Auto / Light / Dark control.",
409
+ schemeRow,
410
+ "+ Add colour scheme",
411
+ add("dark", "(prefers-color-scheme: dark)"),
412
+ "No colour schemes yet — the project renders one way.",
413
+ )}
414
+ ${group(
415
+ "feature",
416
+ "Feature queries",
417
+ "Anything else a media query can ask: reduced motion, print, hover, orientation.",
418
+ queryRow,
419
+ "+ Add feature query",
420
+ add("feature", NEW_FEATURE_QUERY),
421
+ "No feature queries yet.",
422
+ )}
423
+ </div>
424
+ `;
425
+
426
+ litRender(tpl, container);
427
+ }
@@ -9,11 +9,17 @@
9
9
  */
10
10
 
11
11
  import { html, nothing, render as litRender } from "lit-html";
12
+ import { errorMessage } from "@jxsuite/schema/parse";
12
13
  import { getPlatform } from "../platform";
14
+ import { commitProjectConfig } from "../tabs/project-config";
13
15
  import { projectState } from "../store";
14
16
  import { renderForm } from "../ui/schema-form";
15
17
  import { resolveContextPointer } from "../services/context-resolver";
16
18
  import { deriveSecretEnvName } from "../services/data-service";
19
+ import { validateProjectConfig } from "../services/jx-validate";
20
+ import { notify } from "../services/notify";
21
+ import { paneRegion } from "../ui/regions";
22
+ import { paneOfContainer } from "../canvas/canvas-surface";
17
23
 
18
24
  import type { TemplateResult } from "lit-html";
19
25
  import type { JsonSchema, SchemaFormContext } from "../ui/schema-form";
@@ -77,12 +83,134 @@ export function resetContributedSectionState(): void {
77
83
  newEntryNames.clear();
78
84
  }
79
85
 
86
+ /**
87
+ * Select an entry of a map-layout section by key — the addressable form of clicking an entry row.
88
+ *
89
+ * Five screenshot steps used to reach these rows by typing an empty string into a hand-stamped
90
+ * region id, carrying `unstable: { reason: "\"settings.selectEntry\" has no command record", until:
91
+ * "P6.2" }`. This is P6.2 and the record is `settings.open {section, entry}`: an entry is a KEY in
92
+ * the section's own map, so naming it is naming state rather than reproducing a gesture.
93
+ *
94
+ * @param {string} sectionKey
95
+ * @param {string} entryKey
96
+ * @returns {string[] | null} `null` once selected; the section's actual entry keys when it has no
97
+ * such entry, so the caller's refusal can name both sides.
98
+ */
99
+ export function selectContributedEntry(sectionKey: string, entryKey: string): string[] | null {
100
+ const section = (projectState?.projectConfig as Record<string, unknown> | null | undefined)?.[
101
+ sectionKey
102
+ ];
103
+ const entries =
104
+ section && typeof section === "object" && !Array.isArray(section)
105
+ ? (section as Record<string, unknown>)
106
+ : {};
107
+ if (!Object.hasOwn(entries, entryKey)) {
108
+ return Object.keys(entries);
109
+ }
110
+ selectedEntries.set(sectionKey, entryKey);
111
+ return null;
112
+ }
113
+
114
+ // ─── Validation (§7.1 inline tier, §7.2 Problems) ─────────────────────────────
115
+
116
+ /**
117
+ * The last validator run's messages, keyed by the JSON-pointer base they were rendered under.
118
+ *
119
+ * `jx-validate` validates the WHOLE `project.json` against the project's generated entry document,
120
+ * so its output has to be routed back to individual controls. Until now it was wired to exactly one
121
+ * caller — the AI's `write_project_config` — which meant the model's edits to this file were
122
+ * schema-checked and a human's edits through this very form were not.
123
+ */
124
+ const diagnostics = new Map<string, Record<string, string>>();
125
+
126
+ /** Drop every cached diagnostic (test hook, and the "project closed" path). */
127
+ export function resetContributedDiagnostics(): void {
128
+ diagnostics.clear();
129
+ }
130
+
131
+ /**
132
+ * Route `jx-validate` messages ("/search/index: must be string") to the field they are about.
133
+ *
134
+ * Only the messages one level under `base` become field errors: `/search/index` belongs to the
135
+ * `index` control, and `/search/index/0/kind` belongs to it too — the deepest control that exists
136
+ * on this form is the one that can be corrected. A message ABOUT the base itself has no field to
137
+ * live at and is returned separately, for the section line.
138
+ *
139
+ * @param {string} base - JSON pointer of the record the form is editing, e.g. `/search`
140
+ * @param {string[]} messages
141
+ * @returns {{ fields: Record<string, string>; section: string[] }}
142
+ */
143
+ export function routeDiagnostics(
144
+ base: string,
145
+ messages: string[],
146
+ ): { fields: Record<string, string>; section: string[] } {
147
+ const fields: Record<string, string> = {};
148
+ const section: string[] = [];
149
+ for (const message of messages) {
150
+ const at = message.indexOf(": ");
151
+ const pointer = at === -1 ? "" : message.slice(0, at);
152
+ const text = at === -1 ? message : message.slice(at + 2);
153
+ if (pointer === base) {
154
+ section.push(text);
155
+ continue;
156
+ }
157
+ if (!pointer.startsWith(`${base}/`)) {
158
+ continue;
159
+ }
160
+ const [prop] = pointer.slice(base.length + 1).split("/");
161
+ if (prop && fields[prop] === undefined) {
162
+ fields[prop] = text;
163
+ }
164
+ }
165
+ return { fields, section };
166
+ }
167
+
80
168
  // ─── Persistence ──────────────────────────────────────────────────────────────
81
169
 
82
- async function saveProjectConfig() {
83
- const platform = getPlatform();
170
+ /**
171
+ * Commit `project.json`, then say what is wrong with it.
172
+ *
173
+ * The write itself now belongs to `tabs/project-config.ts` — one serialisation, one error path, and
174
+ * a transaction the author can undo. This module used to own a second writer, at
175
+ * `JSON.stringify(config, null, "\t")`, which re-indented the whole file on any edit; and before
176
+ * that it was `void saveProjectConfig()` at five call sites with no validation at all. §7.2 files a
177
+ * failed config write as a Problem, because it must be fixed and it is about a named file; the
178
+ * chokepoint files it, so a failure here only has to stop.
179
+ *
180
+ * **The write comes first, and the order is the honest one.** This form mutates
181
+ * `projectState.projectConfig` in place before calling here, so the value is already live in the
182
+ * editor and the canvas; a validator run in front of the write could not have prevented anything,
183
+ * it would only have delayed persisting what the user can already see. This surface REPORTS. The
184
+ * one that gates is `contexts-section.ts`, which builds a candidate map and validates it before
185
+ * anything is applied.
186
+ *
187
+ * @param {string} base - JSON pointer of the record being edited, for routing field errors
188
+ * @param {() => void} rerender
189
+ */
190
+ async function saveProjectConfig(base: string, rerender: () => void) {
84
191
  const config = (projectState as { projectConfig: ProjectConfig }).projectConfig;
85
- await platform.writeFile("project.json", JSON.stringify(config, null, "\t"));
192
+
193
+ const commit = await commitProjectConfig();
194
+ if (!commit.ok) {
195
+ return;
196
+ }
197
+
198
+ let messages: string[] = [];
199
+ try {
200
+ messages = await validateProjectConfig(config);
201
+ } catch (error) {
202
+ /* A validator that will not compile must not be silent about it — but it is not the edit's
203
+ fault, so it is a problem of its own rather than an error parked on the user's field. */
204
+ notify.warn(`Could not validate project.json — ${errorMessage(error)}`, {
205
+ key: "settings:validator",
206
+ path: "project.json",
207
+ source: "Settings",
208
+ tier: "problem",
209
+ });
210
+ return;
211
+ }
212
+ diagnostics.set(base, routeDiagnostics(base, messages).fields);
213
+ rerender();
86
214
  }
87
215
 
88
216
  // ─── Helpers ──────────────────────────────────────────────────────────────────
@@ -181,8 +309,13 @@ function sectionValue(key: string): Record<string, unknown> | null {
181
309
 
182
310
  // ─── Render ───────────────────────────────────────────────────────────────────
183
311
 
312
+ /* There is no `paneOfContainer` here. It moved to `canvas/canvas-surface.ts`, beside the other
313
+ "which pane is this about" answers, because it is not a settings idea: any stage content handed
314
+ only a host needs it, and `settings/general-settings.ts` and `settings/project-sections.ts` are
315
+ two more that do — both write a canvas mode, and both used to write the FOCUSED pane's. */
316
+
184
317
  /**
185
- * Render a contributed settings section into a settings-modal content container.
318
+ * Render a contributed settings section into the settings document's content area.
186
319
  *
187
320
  * @param {HTMLElement} container
188
321
  * @param {SettingsContribution} contribution
@@ -199,7 +332,7 @@ export function renderContributedSection(
199
332
 
200
333
  const body =
201
334
  layout === "map"
202
- ? renderMapLayout(contribution, opts, rerender)
335
+ ? renderMapLayout(contribution, opts, rerender, paneOfContainer(container))
203
336
  : renderFormLayout(contribution, opts, rerender);
204
337
 
205
338
  const selected = layout === "map" ? (selectedEntries.get(contribution.key) ?? null) : null;
@@ -225,11 +358,13 @@ function renderFormLayout(
225
358
  ): TemplateResult {
226
359
  const value = sectionValue(contribution.key) ?? {};
227
360
  const ui = contribution.settings.entry?.ui;
361
+ const base = `/${contribution.key}`;
228
362
 
229
363
  return html`
230
364
  <div class="settings-form-panel">
231
365
  ${renderForm(contribution.entrySchema, value, {
232
366
  context: buildContext(contribution.key, opts, null),
367
+ errors: diagnostics.get(base) ?? {},
233
368
  onChange: (patch) => {
234
369
  const target = sectionValue(contribution.key);
235
370
  if (!target) {
@@ -237,7 +372,7 @@ function renderFormLayout(
237
372
  }
238
373
  applyPatch(target, patch);
239
374
  rerender();
240
- void saveProjectConfig();
375
+ void saveProjectConfig(base, rerender);
241
376
  },
242
377
  rerender,
243
378
  ...(ui !== undefined && { ui }),
@@ -251,6 +386,7 @@ function renderMapLayout(
251
386
  contribution: SettingsContribution,
252
387
  opts: ContributedSectionOptions,
253
388
  rerender: () => void,
389
+ paneId: string,
254
390
  ): TemplateResult {
255
391
  const { key: sectionKey } = contribution;
256
392
  const entries = sectionValue(sectionKey) ?? {};
@@ -269,7 +405,7 @@ function renderMapLayout(
269
405
  newEntryOpen.delete(sectionKey);
270
406
  newEntryNames.delete(sectionKey);
271
407
  rerender();
272
- void saveProjectConfig();
408
+ void saveProjectConfig(`/${sectionKey}`, rerender);
273
409
  };
274
410
 
275
411
  const handleDelete = () => {
@@ -280,7 +416,7 @@ function renderMapLayout(
280
416
  delete target[selected];
281
417
  selectedEntries.set(sectionKey, null);
282
418
  rerender();
283
- void saveProjectConfig();
419
+ void saveProjectConfig(`/${sectionKey}`, rerender);
284
420
  };
285
421
 
286
422
  const handleRename = (newName: string) => {
@@ -298,7 +434,7 @@ function renderMapLayout(
298
434
  Object.assign(target, next);
299
435
  selectedEntries.set(sectionKey, slug);
300
436
  rerender();
301
- void saveProjectConfig();
437
+ void saveProjectConfig(`/${sectionKey}`, rerender);
302
438
  };
303
439
 
304
440
  // Left column — entry key list
@@ -308,6 +444,7 @@ function renderMapLayout(
308
444
  (name) => html`
309
445
  <sp-action-button
310
446
  size="s"
447
+ data-jx-region=${paneRegion(paneId, `entry:${name}`)}
311
448
  ?selected=${selected === name}
312
449
  @click=${() => {
313
450
  selectedEntries.set(sectionKey, name);
@@ -363,7 +500,7 @@ function renderMapLayout(
363
500
  const editorTpl: TemplateResult =
364
501
  selected && selectedEntry && typeof selectedEntry === "object"
365
502
  ? html`
366
- <div class="settings-editor-panel">
503
+ <div class="settings-editor-panel" data-jx-region=${paneRegion(paneId, "editor")}>
367
504
  <div class="settings-editor-header">
368
505
  <sp-textfield
369
506
  size="s"
@@ -392,10 +529,11 @@ function renderMapLayout(
392
529
  </div>
393
530
  ${renderForm(contribution.entrySchema, selectedEntry as Record<string, unknown>, {
394
531
  context: buildContext(sectionKey, opts, selected),
532
+ errors: diagnostics.get(`/${sectionKey}/${selected}`) ?? {},
395
533
  onChange: (patch) => {
396
534
  applyPatch(selectedEntry as Record<string, unknown>, patch);
397
535
  rerender();
398
- void saveProjectConfig();
536
+ void saveProjectConfig(`/${sectionKey}/${selected}`, rerender);
399
537
  },
400
538
  rerender,
401
539
  ...(ui !== undefined && { ui }),