@jxsuite/studio 2.0.0 → 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-0hzentwq.js → components-2bpqp4mq.js} +5 -7
  7. package/dist/chunks/{components-0hzentwq.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-2c188n41.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-97qcgvdn.js → studio-adn0z1k3.js} +14 -8
  74. package/dist/chunks/{studio-97qcgvdn.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-hhj92gcr.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-ygzav7z5.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-n9zmpxjg.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 +23711 -35185
  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 +34 -9
  214. package/src/new-project/location-fields.ts +9 -1
  215. package/src/new-project/new-project-modal.ts +270 -262
  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 -120
  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 +75 -2
  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 +113 -0
  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-qct0gt8f.js +0 -531
  363. package/dist/chunks/extension-sections-qct0gt8f.js.map +0 -12
  364. package/dist/chunks/files-02zn5y24.js +0 -50
  365. package/dist/chunks/files-02zn5y24.js.map +0 -9
  366. package/dist/chunks/git-panel-mbb0y0hz.js +0 -28
  367. package/dist/chunks/git-panel-mbb0y0hz.js.map +0 -9
  368. package/dist/chunks/layers-panel-9qekmqk1.js +0 -26
  369. package/dist/chunks/layers-panel-9qekmqk1.js.map +0 -9
  370. package/dist/chunks/settings-modal-gj0hrah4.js +0 -29
  371. package/dist/chunks/settings-modal-gj0hrah4.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-4k0m4fmp.js.map +0 -13
  381. package/dist/chunks/studio-568nx4t5.js +0 -469
  382. package/dist/chunks/studio-568nx4t5.js.map +0 -14
  383. package/dist/chunks/studio-9smfzqhc.js +0 -19
  384. package/dist/chunks/studio-9smfzqhc.js.map +0 -10
  385. package/dist/chunks/studio-asvf0j9e.js +0 -429
  386. package/dist/chunks/studio-asvf0j9e.js.map +0 -13
  387. package/dist/chunks/studio-d77qm3pk.js +0 -3427
  388. package/dist/chunks/studio-d77qm3pk.js.map +0 -26
  389. package/dist/chunks/studio-j6gqyjf4.js +0 -1854
  390. package/dist/chunks/studio-j6gqyjf4.js.map +0 -18
  391. package/dist/chunks/studio-n9zmpxjg.js.map +0 -115
  392. package/dist/chunks/studio-qkvt37c0.js.map +0 -9
  393. package/dist/chunks/studio-rq0vxb19.js +0 -3744
  394. package/dist/chunks/studio-rq0vxb19.js.map +0 -22
  395. package/dist/chunks/studio-wd6ewdvd.js +0 -853
  396. package/dist/chunks/studio-wd6ewdvd.js.map +0 -12
  397. package/dist/chunks/studio-xq0t7n5t.js +0 -140
  398. package/dist/chunks/studio-xq0t7n5t.js.map +0 -10
  399. package/dist/chunks/studio-xze4zrs1.js.map +0 -10
  400. package/dist/chunks/studio-ygzav7z5.js.map +0 -46
  401. package/dist/chunks/studio-zmy7241e.js +0 -399
  402. package/dist/chunks/studio-zmy7241e.js.map +0 -12
  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-2c188n41.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-hhj92gcr.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,592 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Languages — which pages exist in which locale, and which do not.
4
+ *
5
+ * Jx has no message catalogue. A translation is a DIFFERENT FILE in a different directory
6
+ * (`pages/fr/about.json` beside `pages/about.json`, `specs/site-architecture.md` §13.3), and that
7
+ * is why nothing in the shell could answer "is this site translated": the Files panel draws `fr/`
8
+ * the way it draws any other directory, and a page nobody has translated is invisible precisely
9
+ * because the file that would prove it does not exist.
10
+ *
11
+ * So this panel is a grid over ABSENCE. One row per translation key — the path with its locale
12
+ * segment taken out, which is what makes two files the same page — and one column per declared
13
+ * locale. A cell says one of three things:
14
+ *
15
+ * | Cell | Means | Its button |
16
+ * | --------- | ------------------------------------------------------------- | ------------------ |
17
+ * | `present` | the file is there | opens it |
18
+ * | `stale` | it is there, and older than the source it was translated from | opens it, and says |
19
+ * | `missing` | it is not there, and this is where it would go | creates it |
20
+ *
21
+ * **Every button is a command, run by id through the registry** — never a direct call, for the
22
+ * reason `panels/problems-panel.ts` gives about its recovery button: the registry is what makes the
23
+ * action reachable from the palette and from automation, and it is where the refusal sentence comes
24
+ * from. A cell whose command this window has not registered is drawn disabled with that as its
25
+ * tooltip, rather than as a button that does nothing.
26
+ *
27
+ * **Off the rail.** `railDeclarations()` does not apply `when`, so a rail button here would spend
28
+ * the last `rail/project` slot in every monolingual project and shift every document panel's ⌘1–8
29
+ * chord by one. `i18n.showParity` is how this panel is reached.
30
+ *
31
+ * @docs studio/interface/languages
32
+ */
33
+
34
+ import { html, nothing } from "lit-html";
35
+ import { repeat } from "lit-html/directives/repeat.js";
36
+ import { activeRegistry } from "../commands/active-registry";
37
+ import { errorMessage } from "@jxsuite/schema/parse";
38
+ import { getEffectiveLocales } from "../site-context";
39
+ import { getPlatform } from "../platform";
40
+ import {
41
+ localeLabel,
42
+ localeOfPath,
43
+ translationKeyOfPath,
44
+ translationPathFor,
45
+ } from "@jxsuite/schema/locale";
46
+ import { notify } from "../services/notify";
47
+ import { projectState } from "../store";
48
+ import { registerPanel } from "./panel-registry";
49
+ import { renderEmptyState } from "./empty-state";
50
+ import { scanLibrary } from "../browse/library-model";
51
+ import type { LibraryFile, ScanFailure } from "../browse/library-model";
52
+ import type { NavigatorPanelContext, PanelBody } from "./panel-registry";
53
+ import type { ResolvedI18n } from "@jxsuite/schema/locale";
54
+ import type { TemplateResult } from "lit-html";
55
+
56
+ /**
57
+ * One cell of the parity grid.
58
+ *
59
+ * **Stale is defined here and nowhere else**: a translation is stale when the DEFAULT locale's file
60
+ * for the same key carries a `modified` strictly newer than the translation's. A file the platform
61
+ * reported no `modified` for is `present`, never `stale` — an absent timestamp is not evidence of
62
+ * being behind, and `browse/library-layouts.ts`'s `formatModified` already set the precedent of
63
+ * refusing to invent one.
64
+ */
65
+ export type ParityCell =
66
+ /** The file is on disk at `path`. */
67
+ | { state: "present"; path: string }
68
+ /** The file is on disk at `path` and older than `behind`, the source it was translated from. */
69
+ | { state: "stale"; path: string; behind: string }
70
+ /**
71
+ * No file. `path` is where it WOULD go, and `null` when the key cannot carry a locale directory
72
+ * at all — a file directly under `content/`, outside any collection, has no place to put one.
73
+ */
74
+ | { state: "missing"; path: string | null };
75
+
76
+ /** One row of the parity grid: a translation key and its cell per declared locale. */
77
+ export interface ParityRow {
78
+ /** `translationKeyOfPath` of the files behind the row — the path with the locale taken out. */
79
+ key: string;
80
+ /** Locale → cell, one entry per declared locale, in declaration order. */
81
+ cells: Map<string, ParityCell>;
82
+ }
83
+
84
+ /**
85
+ * The directories a translation can live in — the two layouts §13 defines, and nothing else.
86
+ *
87
+ * Not `projectState.projectDirs`: `layouts/` and `components/` are shared by every locale and
88
+ * `public/` is not addressable per language, so scanning them would fill the grid with rows whose
89
+ * every cell is permanently `missing` for a reason that is not the author's to fix.
90
+ */
91
+ const PARITY_DIRS = ["pages", "content"] as const;
92
+
93
+ /**
94
+ * How many keys the grid draws.
95
+ *
96
+ * It states the remainder rather than dropping it silently, which is what `calendarTpl` does with
97
+ * the days it does not show — a truncation nobody is told about is indistinguishable from a project
98
+ * that really is that small.
99
+ */
100
+ export const PARITY_ROW_LIMIT = 200;
101
+
102
+ /** One completed pass over {@link PARITY_DIRS}, and the project root it was taken in. */
103
+ interface ParityScan {
104
+ root: string;
105
+ files: readonly LibraryFile[];
106
+ failures: readonly ScanFailure[];
107
+ /** Path → the `$translationKey` that document declares, for the documents that declare one. */
108
+ declared: ReadonlyMap<string, string>;
109
+ }
110
+
111
+ /*
112
+ * The scan is module state rather than panel state because the panel's `render` is synchronous and
113
+ * the Navigator repaints it on every keystroke elsewhere in the shell. Keying it on the project
114
+ * root is what makes a project switch re-read rather than draw the previous project's file tree.
115
+ */
116
+ let scanned: ParityScan | null = null;
117
+ let scanning: string | null = null;
118
+
119
+ /** Forget the scan, so the next render takes a fresh one. The Rescan button, and every test. */
120
+ export function refreshParityScan(): void {
121
+ scanned = null;
122
+ scanning = null;
123
+ }
124
+
125
+ /**
126
+ * Walk the project once, then repaint.
127
+ *
128
+ * `scanLibrary` never rejects, so the only way into the catch is {@link getPlatform} throwing in a
129
+ * window that has none. That is recorded as a scan failure rather than swallowed, for the reason
130
+ * `browse/library-model.ts` states at length: a listing that half-worked and a project that is
131
+ * empty produced the same sentence, and only one of them is true.
132
+ */
133
+ async function takeScan(root: string, rerender: () => void): Promise<void> {
134
+ try {
135
+ const scan = await scanLibrary(PARITY_DIRS, getPlatform());
136
+ scanned = {
137
+ declared: await readDeclaredKeys(scan.files),
138
+ failures: scan.failures,
139
+ files: scan.files,
140
+ root,
141
+ };
142
+ } catch (error) {
143
+ scanned = {
144
+ declared: new Map(),
145
+ failures: [{ dir: PARITY_DIRS.join(", "), error: errorMessage(error) }],
146
+ files: [],
147
+ root,
148
+ };
149
+ }
150
+ scanning = null;
151
+ rerender();
152
+ }
153
+
154
+ /**
155
+ * The `$translationKey` each page declares, for the pages that declare one.
156
+ *
157
+ * Without this the grid keys on the path alone, and a **localized slug** — the case
158
+ * `site-architecture.md` §13.5 exists to handle — reads as two half-translated pages rather than
159
+ * one whole one: `pages/about.json` present in English and Arabic, `pages/a-propos.json` present in
160
+ * French, and four `missing` cells describing files nobody should write. That is the wrong answer
161
+ * on the one surface whose whole job is to say which pages are translated.
162
+ *
163
+ * A route parameter is normalized to its template spelling — `exhibitions/${slug}` and the English
164
+ * `pages/exhibitions/[slug].json` are the same page — because the grid keys on FILES where the
165
+ * build keys on routes, and a template stands for every route it expands to.
166
+ *
167
+ * Read from the raw text, and only for the files that mention the key: the parse is what costs, and
168
+ * most pages of a multilingual site never declare one — their paths are parallel and the derivation
169
+ * is right. A file that cannot be read is simply absent from the map, which leaves it keyed by
170
+ * path.
171
+ *
172
+ * @param {readonly LibraryFile[]} files
173
+ * @returns {Promise<Map<string, string>>}
174
+ */
175
+ async function readDeclaredKeys(files: readonly LibraryFile[]): Promise<Map<string, string>> {
176
+ const out = new Map<string, string>();
177
+ const platform = getPlatform();
178
+ for (const file of files) {
179
+ if (file.category === "Media" || !file.path.startsWith("pages/")) {
180
+ continue;
181
+ }
182
+ let text: string;
183
+ try {
184
+ text = await platform.readFile(file.path);
185
+ } catch {
186
+ continue;
187
+ }
188
+ if (!text.includes("$translationKey")) {
189
+ continue;
190
+ }
191
+ const declared = /"\$translationKey"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(text)?.[1];
192
+ if (declared !== undefined && declared !== "") {
193
+ out.set(file.path, declared.replaceAll(/\$\{(\w+)\}/g, "[$1]").replaceAll(/^\/+|\/+$/g, ""));
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+
199
+ /** ISO timestamp as a comparable number, or `null` — never a guessed "now". */
200
+ function timeOf(modified: string | undefined): number | null {
201
+ if (!modified) {
202
+ return null;
203
+ }
204
+ const parsed = Date.parse(modified);
205
+ return Number.isNaN(parsed) ? null : parsed;
206
+ }
207
+
208
+ /**
209
+ * Fold a scan into the grid. Pure: no I/O, no platform, no DOM.
210
+ *
211
+ * A file under no locale segment lands under the DEFAULT locale, because that is what it is — under
212
+ * `prefix-except-default` the unprefixed path is the default locale's copy, and under
213
+ * `prefix-always` it is the copy an author wrote before declaring the locale they meant. Either way
214
+ * it is the source the other columns are measured against, and hiding it would leave the grid with
215
+ * a column of `missing` cells whose sources are on disk.
216
+ *
217
+ * Assets are skipped. `categoryFor` already decides what is media by extension, so an image beside
218
+ * a page is not a row here for the same reason it is not a page anywhere else.
219
+ */
220
+ export function parityRows(
221
+ files: readonly LibraryFile[],
222
+ i18n: ResolvedI18n,
223
+ declared: ReadonlyMap<string, string> = new Map(),
224
+ ): ParityRow[] {
225
+ const byKey = new Map<string, Map<string, LibraryFile>>();
226
+ /*
227
+ * A document's own `$translationKey` overrides the one its path implies, exactly as it does in
228
+ * the build (§13.5) — which is what makes a localized slug one row instead of two half-empty
229
+ * ones. Prefixed with the directory the derivation would have produced, so the two spellings meet:
230
+ * a declared key is route-shaped (`about`) and this grid is file-shaped (`pages/about.json`).
231
+ */
232
+ /* Every key the paths alone produce, so a declared one can be matched against a real file rather
233
+ than against a spelling. */
234
+ const derivedKeys = new Set(
235
+ files.filter((f) => f.category !== "Media").map((f) => translationKeyOfPath(f.path, i18n)),
236
+ );
237
+ const keyOf = (file: LibraryFile): string => {
238
+ const own = declared.get(file.path);
239
+ if (own === undefined) {
240
+ return translationKeyOfPath(file.path, i18n);
241
+ }
242
+ /*
243
+ * A declared key is route-shaped (`about`, `exhibitions`) while this grid is file-shaped
244
+ * (`pages/about.json`, `pages/exhibitions/index.json`), and one route can be either — a page or
245
+ * a directory's index. Both spellings are tried against the files actually scanned, so the key
246
+ * lands on the page that exists; the plain form stands when neither does, which keeps the row
247
+ * visible instead of merging it into something arbitrary.
248
+ */
249
+ const ext = /\.[^./]+$/.exec(file.path)?.[0] ?? "";
250
+ for (const candidate of [`pages/${own}${ext}`, `pages/${own}/index${ext}`]) {
251
+ if (derivedKeys.has(candidate)) {
252
+ return candidate;
253
+ }
254
+ }
255
+ return `pages/${own}${ext}`;
256
+ };
257
+ for (const file of files) {
258
+ if (file.category === "Media") {
259
+ continue;
260
+ }
261
+ const key = keyOf(file);
262
+ const locale = localeOfPath(file.path, i18n) ?? i18n.defaultLocale;
263
+ const cell = byKey.get(key) ?? new Map<string, LibraryFile>();
264
+ /*
265
+ * Two files can claim one square — `pages/about.json` and `pages/en/about.json` are both the
266
+ * English copy under `prefix-except-default`. The one at the CANONICAL path wins, so the grid
267
+ * agrees with the location `i18n.createTranslation` would have written to; otherwise the first
268
+ * in path order stands, and the duplicate is the author's to reconcile.
269
+ */
270
+ if (!cell.has(locale) || file.path === translationPathFor(key, locale, i18n)) {
271
+ cell.set(locale, file);
272
+ }
273
+ byKey.set(key, cell);
274
+ }
275
+
276
+ const rows: ParityRow[] = [];
277
+ for (const [key, found] of byKey) {
278
+ const source = found.get(i18n.defaultLocale);
279
+ const sourceTime = timeOf(source?.modified);
280
+ const cells = new Map<string, ParityCell>();
281
+ for (const locale of i18n.locales) {
282
+ const file = found.get(locale);
283
+ if (!file) {
284
+ cells.set(locale, { path: translationPathFor(key, locale, i18n), state: "missing" });
285
+ continue;
286
+ }
287
+ const time = timeOf(file.modified);
288
+ const behind =
289
+ source !== undefined &&
290
+ source.path !== file.path &&
291
+ sourceTime !== null &&
292
+ time !== null &&
293
+ sourceTime > time;
294
+ cells.set(
295
+ locale,
296
+ behind
297
+ ? { behind: source.path, path: file.path, state: "stale" }
298
+ : { path: file.path, state: "present" },
299
+ );
300
+ }
301
+ rows.push({ cells, key });
302
+ }
303
+ return rows.toSorted((a, b) => a.key.localeCompare(b.key));
304
+ }
305
+
306
+ /** Where a monolingual project declares its languages — the empty state's one action. */
307
+ const SETTINGS_COMMAND = "settings.open";
308
+
309
+ /** The command each cell state runs. */
310
+ const CELL_COMMAND: Readonly<Record<ParityCell["state"], string>> = {
311
+ missing: "i18n.createTranslation",
312
+ present: "i18n.openTranslation",
313
+ stale: "i18n.openTranslation",
314
+ };
315
+
316
+ /** One glyph per state, the way `problems-panel.ts` draws severity — scannable down a column. */
317
+ const CELL_GLYPH: Readonly<Record<ParityCell["state"], string>> = {
318
+ missing: "+",
319
+ present: "✓",
320
+ stale: "!",
321
+ };
322
+
323
+ /**
324
+ * The path a row's commands are addressed by: whichever file of the row actually exists, preferring
325
+ * the source, and the key itself when the row is empty in every locale.
326
+ *
327
+ * Any sibling answers, because `translationPathFor` takes the locale out before it puts one back.
328
+ */
329
+ function rowSourcePath(row: ParityRow, i18n: ResolvedI18n): string {
330
+ const source = row.cells.get(i18n.defaultLocale);
331
+ if (source && source.state !== "missing") {
332
+ return source.path;
333
+ }
334
+ for (const cell of row.cells.values()) {
335
+ if (cell.state !== "missing") {
336
+ return cell.path;
337
+ }
338
+ }
339
+ return row.key;
340
+ }
341
+
342
+ /** Why a cell's button will not run, or `undefined` when it will. */
343
+ function cellRefusal(cell: ParityCell, id: string): string | undefined {
344
+ if (cell.state === "missing" && cell.path === null) {
345
+ return "a file that can hold a locale directory — this one is under no collection";
346
+ }
347
+ const registry = activeRegistry();
348
+ if (!registry?.get(id)) {
349
+ return "the Languages commands, which this window has not registered";
350
+ }
351
+ return registry.disabledReason(id);
352
+ }
353
+
354
+ /** The cell's whole sentence: what the click does, or what it needs first. */
355
+ function cellTitle(cell: ParityCell, locale: string, refusal: string | undefined): string {
356
+ const label = localeLabel(locale);
357
+ const said =
358
+ cell.state === "missing"
359
+ ? cell.path === null
360
+ ? `No place for a ${label} copy of this file`
361
+ : `Create ${cell.path}`
362
+ : cell.state === "stale"
363
+ ? `Open ${cell.path} — older than ${cell.behind}`
364
+ : `Open ${cell.path}`;
365
+ return refusal === undefined ? said : `${said} — requires ${refusal}`;
366
+ }
367
+
368
+ /**
369
+ * Run one of this panel's actions, by id, and say so when it refuses.
370
+ *
371
+ * The `catch` is the point: `i18n.createTranslation` rejects when the file it would write is
372
+ * already there, and a click that produced nothing and printed nothing would leave the author
373
+ * looking at a grid that disagrees with the disk.
374
+ */
375
+ function runByName(id: string, args: Record<string, unknown> = {}): void {
376
+ const registry = activeRegistry();
377
+ if (!registry?.get(id)) {
378
+ return;
379
+ }
380
+ void Promise.resolve(registry.run(id, args)).catch((error: unknown) => {
381
+ notify("error", errorMessage(error), { source: "Languages" });
382
+ });
383
+ }
384
+
385
+ /** One square of the grid. */
386
+ function cellTpl(
387
+ row: ParityRow,
388
+ locale: string,
389
+ cell: ParityCell,
390
+ i18n: ResolvedI18n,
391
+ ): TemplateResult {
392
+ const id = CELL_COMMAND[cell.state];
393
+ const refusal = cellRefusal(cell, id);
394
+ const title = cellTitle(cell, locale, refusal);
395
+ return html`<td class="i18n-cell">
396
+ <button
397
+ class="i18n-cell-button i18n-cell-button--${cell.state}"
398
+ ?disabled=${refusal !== undefined}
399
+ title=${title}
400
+ aria-label=${title}
401
+ @click=${() => {
402
+ runByName(id, { locale, path: rowSourcePath(row, i18n) });
403
+ }}
404
+ >
405
+ <span aria-hidden="true">${CELL_GLYPH[cell.state]}</span>
406
+ </button>
407
+ </td>`;
408
+ }
409
+
410
+ /** One row: the key it names, then its cell per declared locale. */
411
+ function rowTpl(row: ParityRow, i18n: ResolvedI18n): TemplateResult {
412
+ return html`<tr class="i18n-row">
413
+ <th class="i18n-key" scope="row" title=${row.key}>${row.key}</th>
414
+ ${i18n.locales.map((locale) =>
415
+ cellTpl(row, locale, row.cells.get(locale) ?? { path: null, state: "missing" }, i18n),
416
+ )}
417
+ </tr>`;
418
+ }
419
+
420
+ /** The column heads: each locale's autonym, with the tag as the tooltip. */
421
+ function headTpl(i18n: ResolvedI18n): TemplateResult {
422
+ return html`<tr>
423
+ <th class="i18n-key-head" scope="col">Page</th>
424
+ ${i18n.locales.map(
425
+ (locale) => html`<th class="i18n-locale-head" scope="col" title=${locale}>
426
+ ${localeLabel(locale)}
427
+ ${
428
+ locale === i18n.defaultLocale
429
+ ? html`<span class="i18n-default-mark">source</span>`
430
+ : nothing
431
+ }
432
+ </th>`,
433
+ )}
434
+ </tr>`;
435
+ }
436
+
437
+ /** How many squares are not yet a file, and how many are behind their source. */
438
+ function countCells(rows: readonly ParityRow[]): { missing: number; stale: number } {
439
+ let missing = 0;
440
+ let stale = 0;
441
+ for (const row of rows) {
442
+ for (const cell of row.cells.values()) {
443
+ if (cell.state === "missing") {
444
+ missing += 1;
445
+ } else if (cell.state === "stale") {
446
+ stale += 1;
447
+ }
448
+ }
449
+ }
450
+ return { missing, stale };
451
+ }
452
+
453
+ /** The one sentence above the grid: its size, and what is outstanding in it. */
454
+ function summaryText(rows: readonly ParityRow[]): string {
455
+ const { missing, stale } = countCells(rows);
456
+ const pages = `${rows.length} page${rows.length === 1 ? "" : "s"}`;
457
+ if (missing === 0 && stale === 0) {
458
+ return `${pages}, translated into every declared language.`;
459
+ }
460
+ const parts: string[] = [];
461
+ if (missing > 0) {
462
+ parts.push(`${missing} not written`);
463
+ }
464
+ if (stale > 0) {
465
+ parts.push(`${stale} older than their source`);
466
+ }
467
+ return `${pages} — ${parts.join(", ")}.`;
468
+ }
469
+
470
+ /**
471
+ * The panel body.
472
+ *
473
+ * `getEffectiveLocales()` is read HERE, on every paint, rather than cached at module scope:
474
+ * `projectState` is a plain binding replaced wholesale by `setProjectState`, so a locale added in
475
+ * Settings reaches a cached copy never.
476
+ */
477
+ export function renderI18nPanel(ctx: NavigatorPanelContext): PanelBody {
478
+ const i18n = getEffectiveLocales();
479
+ if (i18n === null || i18n.locales.length < 2) {
480
+ return renderEmptyState({
481
+ actions: [
482
+ {
483
+ disabled: activeRegistry()?.get(SETTINGS_COMMAND) === undefined,
484
+ label: "Open project settings…",
485
+ run: () => {
486
+ runByName(SETTINGS_COMMAND);
487
+ },
488
+ },
489
+ ],
490
+ detail:
491
+ "Declare them under Locales in the project's settings, then a page's translations are " +
492
+ "sibling files under a directory named for each language.",
493
+ message: "This project is written in one language.",
494
+ });
495
+ }
496
+
497
+ const root = projectState?.projectRoot ?? "";
498
+ if (scanned === null || scanned.root !== root) {
499
+ if (scanning !== root) {
500
+ scanning = root;
501
+ void takeScan(root, ctx.rerender);
502
+ }
503
+ return renderEmptyState({
504
+ detail: "Reading pages/ and content/ to find out which languages each page is written in.",
505
+ message: "Looking for translations…",
506
+ });
507
+ }
508
+
509
+ const rows = parityRows(scanned.files, i18n, scanned.declared);
510
+ const shown = rows.slice(0, PARITY_ROW_LIMIT);
511
+ const hidden = rows.length - shown.length;
512
+ return html`
513
+ <div class="i18n-panel">
514
+ <div class="i18n-actions">
515
+ <p class="i18n-summary">${summaryText(rows)}</p>
516
+ <button
517
+ class="i18n-rescan"
518
+ title="Read pages/ and content/ again"
519
+ @click=${() => {
520
+ refreshParityScan();
521
+ ctx.rerender();
522
+ }}
523
+ >
524
+ Rescan
525
+ </button>
526
+ </div>
527
+ ${
528
+ scanned.failures.length > 0
529
+ ? html`<p class="i18n-incomplete">
530
+ This list is incomplete —
531
+ ${scanned.failures.map((failure) => `${failure.dir} (${failure.error})`).join("; ")}
532
+ </p>`
533
+ : nothing
534
+ }
535
+ ${
536
+ rows.length === 0
537
+ ? renderEmptyState({
538
+ compact: true,
539
+ detail: "Add a page under pages/, or an entry to a content collection.",
540
+ message: "There is nothing to translate yet.",
541
+ })
542
+ : html`<table class="i18n-parity">
543
+ <thead>
544
+ ${headTpl(i18n)}
545
+ </thead>
546
+ <tbody>
547
+ ${repeat(
548
+ shown,
549
+ (row) => row.key,
550
+ (row) => rowTpl(row, i18n),
551
+ )}
552
+ </tbody>
553
+ </table>`
554
+ }
555
+ ${
556
+ hidden > 0
557
+ ? html`<p class="i18n-truncated">
558
+ ${hidden} more page${hidden === 1 ? " is" : "s are"} not shown — the grid stops at
559
+ ${PARITY_ROW_LIMIT}.
560
+ </p>`
561
+ : nothing
562
+ }
563
+ </div>
564
+ `;
565
+ }
566
+
567
+ /**
568
+ * Define the Languages panel.
569
+ *
570
+ * `level: "project"` because it writes project FILES, and its body therefore never reads `ctx.doc`
571
+ * — `tests/panel-registry.test.ts` renders every project-level panel with `doc: null` and a deps
572
+ * object whose document-level members throw.
573
+ *
574
+ * One caller: `panels/navigator-panels.ts`, which is the one place panel records are composed.
575
+ */
576
+ export function registerI18nPanel(): void {
577
+ registerPanel({
578
+ id: "i18n",
579
+ title: "Languages",
580
+ level: "project",
581
+ dock: "navigator",
582
+ // Inert, like every other `rail: false` panel's: `tabIcon()` is only ever called by a rail
583
+ // Button, and `check-icons.ts` fails on a resolver row no rail panel declares.
584
+ icon: "sp-icon-globe",
585
+ // OFF THE RAIL. `railDeclarations()` does not apply `when`, so a rail button would spend the
586
+ // Last rail/project slot in every monolingual project — and would shift every document panel's
587
+ // ⌘1-8 chord by one. `i18n.showParity` is how it is reached.
588
+ rail: false,
589
+ when: (ctx) => ctx.project.isMultilingual,
590
+ render: (ctx) => renderI18nPanel(ctx),
591
+ });
592
+ }