@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
@@ -143,8 +143,19 @@ export function cellValuesEqual(a: GridCellValue, b: GridCellValue): boolean {
143
143
 
144
144
  const GRID_SCHEME = "grid://";
145
145
 
146
+ /**
147
+ * The virtual sources a tab id can name.
148
+ *
149
+ * `library` is the odd one and deliberately so: the Library is a source over this same contract
150
+ * (`browse/library-source.ts`) but its tab's editor kind is `library`, not `grid` — Table is one of
151
+ * its five layouts rather than its identity. It lives in this id space because "which virtual tab
152
+ * is this?" has exactly one answer per window and there is no second scheme worth inventing.
153
+ * Category and layout are VIEW state and are deliberately absent from the id: a Library that
154
+ * changed identity when you clicked "Pages" would open a second tab per filter.
155
+ */
146
156
  export type GridTabRef =
147
157
  | { kind: "pages" }
158
+ | { kind: "library" }
148
159
  | { kind: "collection"; name: string }
149
160
  | { kind: "data"; connection: string; table: string };
150
161
 
@@ -154,6 +165,9 @@ export function makeGridTabId(ref: GridTabRef): string {
154
165
  case "pages": {
155
166
  return `${GRID_SCHEME}pages`;
156
167
  }
168
+ case "library": {
169
+ return `${GRID_SCHEME}library`;
170
+ }
157
171
  case "collection": {
158
172
  return `${GRID_SCHEME}collection/${encodeURIComponent(ref.name)}`;
159
173
  }
@@ -177,6 +191,9 @@ export function parseGridTabId(id: string): GridTabRef | null {
177
191
  if (parts[0] === "pages" && parts.length === 1) {
178
192
  return { kind: "pages" };
179
193
  }
194
+ if (parts[0] === "library" && parts.length === 1) {
195
+ return { kind: "library" };
196
+ }
180
197
  if (parts[0] === "collection" && parts.length === 2 && parts[1]) {
181
198
  return { kind: "collection", name: decodeURIComponent(parts[1]) };
182
199
  }
@@ -190,7 +207,13 @@ export function parseGridTabId(id: string): GridTabRef | null {
190
207
  return null;
191
208
  }
192
209
 
193
- /** Short human label for a grid tab ("posts · grid", "users @ main · grid", "Pages · grid"). */
210
+ /**
211
+ * Short human label for a virtual tab ("posts · grid", "users @ main · grid", "Pages · grid").
212
+ *
213
+ * The Library is just "Library": the `· grid` suffix distinguishes a grid over a source from the
214
+ * document tab of the same name, and the Library has no document tab to be confused with — nor is a
215
+ * grid what it is, only what one of its layouts draws.
216
+ */
194
217
  export function gridTabLabel(id: string): string | null {
195
218
  const ref = parseGridTabId(id);
196
219
  if (!ref) {
@@ -200,6 +223,9 @@ export function gridTabLabel(id: string): string | null {
200
223
  case "pages": {
201
224
  return "Pages · grid";
202
225
  }
226
+ case "library": {
227
+ return "Library";
228
+ }
203
229
  case "collection": {
204
230
  return `${ref.name} · grid`;
205
231
  }
@@ -0,0 +1,599 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Redirects as a {@link GridSource} — the editor §11.4 asks for, over the table the grid already
4
+ * is.
5
+ *
6
+ * `project.json`'s `redirects` map is tabular, so it does not need a bespoke editor: it needs the
7
+ * one Studio already has. Columns, inline editing, add/delete rows, undo, the dirty dot, ⌘S,
8
+ * find-and-replace, saved views and CSV export all arrive with the contract. What this module adds
9
+ * is the three things a redirect table needs and a spreadsheet cannot know — that a chain costs a
10
+ * round trip, that a loop is a broken page, and that a rule shadowed by a real route is dead config
11
+ * — and it says each one as a **Problem naming the rule** (studio.md §16) rather than a toast that
12
+ * disappears before the author has read the path it printed.
13
+ *
14
+ * **Every write goes through the one door.** Rows commit with {@link commitProjectConfig}, so a
15
+ * redirect edit is a transaction on the `project.json` document with the same undo, the same
16
+ * serialisation and the same refusal-with-a-Problem as every other configuration write (P6.1). This
17
+ * module never touches `platform.writeFile`.
18
+ *
19
+ * **Row keys are positions.** A redirect's identity in `project.json` IS its source string, and the
20
+ * source is the cell an author most often edits — so keying rows by it would turn every rename into
21
+ * a delete plus an insert and lose the row's pending edits. Positions survive a rename; an insert
22
+ * or a delete is structural and makes the controller reload, which re-keys everything.
23
+ *
24
+ * @docs studio/projects/settings
25
+ */
26
+
27
+ import { html } from "lit-html";
28
+ import { getPlatform } from "../platform";
29
+ import { projectState } from "../store";
30
+ import { clearProblems, notify } from "../services/notify";
31
+ import { showDialog } from "../ui/layers";
32
+ import { activateTab, openTab, workspace } from "../workspace/workspace";
33
+ import { PROJECT_CONFIG_PATH, commitProjectConfig } from "../tabs/project-config";
34
+ import { pageRoute } from "../panels/tab-strip";
35
+ import { createGridController, getGridController } from "./grid-controller";
36
+ import {
37
+ REDIRECT_TARGETS,
38
+ REWRITE,
39
+ configFromRules,
40
+ parseRedirectImport,
41
+ rulesFromConfig,
42
+ validateRedirects,
43
+ } from "./redirects";
44
+ import { optionalStringArg, stringProperty } from "../commands/command-args";
45
+ import type { RedirectConfig, RedirectRule, RedirectTarget } from "./redirects";
46
+ import type {
47
+ CommitResult,
48
+ GridCellValue,
49
+ GridColumn,
50
+ GridEditBatch,
51
+ GridRow,
52
+ GridRowsResult,
53
+ GridSource,
54
+ } from "./grid-source";
55
+ import type { GridController } from "./grid-controller";
56
+ import type { ProjectConfig } from "@jxsuite/schema/types";
57
+ import type { AnyCommand, CommandRegistry } from "../commands/registry";
58
+
59
+ /**
60
+ * The redirect table's tab id.
61
+ *
62
+ * A `grid://` id, in the space `grid-source.ts` owns — but NOT yet one of its `GridTabRef` kinds,
63
+ * which is a one-line union member plus three switch cases in a file this workstream does not own.
64
+ * Until that lands `gridTabLabel()` cannot name the tab and the strip falls back to "Untitled".
65
+ */
66
+ export const REDIRECTS_TAB_ID = "grid://redirects";
67
+
68
+ /** Who the Problems list attributes a redirect finding to. Also the key it clears them by. */
69
+ export const REDIRECTS_PROBLEM_SOURCE = "Redirects";
70
+
71
+ /** Placeholder document for the virtual tab — the grid never reads it; save routes to the source. */
72
+ const GRID_STUB_DOCUMENT = { children: [], tagName: "div" };
73
+
74
+ /** Extensions the router treats as pages. Anything else under `pages/` is an asset, not a route. */
75
+ const PAGE_EXTENSIONS = [".json", ".md", ".html"];
76
+
77
+ // ─── Columns and rows ─────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Three columns, all editable.
81
+ *
82
+ * `source` is not declared `pk`: a primary-key column is frozen and read-only by contract, and
83
+ * retargeting an old URL is the second most common edit a redirect table sees.
84
+ */
85
+ export function redirectColumns(): GridColumn[] {
86
+ return [
87
+ {
88
+ editable: true,
89
+ field: "source",
90
+ kind: "string",
91
+ required: true,
92
+ title: "Source",
93
+ widthHint: 260,
94
+ },
95
+ {
96
+ editable: true,
97
+ field: "destination",
98
+ kind: "string",
99
+ required: true,
100
+ title: "Destination",
101
+ widthHint: 260,
102
+ },
103
+ {
104
+ editable: true,
105
+ field: "status",
106
+ kind: "enum",
107
+ schema: { enum: REDIRECT_TARGETS.map(String) },
108
+ title: "Status",
109
+ widthHint: 110,
110
+ },
111
+ ];
112
+ }
113
+
114
+ /** One rule as a grid row. Status is text because that is what an enum editor traffics in. */
115
+ export function redirectRow(rule: RedirectRule, index: number): GridRow {
116
+ return {
117
+ cells: { destination: rule.destination, source: rule.source, status: String(rule.status) },
118
+ key: String(index),
119
+ };
120
+ }
121
+
122
+ /** The live redirects map, straight off the configuration document. */
123
+ function currentConfig(): RedirectConfig | undefined {
124
+ return projectState?.projectConfig?.redirects as RedirectConfig | undefined;
125
+ }
126
+
127
+ /**
128
+ * A cell's text back to a target. An unrecognised value becomes the string itself, so
129
+ * {@link ruleErrors} can name what the author actually typed rather than reporting a silent 0.
130
+ */
131
+ function parseTarget(raw: string): RedirectTarget {
132
+ const trimmed = raw.trim();
133
+ if (trimmed.toLowerCase() === REWRITE) {
134
+ return REWRITE;
135
+ }
136
+ const value = Number(trimmed);
137
+ return Number.isInteger(value) ? (value as RedirectTarget) : (trimmed as RedirectTarget);
138
+ }
139
+
140
+ /** A cell bag back to a rule. An unrecognised target is refused by {@link ruleErrors}. */
141
+ function ruleFromCells(cells: Record<string, GridCellValue>): RedirectRule {
142
+ const text = (value: GridCellValue | undefined) =>
143
+ value === null || value === undefined ? "" : String(value);
144
+ return {
145
+ destination: text(cells.destination).trim(),
146
+ source: text(cells.source).trim(),
147
+ status: parseTarget(text(cells.status)),
148
+ };
149
+ }
150
+
151
+ /** Why one rule cannot be written, or null. */
152
+ function ruleErrors(rule: RedirectRule): string | null {
153
+ if (rule.source === "") {
154
+ return "A source path is required.";
155
+ }
156
+ if (rule.destination === "") {
157
+ return "A destination is required.";
158
+ }
159
+ if (!REDIRECT_TARGETS.includes(rule.status)) {
160
+ return `Status must be one of ${REDIRECT_TARGETS.join(", ")}.`;
161
+ }
162
+ return null;
163
+ }
164
+
165
+ // ─── Routes, for the shadow check ─────────────────────────────────────────────
166
+
167
+ export interface ProjectRoutes {
168
+ routes: string[];
169
+ /** False when a directory could not be listed — the shadow check MUST NOT run on a partial list. */
170
+ complete: boolean;
171
+ }
172
+
173
+ /**
174
+ * The site's real routes, derived from `pages/` by the same function the tab strip labels with.
175
+ *
176
+ * `complete` is the honest half: an unreadable directory means the route list is short, and a short
177
+ * list makes every shadowed rule look clean. The caller reports the gap instead of the finding.
178
+ */
179
+ export async function projectRoutes(): Promise<ProjectRoutes> {
180
+ const platform = getPlatform();
181
+ const routes = new Set<string>();
182
+ let complete = true;
183
+
184
+ const walkDir = async (dir: string): Promise<void> => {
185
+ let entries;
186
+ try {
187
+ entries = await platform.listDirectory(dir);
188
+ } catch {
189
+ complete = false;
190
+ return;
191
+ }
192
+ for (const entry of entries) {
193
+ const path = entry.path || `${dir}/${entry.name}`;
194
+ if (entry.type === "directory") {
195
+ await walkDir(path);
196
+ } else if (PAGE_EXTENSIONS.some((ext) => path.endsWith(ext))) {
197
+ const route = pageRoute(path);
198
+ if (route !== null) {
199
+ routes.add(route);
200
+ }
201
+ }
202
+ }
203
+ };
204
+ await walkDir("pages");
205
+ return { complete, routes: [...routes] };
206
+ }
207
+
208
+ /**
209
+ * Re-file the redirect Problems: one per finding, named by its rule, keyed so a re-run replaces
210
+ * rather than stacks.
211
+ *
212
+ * Previous findings are cleared first, so a fixed chain stops being listed without anybody having
213
+ * kept its record id — and a run that finds nothing leaves an empty list rather than a stale one.
214
+ *
215
+ * @returns The number of Problems filed.
216
+ */
217
+ export function reportRedirectProblems(
218
+ rules: readonly RedirectRule[],
219
+ routes: ProjectRoutes,
220
+ ): number {
221
+ clearProblems((record) => record.source === REDIRECTS_PROBLEM_SOURCE);
222
+ const problems = validateRedirects(rules, routes.complete ? routes.routes : []);
223
+ for (const problem of problems) {
224
+ notify(problem.rule === "loop" ? "error" : "warn", problem.message, {
225
+ action: "redirects.open",
226
+ detail: problem.detail,
227
+ key: `redirects.${problem.rule}:${problem.source}`,
228
+ path: PROJECT_CONFIG_PATH,
229
+ source: REDIRECTS_PROBLEM_SOURCE,
230
+ tier: "problem",
231
+ });
232
+ }
233
+ if (!routes.complete) {
234
+ notify.warn("Redirects were checked for chains and loops only.", {
235
+ action: "redirects.validate",
236
+ detail:
237
+ "pages/ could not be listed, so this run cannot say whether a rule is shadowed by a real " +
238
+ "route. Reporting no shadowed rules from an incomplete list would be a guess.",
239
+ key: "redirects.routes",
240
+ path: "pages",
241
+ source: REDIRECTS_PROBLEM_SOURCE,
242
+ tier: "problem",
243
+ });
244
+ return problems.length + 1;
245
+ }
246
+ return problems.length;
247
+ }
248
+
249
+ // ─── The source ───────────────────────────────────────────────────────────────
250
+
251
+ /**
252
+ * The redirect table as a grid source.
253
+ *
254
+ * Rows are read from the live configuration each time, because `project.json` is a document and an
255
+ * undo on its tab is a legitimate way for the table to change underneath this.
256
+ */
257
+ export function createRedirectsSource(): GridSource {
258
+ const load = () => rulesFromConfig(currentConfig());
259
+
260
+ return {
261
+ backingPaths: () => new Map([[PROJECT_CONFIG_PATH, "*"]]),
262
+ capabilities: { delete: true, insert: true, remotePaging: false, remoteSort: false },
263
+ columns: () => Promise.resolve(redirectColumns()),
264
+
265
+ /**
266
+ * Write the batch, then say what happened to **every** row in it.
267
+ *
268
+ * The result is not a log: `edit-buffer.ts`'s `applyCommitResult` clears a pending change only
269
+ * when an outcome naming it comes back `ok`. An arm that writes the file and reports nothing
270
+ * leaves its rows pending forever — the dirty dot never goes out, `grid-controller` counts the
271
+ * write as a failure and skips the reload, and the table shows a phantom row on top of the one
272
+ * it just saved. All three arms report; a refusal reports through {@link refuseAll}.
273
+ */
274
+ async commit(batch: GridEditBatch): Promise<CommitResult> {
275
+ const rules = load();
276
+ const result: CommitResult = { cells: [], deletes: [], inserts: [] };
277
+
278
+ // Cell edits land on the rule at that position; a row whose position no longer exists is a
279
+ // Row the author cannot see, so it is refused rather than written somewhere else.
280
+ for (const cell of batch.cells) {
281
+ const rule = rules[Number(cell.rowKey)];
282
+ if (!rule) {
283
+ result.cells.push({
284
+ error: "This row is no longer in project.json — refresh the grid.",
285
+ field: cell.field,
286
+ ok: false,
287
+ rowKey: cell.rowKey,
288
+ stale: true,
289
+ });
290
+ continue;
291
+ }
292
+ const text = cell.value === null ? "" : String(cell.value);
293
+ if (cell.field === "status") {
294
+ rule.status = parseTarget(text);
295
+ } else if (cell.field === "source") {
296
+ rule.source = text.trim();
297
+ } else {
298
+ rule.destination = text.trim();
299
+ }
300
+ result.cells.push({ field: cell.field, ok: true, rowKey: cell.rowKey });
301
+ }
302
+
303
+ const deleted = new Set(batch.deletes.map((row) => Number(row.rowKey)));
304
+ const next = rules.filter((_rule, index) => !deleted.has(index));
305
+ for (const row of batch.deletes) {
306
+ result.deletes.push({ ok: true, rowKey: row.rowKey });
307
+ }
308
+ for (const insert of batch.inserts) {
309
+ next.push(ruleFromCells(insert.cells));
310
+ result.inserts.push({ ok: true, tempKey: insert.tempKey });
311
+ }
312
+
313
+ // Validity and uniqueness are decided BEFORE anything is written: a half-written redirects
314
+ // Map is a site with some of its old URLs working.
315
+ const seen = new Set<string>();
316
+ const failures: string[] = [];
317
+ for (const rule of next) {
318
+ const error = ruleErrors(rule) ?? (seen.has(rule.source) ? `Duplicate source.` : null);
319
+ if (error) {
320
+ failures.push(`${rule.source || "(blank)"} — ${error}`);
321
+ }
322
+ seen.add(rule.source);
323
+ }
324
+ if (failures.length > 0) {
325
+ return refuseAll(batch, failures.join(" "));
326
+ }
327
+
328
+ // `undefined` CLEARS the key (see `commitProjectConfig`): an empty table should leave no
329
+ // `"redirects": {}` behind. The local type spells the undefined out, which
330
+ // `exactOptionalPropertyTypes` will not let `Partial<ProjectConfig>` do.
331
+ const patch: { redirects: RedirectConfig | undefined } = {
332
+ redirects: next.length > 0 ? configFromRules(next) : undefined,
333
+ };
334
+ const commit = await commitProjectConfig(patch as Partial<ProjectConfig>);
335
+ if (!commit.ok) {
336
+ // `commitProjectConfig` already filed the Problem; repeating it would list one failure
337
+ // Twice. The cell errors are what keep the rows pending and marked.
338
+ return refuseAll(batch, "project.json could not be written — see Problems.");
339
+ }
340
+ reportRedirectProblems(next, await projectRoutes());
341
+ return result;
342
+ },
343
+
344
+ id: REDIRECTS_TAB_ID,
345
+ label: "Redirects",
346
+ refresh: () => Promise.resolve(),
347
+ rows(): Promise<GridRowsResult> {
348
+ const rules = load();
349
+ return Promise.resolve({
350
+ rows: rules.map((rule, index) => redirectRow(rule, index)),
351
+ total: rules.length,
352
+ });
353
+ },
354
+ };
355
+ }
356
+
357
+ /** Mark every pending change failed with one reason — a refusal is all-or-nothing here. */
358
+ function refuseAll(batch: GridEditBatch, error: string): CommitResult {
359
+ return {
360
+ cells: batch.cells.map((cell) => ({
361
+ error,
362
+ field: cell.field,
363
+ ok: false,
364
+ rowKey: cell.rowKey,
365
+ })),
366
+ deletes: batch.deletes.map((row) => ({ error, ok: false, rowKey: row.rowKey })),
367
+ inserts: batch.inserts.map((row) => ({ error, ok: false, tempKey: row.tempKey })),
368
+ };
369
+ }
370
+
371
+ // ─── Opening ──────────────────────────────────────────────────────────────────
372
+
373
+ /**
374
+ * Open (or activate) the redirect table, and hand back its controller.
375
+ *
376
+ * Idempotent, and awaits the first load, so a caller that wants to stage imported rows into the
377
+ * buffer has columns to stage them against.
378
+ */
379
+ export async function openRedirectsGrid(): Promise<GridController> {
380
+ const existing = workspace.tabs.get(REDIRECTS_TAB_ID);
381
+ if (existing) {
382
+ activateTab(REDIRECTS_TAB_ID);
383
+ const controller = getGridController(existing);
384
+ if (controller) {
385
+ return controller;
386
+ }
387
+ }
388
+ const tab =
389
+ existing ??
390
+ openTab({
391
+ capabilities: { modes: ["grid"] },
392
+ document: structuredClone(GRID_STUB_DOCUMENT),
393
+ documentPath: null,
394
+ id: REDIRECTS_TAB_ID,
395
+ });
396
+ const controller = createGridController(tab, createRedirectsSource());
397
+ await controller.load();
398
+ return controller;
399
+ }
400
+
401
+ // ─── Import ───────────────────────────────────────────────────────────────────
402
+
403
+ /** The paste box. One textarea, because both formats are things an author copies out of a file. */
404
+ export function promptRedirectImport(): Promise<string | null> {
405
+ let text = "";
406
+ return showDialog<string | null>(
407
+ (done) => html`
408
+ <sp-dialog-wrapper
409
+ open
410
+ dismissable
411
+ underlay
412
+ headline="Import Redirects"
413
+ confirm-label="Import"
414
+ cancel-label="Cancel"
415
+ @confirm=${() => done(text)}
416
+ @cancel=${() => done(null)}
417
+ @close=${() => done(null)}
418
+ >
419
+ <p>
420
+ Paste a Netlify/Cloudflare <code>_redirects</code> file, or CSV with
421
+ <code>source,destination,status</code> columns. Rows are staged for review — nothing is
422
+ written until you save.
423
+ </p>
424
+ <textarea
425
+ class="jx-grid-input jx-redirect-import"
426
+ rows="10"
427
+ placeholder="/old-page /new-page 301"
428
+ @input=${(e: Event) => (text = (e.target as HTMLTextAreaElement).value)}
429
+ ></textarea>
430
+ </sp-dialog-wrapper>
431
+ `,
432
+ { region: "redirects/import" },
433
+ );
434
+ }
435
+
436
+ /**
437
+ * Stage parsed rules as pending rows on the open table.
438
+ *
439
+ * Staged, not written: an import is the one redirect edit an author has not read line by line, so
440
+ * it arrives in the same reviewable, undoable buffer a typed row does. A source the table already
441
+ * holds is skipped and named — silently overwriting a rule with a pasted one is how an import
442
+ * quietly retargets a URL that was already correct.
443
+ *
444
+ * @returns How many rows were staged.
445
+ */
446
+ export function stageRedirectImport(
447
+ controller: GridController,
448
+ rules: readonly RedirectRule[],
449
+ errors: readonly string[],
450
+ ): number {
451
+ const existing = new Set(
452
+ controller.effectiveRows().map((row) => String(row.source ?? "").trim()),
453
+ );
454
+ const skipped: string[] = [];
455
+ let staged = 0;
456
+ for (const rule of rules) {
457
+ if (existing.has(rule.source)) {
458
+ skipped.push(rule.source);
459
+ continue;
460
+ }
461
+ existing.add(rule.source);
462
+ controller.addRow({
463
+ destination: rule.destination,
464
+ source: rule.source,
465
+ status: String(rule.status),
466
+ });
467
+ staged += 1;
468
+ }
469
+
470
+ const notes = [
471
+ ...errors,
472
+ ...(skipped.length > 0 ? [`Already in the table, skipped: ${skipped.join(", ")}.`] : []),
473
+ ];
474
+ if (notes.length > 0) {
475
+ notify.warn(
476
+ `Imported ${staged} redirect${staged === 1 ? "" : "s"} · ${notes.length} line${notes.length === 1 ? "" : "s"} not imported.`,
477
+ {
478
+ action: "redirects.import",
479
+ detail: notes.join("\n"),
480
+ key: "redirects.import",
481
+ path: PROJECT_CONFIG_PATH,
482
+ source: REDIRECTS_PROBLEM_SOURCE,
483
+ tier: "problem",
484
+ },
485
+ );
486
+ } else if (staged > 0) {
487
+ notify.success(`Staged ${staged} redirect${staged === 1 ? "" : "s"} — review, then save.`, {
488
+ action: "file.save",
489
+ key: "redirects.import",
490
+ });
491
+ } else {
492
+ notify.info("Nothing to import.", { key: "redirects.import" });
493
+ }
494
+ return staged;
495
+ }
496
+
497
+ // ─── Commands ─────────────────────────────────────────────────────────────────
498
+
499
+ /**
500
+ * The redirect verbs.
501
+ *
502
+ * All three are `project` level: a redirect belongs to the site, not to whichever document happens
503
+ * to be open. `redirects.validate` exists as a command because it is the `action` a Problem can
504
+ * offer, and because "check this again" is what an author wants after fixing one.
505
+ *
506
+ * @returns {AnyCommand[]}
507
+ */
508
+ export function redirectsCommands(): AnyCommand[] {
509
+ return [
510
+ {
511
+ category: "Project",
512
+ id: "redirects.open",
513
+ level: "project",
514
+ menus: ["palette"],
515
+ group: "5_data",
516
+ requires: "an open project",
517
+ when: (ctx) => ctx.project.open,
518
+ aiTool: {
519
+ description:
520
+ "Open the site's redirect rules as an editable table, validated for chains, loops and rules shadowed by a real page.",
521
+ name: "open_redirects",
522
+ },
523
+ run: async () => {
524
+ await openRedirectsGrid();
525
+ reportRedirectProblems(rulesFromConfig(currentConfig()), await projectRoutes());
526
+ },
527
+ title: "Edit Redirects",
528
+ },
529
+ {
530
+ category: "Project",
531
+ id: "redirects.validate",
532
+ level: "project",
533
+ menus: ["palette"],
534
+ group: "5_data",
535
+ requires: "an open project",
536
+ when: (ctx) => ctx.project.open,
537
+ aiTool: {
538
+ description:
539
+ "Check the site's redirect rules for chains, loops and rules shadowed by a real page. Reports each finding as a Problem.",
540
+ name: "validate_redirects",
541
+ },
542
+ run: async () => {
543
+ const rules = rulesFromConfig(currentConfig());
544
+ const filed = reportRedirectProblems(rules, await projectRoutes());
545
+ if (filed === 0) {
546
+ notify.success(
547
+ `${rules.length} redirect${rules.length === 1 ? "" : "s"} checked — no chains, loops or shadowed rules.`,
548
+ { key: "redirects.validate" },
549
+ );
550
+ }
551
+ },
552
+ title: "Validate Redirects",
553
+ },
554
+ {
555
+ args: {
556
+ additionalProperties: false,
557
+ properties: {
558
+ text: stringProperty(
559
+ "A _redirects file or CSV to import. Omit it and Studio asks for a paste.",
560
+ ),
561
+ },
562
+ required: [],
563
+ type: "object",
564
+ },
565
+ category: "Project",
566
+ id: "redirects.import",
567
+ level: "project",
568
+ menus: ["palette"],
569
+ group: "5_data",
570
+ requires: "an open project",
571
+ when: (ctx) => ctx.project.open,
572
+ aiTool: {
573
+ description:
574
+ "Import redirect rules from _redirects or CSV text. Rows are staged in the redirect table for review, not written.",
575
+ name: "import_redirects",
576
+ },
577
+ run: async (_ctx, args) => {
578
+ const given = optionalStringArg("redirects.import", args, "text");
579
+ const text = given ?? (await promptRedirectImport());
580
+ if (text === null || text.trim() === "") {
581
+ return;
582
+ }
583
+ const parsed = parseRedirectImport(text);
584
+ const controller = await openRedirectsGrid();
585
+ stageRedirectImport(controller, parsed.rules, parsed.errors);
586
+ },
587
+ title: "Import Redirects…",
588
+ },
589
+ ];
590
+ }
591
+
592
+ /**
593
+ * Register the redirect commands.
594
+ *
595
+ * @param {CommandRegistry} registry
596
+ */
597
+ export function registerRedirectsCommands(registry: CommandRegistry): void {
598
+ registry.registerAll(redirectsCommands());
599
+ }