@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
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  import { getPlatform } from "../platform";
12
+ import { invalidateUsages } from "../services/references";
12
13
  import { projectState } from "../store";
13
14
  import type { DirEntry, FsEvent } from "../types";
14
15
 
@@ -116,6 +117,15 @@ export interface FsSyncContext {
116
117
  renderLeftPanel: () => void;
117
118
  /** Optional hook for an external content change to an open file (e.g. reload a clean tab). */
118
119
  onContentChange?: (path: string) => void;
120
+ /**
121
+ * Drop every cache keyed on "what files the project contains".
122
+ *
123
+ * Passed in rather than imported, for the reason `renderLeftPanel` is: the caches live in panels,
124
+ * and this module is imported BY the file layer those panels sit on top of — reaching up would be
125
+ * a cycle. The bootstrap owns the list because the bootstrap is where the panels are already in
126
+ * scope.
127
+ */
128
+ invalidateDerivedCaches?: () => void;
119
129
  }
120
130
 
121
131
  /**
@@ -153,6 +163,17 @@ export function startFsSync(ctx: FsSyncContext): () => void {
153
163
  };
154
164
 
155
165
  return platform.subscribeFileEvents((events) => {
166
+ // Before the echo filter, deliberately. `isRecentLocal` drops the events Studio caused, which
167
+ // Is right for the tree (it already repainted) and wrong for a DERIVED cache — Studio's own
168
+ // Write changes what the project contains exactly as much as anyone else's does.
169
+ //
170
+ // Every cache keyed on "what files exist" is dropped here, in one place, because they answer
171
+ // One event. Each one had an invalidator and no caller, and each stale answer is visible: the
172
+ // Link-target picker offering a route whose page was deleted, the layout picker attributing a
173
+ // Removed layout's `$head` to the open page, a `$paths` enumeration listing entries that are
174
+ // Gone.
175
+ invalidateUsages();
176
+ ctx.invalidateDerivedCaches?.();
156
177
  pending.push(...events);
157
178
  if (timer) {
158
179
  clearTimeout(timer);
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Media metadata — what a file IS, for surfaces that show media rather than resolve it.
3
+ *
4
+ * Three facts, and each has an "unknown" that is not a zero:
5
+ *
6
+ * - **Size and modified time** come from the directory listing the caller already performed. A media
7
+ * surface enumerates a directory to draw itself, so `seedMediaMeta` folds those entries in for
8
+ * free and {@link loadMediaMeta} is the fallback for a path nobody enumerated.
9
+ * - **Pixel dimensions** are OPPORTUNISTIC. There is no stat that returns them and no decoder in the
10
+ * shell, so the only honest source is an `<img>` that already loaded — which every media surface
11
+ * renders anyway. {@link recordImageSize} is how that surface hands the number back, and until
12
+ * one does, the dimensions are `null` and the summary simply omits them. Downloading the file a
13
+ * second time to measure it would be a network request per thumbnail for a line of text.
14
+ *
15
+ * A missing number is `null` everywhere, never `0`. `0 × 0` and `0 B` are things a real file can
16
+ * almost be, so they must not double as "we did not find out" — the same rule
17
+ * `services/references.ts` applies to a count it cannot answer.
18
+ *
19
+ * The cache is dropped wholesale by {@link invalidateMediaMeta}, wired to the same post-upload
20
+ * invalidation the media picker's listing uses. There is no TTL: a size that expires on a timer is
21
+ * wrong at an unannounced moment.
22
+ *
23
+ * @docs studio/projects/media
24
+ */
25
+
26
+ import { getPlatform } from "../platform";
27
+ import { baseName, dirName, normalizeProjectPath } from "./media-paths";
28
+ import { extensionOf, mediaKind } from "./media-upload";
29
+ import type { AssetKind } from "./media-upload";
30
+ import type { DirEntry } from "../types";
31
+
32
+ /** Everything a media surface can say about one file without opening it. */
33
+ export interface MediaMeta {
34
+ /** Project-relative, forward-slashed. */
35
+ path: string;
36
+ /** Filename only. */
37
+ name: string;
38
+ /** Lowercased extension including the dot, or `""`. */
39
+ ext: string;
40
+ /** What kind of element it wants to become — the same classification `uploadAssets` records. */
41
+ kind: AssetKind;
42
+ /** Bytes on disk, or null when the listing did not say. */
43
+ bytes: number | null;
44
+ /** ISO modification time, or null when the listing did not say. */
45
+ modified: string | null;
46
+ /** Intrinsic width in CSS pixels, or null until an `<img>` for this file has loaded. */
47
+ width: number | null;
48
+ /** Intrinsic height in CSS pixels, or null until an `<img>` for this file has loaded. */
49
+ height: number | null;
50
+ }
51
+
52
+ /** Settled metadata, by project-relative path. */
53
+ const settled = new Map<string, MediaMeta>();
54
+ /** In-flight listings, so N surfaces asking about one path share one round trip. */
55
+ const inFlight = new Map<string, Promise<MediaMeta>>();
56
+ /**
57
+ * Dimensions reported by a loaded `<img>`, kept beside the cache rather than inside it.
58
+ *
59
+ * A surface can measure a thumbnail before anyone asks for its size — and a `loadMediaMeta` that
60
+ * landed afterwards would otherwise overwrite the one fact that cost a decode to learn.
61
+ */
62
+ const measured = new Map<string, { width: number; height: number }>();
63
+
64
+ /** Build a meta record from a path and whatever the listing knew about it. */
65
+ function metaFrom(path: string, entry: DirEntry | undefined): MediaMeta {
66
+ const name = baseName(path);
67
+ const dims = measured.get(path);
68
+ return {
69
+ bytes: typeof entry?.size === "number" ? entry.size : null,
70
+ ext: extensionOf(name),
71
+ height: dims?.height ?? null,
72
+ kind: mediaKind({ name }),
73
+ modified: entry?.modified ?? null,
74
+ name,
75
+ path,
76
+ width: dims?.width ?? null,
77
+ };
78
+ }
79
+
80
+ /**
81
+ * The metadata known right now, WITHOUT starting a request.
82
+ *
83
+ * Templates call this: a lit render is synchronous, and a render that listed a directory every time
84
+ * it ran would re-list on its own repaint.
85
+ */
86
+ export function peekMediaMeta(path: string): MediaMeta | null {
87
+ return settled.get(normalizeProjectPath(path)) ?? null;
88
+ }
89
+
90
+ /**
91
+ * Fold a directory listing into the cache — the cheap path, for a surface that just enumerated.
92
+ *
93
+ * Only file entries are recorded, and an entry never overwrites a settled record: the listing that
94
+ * arrives second is not more truthful than the one that arrived first, and replacing the record
95
+ * would drop measured dimensions from the object templates are already holding.
96
+ */
97
+ export function seedMediaMeta(entries: Iterable<DirEntry>): void {
98
+ for (const entry of entries) {
99
+ if (entry.type !== "file") {
100
+ continue;
101
+ }
102
+ const path = normalizeProjectPath(entry.path);
103
+ if (!settled.has(path)) {
104
+ settled.set(path, metaFrom(path, entry));
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Ask for one file's metadata, or join the ask already in flight.
111
+ *
112
+ * Never rejects. A directory that cannot be listed yields a record whose numbers are `null` — "we
113
+ * do not know how big it is" is a renderable answer, and a thrown error at this altitude would only
114
+ * be caught and turned back into one by every caller.
115
+ */
116
+ export async function loadMediaMeta(path: string): Promise<MediaMeta> {
117
+ const key = normalizeProjectPath(path);
118
+ const done = settled.get(key);
119
+ if (done) {
120
+ return done;
121
+ }
122
+ const running = inFlight.get(key);
123
+ if (running) {
124
+ return running;
125
+ }
126
+ const pending = (async (): Promise<MediaMeta> => {
127
+ let entry: DirEntry | undefined;
128
+ try {
129
+ const listing = await getPlatform().listDirectory(dirName(key));
130
+ entry = listing.find((item) => normalizeProjectPath(item.path) === key);
131
+ } catch {
132
+ // Unreadable or not yet created — the size stays unknown rather than becoming zero.
133
+ entry = undefined;
134
+ }
135
+ return metaFrom(key, entry);
136
+ })();
137
+ inFlight.set(key, pending);
138
+ const meta = await pending;
139
+ inFlight.delete(key);
140
+ settled.set(key, meta);
141
+ return meta;
142
+ }
143
+
144
+ /**
145
+ * Record the intrinsic size of a file whose `<img>` has loaded.
146
+ *
147
+ * @returns Whether this was new information — the caller repaints only when it was, so a template
148
+ * that re-renders its thumbnails (and re-fires their `load` from cache) does not loop.
149
+ */
150
+ export function recordImageSize(path: string, width: number, height: number): boolean {
151
+ if (!(width > 0 && height > 0)) {
152
+ return false; // A broken or still-decoding image reports 0 — that is not a measurement.
153
+ }
154
+ const key = normalizeProjectPath(path);
155
+ const known = measured.get(key);
156
+ if (known && known.width === width && known.height === height) {
157
+ return false;
158
+ }
159
+ measured.set(key, { height, width });
160
+ const meta = settled.get(key);
161
+ if (meta) {
162
+ settled.set(key, { ...meta, height, width });
163
+ }
164
+ return true;
165
+ }
166
+
167
+ /** Drop every cached record and measurement. Called wherever the media listing is invalidated. */
168
+ export function invalidateMediaMeta(): void {
169
+ settled.clear();
170
+ measured.clear();
171
+ }
172
+
173
+ /** `84 KB`, `1.2 MB`, `903 B` — one significant fraction, because this is a caption. */
174
+ export function formatBytes(bytes: number): string {
175
+ if (bytes < 1024) {
176
+ return `${bytes} B`;
177
+ }
178
+ if (bytes < 1024 * 1024) {
179
+ return `${Math.round(bytes / 1024)} KB`;
180
+ }
181
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
182
+ }
183
+
184
+ /**
185
+ * The one-line caption a media surface shows: `1200 × 800 · 84 KB`.
186
+ *
187
+ * Unknown facts are omitted rather than rendered as a placeholder, so the line shortens instead of
188
+ * filling with dashes. It is `""` when nothing at all is known, which a template renders as
189
+ * nothing.
190
+ */
191
+ export function mediaMetaSummary(meta: MediaMeta | null): string {
192
+ if (!meta) {
193
+ return "";
194
+ }
195
+ const parts: string[] = [];
196
+ if (meta.width !== null && meta.height !== null) {
197
+ parts.push(`${meta.width} × ${meta.height}`);
198
+ }
199
+ if (meta.bytes !== null) {
200
+ parts.push(formatBytes(meta.bytes));
201
+ }
202
+ return parts.join(" · ");
203
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Media path math — the file, the URL it is published at, and the shapes an author writes.
3
+ *
4
+ * One media file has up to three names, and they are not interchangeable:
5
+ *
6
+ * | name | example | who writes it |
7
+ * | ---------------- | -------------------------------- | ---------------------------------- |
8
+ * | the FILE | `public/hero.jpg` | the file tree, `uploadAssets` |
9
+ * | the SITE URL | `/hero.jpg` | the browser, a preview `<img src>` |
10
+ * | the AUTHORED ref | `/hero.jpg`, `./images/hero.png` | the document on disk |
11
+ *
12
+ * `public/` is served from the site root, so a document that uses `public/hero.jpg` says
13
+ * `/hero.jpg` — a string that shares not one path segment with the file it names. A content entry
14
+ * says `./images/hero.png` and the collection loader republishes it at the content type's asset
15
+ * mount (specs/site-architecture.md §9.3), so the same file can also be named
16
+ * `/content/blog/images/hero.png` when the collection's `source` is not already `content/blog`.
17
+ *
18
+ * **Why this module exists at all.** `findReferences` resolves each authored ref the way the
19
+ * compiler does — rooted values against the project root, relative ones against the referencing
20
+ * document — and compares the RESULT to the path it was asked about. Ask it about `public/hero.jpg`
21
+ * and every `/hero.jpg` in the project resolves to `hero.jpg`, matches nothing, and the answer is a
22
+ * confident zero. That is the exact shape of bug that makes a delete look safe: seven pages break
23
+ * and the dialog says nothing else refers to it. {@link authoredRefTargets} is the fix — it
24
+ * enumerates what an authored reference to this file RESOLVES to, so the query is keyed on the ref
25
+ * as written rather than on the file as stored.
26
+ *
27
+ * Everything here is pure string math over project-relative, forward-slashed paths. The only input
28
+ * from outside is the open project's content sections, read the same way `content-assets.ts` reads
29
+ * them.
30
+ *
31
+ * @docs studio/projects/media
32
+ */
33
+
34
+ import { contentMountFor } from "../canvas/content-assets";
35
+ import { projectState } from "../store";
36
+ import type { AssetMount } from "@jxsuite/schema/asset-paths";
37
+ import type { ContentSectionEntry } from "../types";
38
+
39
+ /** The directory whose contents are served from the site root. */
40
+ const PUBLIC_DIR = "public";
41
+
42
+ /**
43
+ * A project-relative path in the one spelling every function here expects: forward slashes, no `./`
44
+ * prefix, no leading `/`, no trailing `/`.
45
+ */
46
+ export function normalizeProjectPath(path: string): string {
47
+ return path.replaceAll("\\", "/").replace(/^\.\//, "").replace(/^\/+/, "").replace(/\/+$/, "");
48
+ }
49
+
50
+ /** The filename of a project-relative path. */
51
+ export function baseName(path: string): string {
52
+ const normalized = normalizeProjectPath(path);
53
+ const slash = normalized.lastIndexOf("/");
54
+ return slash === -1 ? normalized : normalized.slice(slash + 1);
55
+ }
56
+
57
+ /** The directory of a project-relative path, or `"."` for a file at the project root. */
58
+ export function dirName(path: string): string {
59
+ const normalized = normalizeProjectPath(path);
60
+ const slash = normalized.lastIndexOf("/");
61
+ return slash === -1 ? "." : normalized.slice(0, slash);
62
+ }
63
+
64
+ /** The open project's content sections, or null when no project (or no `content:`) is loaded. */
65
+ function contentSections(): Record<string, ContentSectionEntry> | null {
66
+ const content = projectState?.projectConfig?.content;
67
+ return (content as Record<string, ContentSectionEntry> | undefined) ?? null;
68
+ }
69
+
70
+ /**
71
+ * The mount that publishes `path`, or null when it is not inside a content collection.
72
+ *
73
+ * `contentMountFor` takes a DOCUMENT path, but the test it performs — which collection directory
74
+ * contains this path — is exactly the one an asset needs, so the media file is passed straight in
75
+ * rather than duplicating the loop.
76
+ */
77
+ function mountOf(path: string): AssetMount | null {
78
+ return contentMountFor(path, contentSections());
79
+ }
80
+
81
+ /**
82
+ * The path a content-mounted file is published under, project-root-relative and WITHOUT the leading
83
+ * slash — `content/blog/images/hero.png` for `posts/images/hero.png` mounted at `/content/blog`.
84
+ *
85
+ * Built by string surgery rather than through `assetUrlFor` on purpose: that function
86
+ * percent-encodes each segment for use in an HTML attribute, and this value is compared against
87
+ * paths resolved from raw document strings, which are never decoded on the way in.
88
+ */
89
+ function mountedPath(path: string, mount: AssetMount): string {
90
+ const prefix = normalizeProjectPath(mount.urlPrefix);
91
+ return `${prefix}/${path.slice(mount.dir.length + 1)}`;
92
+ }
93
+
94
+ /**
95
+ * The site URL a media file is published at — what a preview `<img>` in panel chrome should load.
96
+ *
97
+ * `public/` contents are served from the root, a content asset from its collection's mount, and
98
+ * anything else from its own project-relative path (which is what the dev server serves).
99
+ */
100
+ export function mediaSiteUrl(path: string): string {
101
+ const normalized = normalizeProjectPath(path);
102
+ if (normalized.startsWith(`${PUBLIC_DIR}/`)) {
103
+ return `/${normalized.slice(PUBLIC_DIR.length + 1)}`;
104
+ }
105
+ const mount = mountOf(normalized);
106
+ return mount ? `/${mountedPath(normalized, mount)}` : `/${normalized}`;
107
+ }
108
+
109
+ /**
110
+ * Every path an authored reference to this media file resolves to — the keys a usage query must ask
111
+ * about, most literal first.
112
+ *
113
+ * 1. The file itself. A sibling's `./images/hero.png`, a rooted `/content/blog/images/hero.png` and a
114
+ * bare `assets/logo.svg` all land here.
115
+ * 2. Its served path, for `public/` media. `/hero.jpg` resolves to `hero.jpg`, which is not a file on
116
+ * disk and does not need to be — the sweep compares resolved strings, and this is the string
117
+ * every reference to a public asset produces.
118
+ * 3. Its asset-mount path, when a collection's `source` differs from its mount prefix. Identical to
119
+ * (1) for the ordinary `content/<type>/` layout, and deduplicated when it is.
120
+ *
121
+ * The list is a UNION and is deliberately generous: over-counting a reference makes a delete dialog
122
+ * more alarming than it needs to be, while missing one makes it lie.
123
+ */
124
+ export function authoredRefTargets(path: string): string[] {
125
+ const normalized = normalizeProjectPath(path);
126
+ if (normalized === "") {
127
+ return [];
128
+ }
129
+ const targets = [normalized];
130
+ if (normalized.startsWith(`${PUBLIC_DIR}/`)) {
131
+ targets.push(normalized.slice(PUBLIC_DIR.length + 1));
132
+ }
133
+ const mount = mountOf(normalized);
134
+ if (mount) {
135
+ const mounted = mountedPath(normalized, mount);
136
+ if (!targets.includes(mounted)) {
137
+ targets.push(mounted);
138
+ }
139
+ }
140
+ return targets;
141
+ }
@@ -18,7 +18,7 @@
18
18
 
19
19
  import { errorMessage } from "@jxsuite/schema/parse";
20
20
  import { getPlatform } from "../platform";
21
- import { statusMessage } from "../panels/statusbar";
21
+ import { notify } from "../services/notify";
22
22
  import { activeTab } from "../workspace/workspace";
23
23
 
24
24
  // ─── File classification ─────────────────────────────────────────────────────
@@ -234,12 +234,16 @@ export async function uploadAssets(
234
234
  await platform.uploadFile(path, file);
235
235
  uploaded.push({ kind: mediaKind(file), name, path, ref: assetRef(path) });
236
236
  } catch (error) {
237
- statusMessage(`Upload failed: ${file.name} — ${errorMessage(error)}`);
237
+ notify.error(`Could not upload ${file.name}.`, {
238
+ detail: errorMessage(error),
239
+ path,
240
+ source: "Media",
241
+ });
238
242
  }
239
243
  }
240
244
 
241
245
  if (uploaded.length > 0) {
242
- statusMessage(
246
+ notify.success(
243
247
  uploaded.length === 1
244
248
  ? `Uploaded ${uploaded[0]!.name} to ${dir}/`
245
249
  : `Uploaded ${uploaded.length} files to ${dir}/`,
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Media usage — "which pages use this image?", asked in the shape an author actually wrote.
3
+ *
4
+ * `services/references.ts` already answers "where is this used?" for documents, and its answer is
5
+ * what every destructive confirmation says out loud. Media does not fit through it unmodified, for
6
+ * one reason: **the reference index resolves refs, and a media file's authored ref usually resolves
7
+ * somewhere else.** `public/hero.jpg` is written `/hero.jpg`, which the compiler — and therefore
8
+ * the sweep — resolves to `hero.jpg`. Ask about the file and every reference to it is invisible;
9
+ * the query succeeds, returns zero, and the delete dialog says nothing else refers to it while
10
+ * seven pages break. A confident zero is the one answer a destructive dialog must never invent.
11
+ *
12
+ * So the query is keyed on the AUTHORED ref. {@link authoredRefTargets} enumerates every path a
13
+ * reference to this file resolves to — the file, its served path, its asset-mount path — and this
14
+ * module asks all of them and unions the answers. Everything else is deliberately inherited rather
15
+ * than re-implemented: the same `loadUsages` cache (so a dialog and a panel asking at once are one
16
+ * round trip), the same `invalidateUsages` on any filesystem event, the same `usageWarning`
17
+ * sentence in the confirmation, and the same `usageHeadline` wording on a panel.
18
+ *
19
+ * **A partial answer is `failed`, not a total.** If one lane of the union cannot be counted, the
20
+ * union cannot either — a number assembled from the lanes that worked reads exactly like a complete
21
+ * one, and would understate what a delete breaks. Unknown is the honest answer, and
22
+ * {@link mediaUsageHeadline} says the word.
23
+ *
24
+ * @docs studio/projects/media
25
+ */
26
+
27
+ import { authoredRefTargets } from "./media-paths";
28
+ import { loadUsages, peekUsages, retryUsages, usageHeadline } from "../services/references";
29
+ import type { UsageQuery, UsageState } from "../services/references";
30
+ import type { ReferenceFile, ReferenceHit, ReferencesResult } from "../types";
31
+
32
+ /**
33
+ * The queries whose union answers "what uses this media file?".
34
+ *
35
+ * One per authored form. An empty list means there is no question to ask (an empty path), which
36
+ * {@link loadMediaUsages} reports as a failure rather than as "nothing uses it".
37
+ */
38
+ export function mediaUsageQueries(path: string): UsageQuery[] {
39
+ return authoredRefTargets(path).map((target) => ({ path: target }));
40
+ }
41
+
42
+ /** Merge one file's hits into the accumulator, summing counts per `(refType, ref)`. */
43
+ function mergeFile(into: Map<string, ReferenceFile>, file: ReferenceFile): void {
44
+ const existing = into.get(file.path);
45
+ if (!existing) {
46
+ into.set(file.path, { count: file.count, path: file.path, refs: [...file.refs] });
47
+ return;
48
+ }
49
+ for (const hit of file.refs) {
50
+ const same = existing.refs.find(
51
+ (candidate: ReferenceHit) => candidate.refType === hit.refType && candidate.ref === hit.ref,
52
+ );
53
+ if (same) {
54
+ same.count += hit.count;
55
+ } else {
56
+ existing.refs.push({ ...hit });
57
+ }
58
+ }
59
+ existing.count += file.count;
60
+ }
61
+
62
+ /**
63
+ * One result out of several, for the same file asked about under different names.
64
+ *
65
+ * The lanes cannot double-count: each asks about a DIFFERENT resolved path, and one authored string
66
+ * resolves to exactly one of them. Errors are unioned by path so a document that failed to parse in
67
+ * every lane is reported once, and `path` reports the media file the caller asked about rather than
68
+ * whichever lane happened to be first.
69
+ */
70
+ function unionResults(path: string, results: readonly ReferencesResult[]): ReferencesResult {
71
+ const files = new Map<string, ReferenceFile>();
72
+ const errors = new Map<string, { path: string; error: string }>();
73
+ for (const result of results) {
74
+ for (const file of result.files) {
75
+ mergeFile(files, file);
76
+ }
77
+ for (const error of result.errors) {
78
+ if (!errors.has(error.path)) {
79
+ errors.set(error.path, error);
80
+ }
81
+ }
82
+ }
83
+ const merged = [...files.values()].toSorted((a, b) => a.path.localeCompare(b.path));
84
+ return {
85
+ errors: [...errors.values()],
86
+ files: merged,
87
+ filesReferencing: merged.length,
88
+ path,
89
+ refsTotal: merged.reduce((sum, file) => sum + file.count, 0),
90
+ tagName: null,
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Reduce the lanes to one state.
96
+ *
97
+ * Precedence is strictly least-informative-wins, because every step down is a step away from a
98
+ * number the caller could act on: unsupported (the host cannot answer at all) beats failed (it
99
+ * tried and could not) beats pending beats a real union.
100
+ */
101
+ function combine(path: string, states: readonly UsageState[]): UsageState {
102
+ if (states.some((state) => state.status === "unsupported")) {
103
+ return { status: "unsupported" };
104
+ }
105
+ const failure = states.find((state) => state.status === "failed");
106
+ if (failure) {
107
+ return failure;
108
+ }
109
+ if (states.some((state) => state.status === "pending")) {
110
+ return { status: "pending" };
111
+ }
112
+ return {
113
+ result: unionResults(
114
+ path,
115
+ states.map((state) => (state as { result: ReferencesResult }).result),
116
+ ),
117
+ status: "ready",
118
+ };
119
+ }
120
+
121
+ /**
122
+ * The answer right now, WITHOUT starting a request — the synchronous read a lit template needs.
123
+ *
124
+ * `null` means at least one lane has never been asked, and the caller decides whether this paint is
125
+ * the one that asks (see {@link loadMediaUsages}).
126
+ */
127
+ export function peekMediaUsages(path: string): UsageState | null {
128
+ const queries = mediaUsageQueries(path);
129
+ if (queries.length === 0) {
130
+ return null;
131
+ }
132
+ const states: UsageState[] = [];
133
+ for (const query of queries) {
134
+ const state = peekUsages(query);
135
+ if (state === null) {
136
+ return null;
137
+ }
138
+ states.push(state);
139
+ }
140
+ return combine(path, states);
141
+ }
142
+
143
+ /**
144
+ * Ask every lane and union the answers. Never rejects — a failure is a STATE, because the caller
145
+ * has to render something and "unknown" is the honest something.
146
+ */
147
+ export async function loadMediaUsages(path: string): Promise<UsageState> {
148
+ const queries = mediaUsageQueries(path);
149
+ if (queries.length === 0) {
150
+ return { message: "no media file to look for", status: "failed" };
151
+ }
152
+ return combine(path, await Promise.all(queries.map((query) => loadUsages(query))));
153
+ }
154
+
155
+ /** Forget every lane's answer and ask again — the Retry behind a failed media count. */
156
+ export async function retryMediaUsages(path: string): Promise<UsageState> {
157
+ const queries = mediaUsageQueries(path);
158
+ if (queries.length === 0) {
159
+ return { message: "no media file to look for", status: "failed" };
160
+ }
161
+ return combine(path, await Promise.all(queries.map((query) => retryUsages(query))));
162
+ }
163
+
164
+ /**
165
+ * The one line a media surface shows beside a file.
166
+ *
167
+ * `null` when the host has no reference index at all — the surface hides the line rather than
168
+ * printing a zero it cannot stand behind. A cold or in-flight query says it is counting, and a
169
+ * failed one says **unknown**, which is a different fact from "unused" and has to read like one.
170
+ */
171
+ export function mediaUsageHeadline(state: UsageState | null): string | null {
172
+ if (state === null) {
173
+ return "Counting references…";
174
+ }
175
+ switch (state.status) {
176
+ case "unsupported": {
177
+ return null;
178
+ }
179
+ case "pending": {
180
+ return "Counting references…";
181
+ }
182
+ case "failed": {
183
+ return "Usage unknown";
184
+ }
185
+ default: {
186
+ return usageHeadline(state.result);
187
+ }
188
+ }
189
+ }