@jxsuite/studio 8.0.1 → 9.0.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 (282) hide show
  1. package/dist/chunks/{2020-5wtzce5g.js → 2020-89fz3pfy.js} +3 -3
  2. package/dist/chunks/adapter-ay13gema.js +4 -0
  3. package/dist/chunks/add-new-flow-nmkqpjym.js +4 -0
  4. package/dist/chunks/cf-account-picker-2e06qxey.js +4 -0
  5. package/dist/chunks/cf-settings-5y04810c.js +4 -0
  6. package/dist/chunks/color-04361ttk.js +4 -0
  7. package/dist/chunks/{color-area-6rbra5dy.js → color-area-h8dbasp1.js} +2 -2
  8. package/dist/chunks/{color-field-gy56c94g.js → color-field-qv5r2b16.js} +3 -3
  9. package/dist/chunks/{color-slider-sqqdyf6c.js → color-slider-nz2mrzky.js} +2 -2
  10. package/dist/chunks/{combobox-72h32ejk.js → combobox-6hs00996.js} +2 -2
  11. package/dist/chunks/components-3vf75n83.js +4 -0
  12. package/dist/chunks/content-source-4f2raf4a.js +4 -0
  13. package/dist/chunks/{convert-to-repeater-he70fc2b.js → convert-to-repeater-j9hfcj4f.js} +3 -3
  14. package/dist/chunks/editor-645krvtz.js +4 -0
  15. package/dist/chunks/entry-commands-3qmv8wkr.js +4 -0
  16. package/dist/chunks/{extension-sections-0egb88dm.js → extension-sections-thfke054.js} +3 -3
  17. package/dist/chunks/file-ops-kkhp9ma7.js +4 -0
  18. package/dist/chunks/files-yswdhxrf.js +4 -0
  19. package/dist/chunks/format-host-4g8ex6bz.js +4 -0
  20. package/dist/chunks/fs-events-z9gpvc6t.js +4 -0
  21. package/dist/chunks/grid-open-fm4j183c.js +4 -0
  22. package/dist/chunks/{grid-view-qg1b5eqx.js → grid-view-7d2nx83y.js} +16 -15
  23. package/dist/chunks/{icons-63bjtbs7.js → icons-853n6qt3.js} +2 -2
  24. package/dist/chunks/{iframe-entry-pkgmcgp0.js → iframe-entry-2x9bn5ng.js} +4 -4
  25. package/dist/chunks/{iframe-entry-99nr5zfb.js → iframe-entry-42hpmwz5.js} +4 -4
  26. package/dist/chunks/{iframe-entry-76j8vwwx.js → iframe-entry-b5qgggyp.js} +2 -2
  27. package/dist/chunks/{iframe-entry-2zggzajv.js → iframe-entry-btck0e9s.js} +4 -4
  28. package/dist/chunks/iframe-host-q7fedsmr.js +4 -0
  29. package/dist/chunks/{index-32eqzv1k.js → index-jxmt7vh8.js} +3 -3
  30. package/dist/chunks/index-pt4wh98x.js +4 -0
  31. package/dist/chunks/{javascript-pjp734q4.js → javascript-vmqp56ke.js} +2 -2
  32. package/dist/chunks/{jsonMode-njkvsa02.js → jsonMode-n3wmpant.js} +5 -5
  33. package/dist/chunks/jx-validate-19y62knt.js +4 -0
  34. package/dist/chunks/layers-panel-1qvc83y9.js +4 -0
  35. package/dist/chunks/layers-qf0bc7ze.js +4 -0
  36. package/dist/chunks/{listbox-c421cf8c.js → listbox-7gka1h86.js} +2 -2
  37. package/dist/chunks/lit-html-wyqw04y2.js +4 -0
  38. package/dist/chunks/media-library-f9d5qcjb.js +4 -0
  39. package/dist/chunks/media-picker-rsb1fmk2.js +4 -0
  40. package/dist/chunks/{menu-bdpzbfvs.js → menu-d2jxpgma.js} +2 -2
  41. package/dist/chunks/{monaco-binding-vq1hgnn0.js → monaco-binding-wfhmz89d.js} +3 -3
  42. package/dist/chunks/monaco-buffer-ak0raadw.js +4 -0
  43. package/dist/chunks/{monaco-setup-xrd2cbkp.js → monaco-setup-fty7a8vg.js} +3 -3
  44. package/dist/chunks/notify-4qj70q0x.js +4 -0
  45. package/dist/chunks/parse-n8dfxnqf.js +4 -0
  46. package/dist/chunks/platform-ehg6t9vd.js +4 -0
  47. package/dist/chunks/{popover-b89sfej7.js → popover-9tf4cjrk.js} +2 -2
  48. package/dist/chunks/publish-panel-yhmda3cd.js +4 -0
  49. package/dist/chunks/{publish-quick-qx2455cj.js → publish-quick-12hh8926.js} +3 -3
  50. package/dist/chunks/publish-workspace-hx8qdv9p.js +4 -0
  51. package/dist/chunks/quick-search-r955m3q1.js +4 -0
  52. package/dist/chunks/references-8gxv0x7t.js +4 -0
  53. package/dist/chunks/right-panel-jarwqjpy.js +4 -0
  54. package/dist/chunks/source-control-84qt25m9.js +4 -0
  55. package/dist/chunks/{split-c9zfdgxf.js → split-m5rwn31z.js} +2 -2
  56. package/dist/chunks/studio-0nefxhar.js +7 -0
  57. package/dist/chunks/studio-0rg30pvs.js +5 -0
  58. package/dist/chunks/{studio-s32578pn.js → studio-14hh60j8.js} +4 -4
  59. package/dist/chunks/studio-1cfyd1ys.js +5 -0
  60. package/dist/chunks/studio-2bq7hheq.js +5 -0
  61. package/dist/chunks/{studio-zz9qe3rw.js → studio-32d8wyxq.js} +37 -37
  62. package/dist/chunks/studio-3hpcawh8.js +4 -0
  63. package/dist/chunks/{studio-svb7yyve.js → studio-4j35hgsw.js} +4 -4
  64. package/dist/chunks/{studio-x1x167jh.js → studio-4qg61g9j.js} +4 -4
  65. package/dist/chunks/studio-4rgem12q.js +5 -0
  66. package/dist/chunks/studio-6k9djnxw.js +5 -0
  67. package/dist/chunks/studio-6rbaedw2.js +9 -0
  68. package/dist/chunks/studio-732mw0g8.js +5 -0
  69. package/dist/chunks/studio-79r8msnr.js +7 -0
  70. package/dist/chunks/studio-7q6jdd4h.js +5 -0
  71. package/dist/chunks/{studio-r5qy30vx.js → studio-8hf8qcm1.js} +4 -4
  72. package/dist/chunks/{studio-8tr6cfrc.js → studio-8pne0rx4.js} +4 -4
  73. package/dist/chunks/{studio-y0n7yxea.js → studio-9179bsm7.js} +7 -7
  74. package/dist/chunks/{studio-pbxa5fpc.js → studio-9ff0vqff.js} +3 -3
  75. package/dist/chunks/studio-9gzvypw4.js +5 -0
  76. package/dist/chunks/studio-9ndgd55w.js +5 -0
  77. package/dist/chunks/studio-9ra9e7m4.js +5 -0
  78. package/dist/chunks/studio-a6e5fzve.js +5 -0
  79. package/dist/chunks/studio-a9k86nq6.js +5 -0
  80. package/dist/chunks/studio-ae7a69jc.js +6 -0
  81. package/dist/chunks/studio-aghsg0ds.js +7 -0
  82. package/dist/chunks/{studio-k3xxbagr.js → studio-ajw4k8hp.js} +3 -3
  83. package/dist/chunks/{studio-qmn1xc24.js → studio-apapy17g.js} +4 -4
  84. package/dist/chunks/{studio-8m0y2qe7.js → studio-by8x0jvc.js} +4 -4
  85. package/dist/chunks/{studio-wbcjbhy1.js → studio-c88gq88j.js} +5 -5
  86. package/dist/chunks/{studio-n0c5h6mf.js → studio-cg54vtet.js} +4 -4
  87. package/dist/chunks/studio-dvhxxxbd.js +5 -0
  88. package/dist/chunks/{studio-3q40e3rj.js → studio-e4rq6x0q.js} +4 -4
  89. package/dist/chunks/{studio-fbhxxc5f.js → studio-ekzbgnjp.js} +4 -4
  90. package/dist/chunks/studio-fg4fj9ec.js +10 -0
  91. package/dist/chunks/studio-fj6jzgxw.js +5 -0
  92. package/dist/chunks/{studio-nh6d342n.js → studio-g45r5vm8.js} +7 -7
  93. package/dist/chunks/{studio-a2bvaptz.js → studio-gczve8zf.js} +4 -4
  94. package/dist/chunks/studio-ge08zwpv.js +5 -0
  95. package/dist/chunks/studio-gk9yk8dj.js +12 -0
  96. package/dist/chunks/{studio-bbe4c90a.js → studio-gskkgfe3.js} +4 -4
  97. package/dist/chunks/{studio-83mk4bgg.js → studio-h4jbgne2.js} +8 -8
  98. package/dist/chunks/{studio-b7y5mr30.js → studio-hcy9zeac.js} +4 -4
  99. package/dist/chunks/studio-ht81xjkr.js +5 -0
  100. package/dist/chunks/{studio-jaaz8ce9.js → studio-jr30g4ae.js} +4 -4
  101. package/dist/chunks/{studio-qgh13jtz.js → studio-k5n4es74.js} +4 -4
  102. package/dist/chunks/{studio-ccv2svc6.js → studio-k6hbx2fc.js} +4 -4
  103. package/dist/chunks/{studio-9tmebf0s.js → studio-kqh7fxn6.js} +4 -4
  104. package/dist/chunks/{studio-q0mvbaq6.js → studio-kyw47pet.js} +4 -4
  105. package/dist/chunks/studio-mkw17ngg.js +5 -0
  106. package/dist/chunks/{studio-x9hsze61.js → studio-myb4mf0h.js} +4 -4
  107. package/dist/chunks/studio-n4qq1n57.js +5 -0
  108. package/dist/chunks/studio-nfmpmkqb.js +5 -0
  109. package/dist/chunks/studio-nfwacw31.js +5 -0
  110. package/dist/chunks/{studio-rthyg42z.js → studio-nm86874b.js} +172 -172
  111. package/dist/chunks/studio-nqexty8x.js +16 -0
  112. package/dist/chunks/studio-nr0kcqc6.js +5 -0
  113. package/dist/chunks/{studio-jsgf2q5k.js → studio-nvfa137z.js} +5 -5
  114. package/dist/chunks/studio-psh8mwnw.js +4 -0
  115. package/dist/chunks/studio-qd0cycm4.js +9 -0
  116. package/dist/chunks/{studio-tfkznv39.js → studio-qevf5z9d.js} +4 -4
  117. package/dist/chunks/studio-qs5fmhtj.js +14 -0
  118. package/dist/chunks/studio-ry4789ng.js +7 -0
  119. package/dist/chunks/studio-s4fq9jht.js +5 -0
  120. package/dist/chunks/studio-sc35ttdk.js +5 -0
  121. package/dist/chunks/studio-sm2f0sc6.js +6 -0
  122. package/dist/chunks/{studio-3qeh9t0h.js → studio-spza69tb.js} +5 -5
  123. package/dist/chunks/studio-t6dqnnft.js +5 -0
  124. package/dist/chunks/{studio-43sd2mam.js → studio-tfmhnwyr.js} +3 -3
  125. package/dist/chunks/studio-tn7v8gey.js +6 -0
  126. package/dist/chunks/{studio-tft6r105.js → studio-trcfer2b.js} +4 -4
  127. package/dist/chunks/studio-tyw7tk0x.js +5 -0
  128. package/dist/chunks/{studio-jcnsdaep.js → studio-tyzgte21.js} +4 -4
  129. package/dist/chunks/studio-vtey9w98.js +5 -0
  130. package/dist/chunks/studio-ws2r1d7e.js +5 -0
  131. package/dist/chunks/studio-x2trkhk5.js +5 -0
  132. package/dist/chunks/{studio-mn0476ag.js → studio-xzgp0cp7.js} +4 -4
  133. package/dist/chunks/studio-y47090vq.js +5 -0
  134. package/dist/chunks/{studio-5g0rprdt.js → studio-yjh9gddd.js} +5 -5
  135. package/dist/chunks/studio-ywf9y1e2.js +5 -0
  136. package/dist/chunks/{studio-k7evxqh4.js → studio-z1p91z5a.js} +4 -4
  137. package/dist/chunks/studio-ztppbam0.js +5 -0
  138. package/dist/chunks/{toast-host-1yg5ar8d.js → toast-host-g3r2s0kn.js} +2 -2
  139. package/dist/chunks/toast-w11qpkyz.js +4 -0
  140. package/dist/chunks/{tooltip-yypz28gq.js → tooltip-dj1pvgz7.js} +2 -2
  141. package/dist/chunks/{tree-9xa4hm7n.js → tree-j0ebwr5s.js} +2 -2
  142. package/dist/chunks/{tsMode-epyfz1hv.js → tsMode-byje4a01.js} +5 -5
  143. package/dist/chunks/workspace-7q4arnt3.js +4 -0
  144. package/dist/chunks/workspace-home-ge027kr6.js +4 -0
  145. package/dist/chunks/{ws-client-bv552sfx.js → ws-client-h09h6sqg.js} +3 -3
  146. package/dist/iframe-entry.js +14 -14
  147. package/dist/manifest.json +76 -76
  148. package/dist/studio.js +88 -88
  149. package/package.json +15 -16
  150. package/src/browse/component-examples.ts +15 -224
  151. package/src/browse/library-preview.ts +6 -13
  152. package/src/browse/preview-root.ts +7 -4
  153. package/src/canvas/canvas-utils.ts +18 -4
  154. package/src/canvas/iframe-editable-root.ts +13 -4
  155. package/src/canvas/iframe-entry.ts +3 -1
  156. package/src/canvas/iframe-interaction.ts +11 -2
  157. package/src/canvas/iframe-render.ts +25 -3
  158. package/src/canvas/preview-placeholder.ts +120 -0
  159. package/src/collab/collab-session.ts +6 -2
  160. package/src/commands/context.ts +4 -0
  161. package/src/commands/defaults.ts +2 -0
  162. package/src/commands/live-context.ts +2 -0
  163. package/src/editor/shortcuts.ts +3 -0
  164. package/src/files/files.ts +6 -3
  165. package/src/new-project/new-project-modal.ts +1 -4
  166. package/src/panels/ai-panel.ts +27 -34
  167. package/src/panels/component-preview.ts +5 -0
  168. package/src/panels/pane-context.ts +43 -18
  169. package/src/panels/site-model.ts +7 -19
  170. package/src/panels/site-panel.ts +2 -25
  171. package/src/panels/stylebook-doc.ts +2 -53
  172. package/src/platform.ts +20 -0
  173. package/src/platforms/devserver.ts +2 -120
  174. package/src/services/ai-ask.ts +29 -171
  175. package/src/services/ai-command-tools.ts +40 -50
  176. package/src/services/ai-import-tools.ts +4 -5
  177. package/src/services/ai-project-tools.ts +13 -3
  178. package/src/services/ai-tools.ts +26 -564
  179. package/src/services/ai-writes.ts +84 -2
  180. package/src/services/document-assistant.ts +29 -58
  181. package/src/services/harness/assistant-tools.ts +68 -0
  182. package/src/services/harness/interaction-store.ts +118 -0
  183. package/src/services/harness/studio-documents.ts +105 -0
  184. package/src/services/harness/turn-hooks.ts +1 -6
  185. package/src/services/project-adoption.ts +8 -16
  186. package/src/services/references.ts +0 -0
  187. package/src/services/tool-executor.ts +25 -22
  188. package/src/state.ts +3 -13
  189. package/src/style/stylebook-specimens.ts +11 -15
  190. package/src/surfaces/files-panel.json +12 -0
  191. package/src/surfaces/files-panel.ts +5 -2
  192. package/src/surfaces/pane-context.json +286 -265
  193. package/src/surfaces/pane-context.ts +17 -8
  194. package/src/surfaces/welcome.ts +4 -1
  195. package/src/tabs/selection.ts +14 -109
  196. package/src/tabs/tab.ts +15 -7
  197. package/src/tabs/transact.ts +316 -124
  198. package/src/types.ts +13 -8
  199. package/src/utils/geometry.ts +49 -2
  200. package/dist/chunks/adapter-5tphg7xe.js +0 -4
  201. package/dist/chunks/add-new-flow-psym14sn.js +0 -4
  202. package/dist/chunks/cf-account-picker-haqazq5r.js +0 -4
  203. package/dist/chunks/cf-settings-h2wrh6hq.js +0 -4
  204. package/dist/chunks/color-m7r1vk54.js +0 -4
  205. package/dist/chunks/components-r22xca09.js +0 -4
  206. package/dist/chunks/content-source-mmppmcyg.js +0 -4
  207. package/dist/chunks/editor-ccd9jwsa.js +0 -4
  208. package/dist/chunks/entry-commands-6qxz9e8p.js +0 -4
  209. package/dist/chunks/file-ops-qnab36ek.js +0 -4
  210. package/dist/chunks/files-rcn56qvm.js +0 -4
  211. package/dist/chunks/format-host-yxz0kjjs.js +0 -4
  212. package/dist/chunks/fs-events-d1qxq4z4.js +0 -4
  213. package/dist/chunks/grid-open-4z1g062t.js +0 -4
  214. package/dist/chunks/iframe-host-ssv7kyj0.js +0 -4
  215. package/dist/chunks/index-f0jn7nnp.js +0 -4
  216. package/dist/chunks/jx-validate-ttasrbh7.js +0 -4
  217. package/dist/chunks/layers-bgyr11tr.js +0 -4
  218. package/dist/chunks/layers-panel-7ez66zeq.js +0 -4
  219. package/dist/chunks/lit-html-v9gh687x.js +0 -4
  220. package/dist/chunks/media-library-sq468wts.js +0 -4
  221. package/dist/chunks/media-picker-shfe59pt.js +0 -4
  222. package/dist/chunks/monaco-buffer-3afgwvbp.js +0 -4
  223. package/dist/chunks/notify-kzrjr3fv.js +0 -4
  224. package/dist/chunks/parse-v77xtwv7.js +0 -4
  225. package/dist/chunks/platform-7v4rtd7m.js +0 -4
  226. package/dist/chunks/publish-panel-2z9gw8ee.js +0 -4
  227. package/dist/chunks/publish-workspace-wrc1dgwx.js +0 -4
  228. package/dist/chunks/quick-search-n0t0eqxb.js +0 -4
  229. package/dist/chunks/references-9547rg5s.js +0 -4
  230. package/dist/chunks/right-panel-wsgnr9pn.js +0 -4
  231. package/dist/chunks/source-control-kwktr9y6.js +0 -4
  232. package/dist/chunks/studio-03ct6yfv.js +0 -9
  233. package/dist/chunks/studio-07rcfje3.js +0 -5
  234. package/dist/chunks/studio-3e54qh39.js +0 -7
  235. package/dist/chunks/studio-3exxhz5g.js +0 -5
  236. package/dist/chunks/studio-3qqjeep5.js +0 -6
  237. package/dist/chunks/studio-4hp8pkyt.js +0 -5
  238. package/dist/chunks/studio-58xg6j2q.js +0 -9
  239. package/dist/chunks/studio-6m380wp7.js +0 -5
  240. package/dist/chunks/studio-80jyh1xh.js +0 -4
  241. package/dist/chunks/studio-8vcvfs7r.js +0 -6
  242. package/dist/chunks/studio-bavb31r0.js +0 -5
  243. package/dist/chunks/studio-c05wdnt3.js +0 -5
  244. package/dist/chunks/studio-d5grtb9e.js +0 -16
  245. package/dist/chunks/studio-df0baea9.js +0 -5
  246. package/dist/chunks/studio-dgk4dqtx.js +0 -7
  247. package/dist/chunks/studio-e1g64q43.js +0 -5
  248. package/dist/chunks/studio-ed1hp6zf.js +0 -6
  249. package/dist/chunks/studio-ed3xx40h.js +0 -7
  250. package/dist/chunks/studio-f14e1zdj.js +0 -5
  251. package/dist/chunks/studio-fyh0jc96.js +0 -5
  252. package/dist/chunks/studio-gcxk20n7.js +0 -5
  253. package/dist/chunks/studio-kt3pjkwd.js +0 -5
  254. package/dist/chunks/studio-m8g16bgz.js +0 -5
  255. package/dist/chunks/studio-mx1smyq0.js +0 -5
  256. package/dist/chunks/studio-nqxqf8p2.js +0 -4
  257. package/dist/chunks/studio-p4nrkvqr.js +0 -5
  258. package/dist/chunks/studio-pgmvdv7z.js +0 -7
  259. package/dist/chunks/studio-ph84avkf.js +0 -5
  260. package/dist/chunks/studio-q0tccd43.js +0 -5
  261. package/dist/chunks/studio-q7rtx6zq.js +0 -14
  262. package/dist/chunks/studio-qwdqwkj0.js +0 -5
  263. package/dist/chunks/studio-rj9eamf8.js +0 -5
  264. package/dist/chunks/studio-rptgsk96.js +0 -5
  265. package/dist/chunks/studio-s19tp1xr.js +0 -5
  266. package/dist/chunks/studio-syreb644.js +0 -5
  267. package/dist/chunks/studio-tf9jryb1.js +0 -10
  268. package/dist/chunks/studio-txf2kysx.js +0 -5
  269. package/dist/chunks/studio-v8867ddv.js +0 -12
  270. package/dist/chunks/studio-vxq7fcvv.js +0 -5
  271. package/dist/chunks/studio-w4rz9hdk.js +0 -5
  272. package/dist/chunks/studio-wc649wss.js +0 -5
  273. package/dist/chunks/studio-wys23mxz.js +0 -5
  274. package/dist/chunks/studio-y3ahhdpx.js +0 -5
  275. package/dist/chunks/studio-z97c48fk.js +0 -5
  276. package/dist/chunks/studio-zat62m0c.js +0 -5
  277. package/dist/chunks/studio-zcd2e7yz.js +0 -5
  278. package/dist/chunks/toast-1gv0h53h.js +0 -4
  279. package/dist/chunks/workspace-dz7nxfhv.js +0 -4
  280. package/dist/chunks/workspace-home-xsd7hkjw.js +0 -4
  281. package/src/services/directory-picker.ts +0 -187
  282. package/src/services/gated-registry.ts +0 -77
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "8.0.1",
3
+ "version": "9.0.0",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,7 +31,6 @@
31
31
  "./platform": "./src/platform.ts",
32
32
  "./types": "./src/types.ts",
33
33
  "./base64": "./src/utils/base64.ts",
34
- "./directory-picker": "./src/services/directory-picker.ts",
35
34
  "./import-client": "./src/services/import-client.ts",
36
35
  "./github-auth": "./src/github/github-auth.ts",
37
36
  "./github-auth-status": "./src/github/github-auth-status.ts",
@@ -61,31 +60,31 @@
61
60
  "dependencies": {
62
61
  "@atlaskit/pragmatic-drag-and-drop": "^4.0.0",
63
62
  "@atlaskit/pragmatic-drag-and-drop-hitbox": "^3.0.0",
64
- "@jxsuite/ai": "^0.39.5",
65
- "@jxsuite/collab": "^0.11.2",
66
- "@jxsuite/create": "^1.3.17",
67
- "@jxsuite/formulas": "^0.0.23",
68
- "@jxsuite/markup": "^1.0.0",
69
- "@jxsuite/protocol": "^4.0.0",
70
- "@jxsuite/runtime": "^5.1.0",
71
- "@jxsuite/schema": "^4.0.0",
72
- "@jxsuite/site": "^2.1.0",
73
- "@jxsuite/ui": "^0.7.1",
63
+ "@jxsuite/ai": "^0.41.0",
64
+ "@jxsuite/collab": "^0.11.3",
65
+ "@jxsuite/create": "^1.4.1",
66
+ "@jxsuite/formulas": "^0.0.25",
67
+ "@jxsuite/markup": "^1.0.1",
68
+ "@jxsuite/protocol": "^5.0.0",
69
+ "@jxsuite/runtime": "^6.0.0",
70
+ "@jxsuite/schema": "^4.1.0",
71
+ "@jxsuite/site": "^2.2.1",
72
+ "@jxsuite/ui": "^0.8.1",
74
73
  "@vue/reactivity": "3.5.44",
75
74
  "ajv": "^8.20.0",
76
75
  "ajv-formats": "^3.0.1",
77
76
  "lit-html": "^3.3.3",
78
77
  "monaco-editor": "^0.57.0",
79
- "tabulator-tables": "^6.6.1",
78
+ "tabulator-tables": "^6.7.1",
80
79
  "yaml": "^2.9.1"
81
80
  },
82
81
  "devDependencies": {
83
- "@happy-dom/global-registrator": "^21.0.3",
82
+ "@happy-dom/global-registrator": "^21.0.4",
84
83
  "@jxsuite/test-support": "^0.0.0",
85
84
  "@types/mdast": "^4.0.4",
86
- "@webref/css": "^8.7.7",
85
+ "@webref/css": "^8.7.8",
87
86
  "@webref/elements": "^2.9.2",
88
- "@webref/idl": "^3.85.2"
87
+ "@webref/idl": "^3.86.0"
89
88
  },
90
89
  "//vue-reactivity": "Exact pin required — must match @jxsuite/runtime to avoid cross-boundary proxy bugs"
91
90
  }
@@ -3,49 +3,25 @@
3
3
  *
4
4
  * A preview instance used to be given nothing, so it drew its definition's DEFAULTS, and a starter
5
5
  * component's defaults are empty strings: a page header with no title, a band with no label, a card
6
- * with no name. A component is only ever seen with props, so the preview asks where it has been
7
- * given some, in this order:
6
+ * with no name. The definition's own `examples` fill that gap: the JSON Schema keyword a typed
7
+ * state entry may carry (`spec.md` §5.3, Shape 2), whose first value is the author's own sample.
8
+ * Only a prop that is not private (`spec.md` §5.6), and whose sample is literal data, is taken.
8
9
  *
9
- * 1. The definition's own `examples`: the JSON Schema keyword a typed state entry may carry (`spec.md`
10
- * §5.3, Shape 2), whose first value is the author's own sample.
11
- * 2. Its first use in the project that passes literal values: the reference index names the files that
12
- * use the tag, and the instance among them that passes the most literal props is the likeliest
13
- * to draw the component as the author meant it. A bound prop (`${…}`, `$ref`) has no value
14
- * outside the page that binds it, so it is not one.
15
- * 3. A list that draws it ({@link componentSampleRow}): a component used only as a repeater's row,
16
- * every prop bound to the row, is drawn as that list's first row, with the list's data.
17
- *
18
- * Only a prop the definition declares, and that is not private (`spec.md` §5.6), is taken. Files in
19
- * the project's default language are read before translations, so a preview reads in the language
20
- * the site is written in first.
10
+ * The preview never looks further than the definition. It once borrowed props from the component's
11
+ * first use in the project, and drew a component used only as a list row as that list's first row,
12
+ * both found through the reference index; that cost one project-wide usage query per component on
13
+ * every Library and Project Styles paint, which on a hosted project was enough requests at once to
14
+ * reset the session. A batched or on-demand source may bring it back.
21
15
  *
22
16
  * @docs studio/interface
23
17
  */
24
18
 
25
- import {
26
- isJsonObject,
27
- isNodeObject,
28
- isPrivateStateKey,
29
- isRef,
30
- isString,
31
- } from "@jxsuite/schema/guards";
32
- import { localeOfPath, resolveI18n } from "@jxsuite/schema/locale";
33
- import { getPlatform } from "../platform";
34
- import { formatForPath, loadFormats } from "../format/format-host";
35
- import { parseSourceForPath } from "../files/file-ops";
36
- import { loadUsages } from "../services/references";
37
- import { documentBase } from "../canvas/canvas-origin";
38
- import { bakeRepeaterSources } from "../canvas/repeater-sources";
39
- import { pageContextFor } from "../canvas/canvas-page-context";
40
- import { projectState } from "../store";
41
- import type { JsonValue, JxDocument, JxDocumentValue, JxMutableNode } from "@jxsuite/schema/types";
19
+ import { isJsonObject, isPrivateStateKey, isString } from "@jxsuite/schema/guards";
20
+ import type { JsonValue, JxDocument, JxDocumentValue } from "@jxsuite/schema/types";
42
21
 
43
22
  /** A component's sample props, by state key. */
44
23
  export type ExampleProps = Map<string, JsonValue>;
45
24
 
46
- /** How many of the files that use a component are read for an instance to draw it from. */
47
- const USAGE_FILES_READ = 4;
48
-
49
25
  /** Whether a value is plain data a prop can hold outside any page: no template, no binding. */
50
26
  export function isLiteralProp(value: JxDocumentValue | undefined): value is JsonValue {
51
27
  if (value === undefined) {
@@ -68,13 +44,11 @@ export function isLiteralProp(value: JxDocumentValue | undefined): value is Json
68
44
  return true;
69
45
  }
70
46
 
71
- /** The definition's public prop keys: its state entries, without the private ones. */
72
- function propKeys(definition: JxDocument): Set<string> {
73
- return new Set(Object.keys(definition.state ?? {}).filter((key) => !isPrivateStateKey(key)));
74
- }
75
-
76
- /** The first `examples` value of each typed state entry that declares one. */
77
- export function declaredExamples(definition: JxDocument): ExampleProps {
47
+ /**
48
+ * The props a component's preview instance is given: the first `examples` value of each public
49
+ * typed state entry that declares a literal one.
50
+ */
51
+ export function componentExampleProps(definition: JxDocument): ExampleProps {
78
52
  const out: ExampleProps = new Map();
79
53
  for (const [key, entry] of Object.entries(definition.state ?? {})) {
80
54
  if (isPrivateStateKey(key) || !isJsonObject(entry) || !Array.isArray(entry.examples)) {
@@ -87,186 +61,3 @@ export function declaredExamples(definition: JxDocument): ExampleProps {
87
61
  }
88
62
  return out;
89
63
  }
90
-
91
- /** Every node of a tree, each with the repeater (if any) whose row template holds it. */
92
- function eachNode(
93
- tree: JxDocumentValue | undefined,
94
- visit: (node: JxMutableNode, repeater: JxMutableNode | null) => void,
95
- ): void {
96
- const walk = (value: JxDocumentValue | undefined, repeater: JxMutableNode | null): void => {
97
- if (Array.isArray(value)) {
98
- for (const member of value) {
99
- walk(member, repeater);
100
- }
101
- return;
102
- }
103
- if (!isNodeObject(value)) {
104
- return;
105
- }
106
- visit(value, repeater);
107
- walk(value.children, repeater);
108
- walk(value.map, value.$prototype === "Array" ? value : repeater);
109
- for (const branch of Object.values(value.cases ?? {})) {
110
- walk(branch, repeater);
111
- }
112
- };
113
- walk(tree, null);
114
- }
115
-
116
- /**
117
- * The literal props of the instance of `tagName` in `tree` that passes the most of them, among the
118
- * keys the definition declares; empty when no instance passes any.
119
- */
120
- export function literalUsageProps(
121
- tree: JxDocumentValue | undefined,
122
- tagName: string,
123
- keys: ReadonlySet<string>,
124
- ): ExampleProps {
125
- let best: ExampleProps = new Map();
126
- eachNode(tree, (node) => {
127
- if (node.tagName !== tagName || !isJsonObject(node.$props)) {
128
- return;
129
- }
130
- const found: ExampleProps = new Map();
131
- for (const [key, prop] of Object.entries(node.$props)) {
132
- if (keys.has(key) && isLiteralProp(prop)) {
133
- found.set(key, prop);
134
- }
135
- }
136
- if (found.size > best.size) {
137
- best = found;
138
- }
139
- });
140
- return best;
141
- }
142
-
143
- /** One project document as a tree, or null when it cannot be read or parsed. */
144
- async function readTree(path: string): Promise<JxMutableNode | null> {
145
- try {
146
- const content = await getPlatform().readFile(path);
147
- await loadFormats();
148
- let tree: JxDocumentValue;
149
- if (formatForPath(path)) {
150
- const parsed = await parseSourceForPath(path, content);
151
- tree = parsed.document;
152
- } else if (path.endsWith(".json")) {
153
- /* SAFETY: a `.json` project document is JSON, and every read of it below is guarded. */
154
- tree = JSON.parse(content) as JxDocumentValue;
155
- } else {
156
- return null;
157
- }
158
- return isNodeObject(tree) ? tree : null;
159
- } catch {
160
- return null;
161
- }
162
- }
163
-
164
- /** The files that use a component, default language first, at most {@link USAGE_FILES_READ}. */
165
- async function usageFiles(path: string, tagName: string): Promise<string[]> {
166
- const usages = await loadUsages({ path, tagName });
167
- if (usages.status !== "ready") {
168
- return [];
169
- }
170
- const { i18n } = resolveI18n(projectState?.projectConfig ?? {});
171
- const translated = (file: string): boolean => {
172
- const locale = localeOfPath(file, i18n);
173
- return locale !== null && locale !== i18n?.defaultLocale;
174
- };
175
- const files = usages.result.files.map((file) => file.path);
176
- return [
177
- ...files.filter((file) => !translated(file)),
178
- ...files.filter((file) => translated(file)),
179
- ].slice(0, USAGE_FILES_READ);
180
- }
181
-
182
- /** The props a component's preview instance is given (sources 1 and 2 of the module comment). */
183
- export async function componentExampleProps(
184
- definition: JxDocument,
185
- path: string,
186
- ): Promise<ExampleProps> {
187
- const keys = propKeys(definition);
188
- const tagName = isString(definition.tagName) ? definition.tagName : null;
189
- const examples = declaredExamples(definition);
190
- if (!tagName || keys.size === 0) {
191
- return examples;
192
- }
193
- let best: ExampleProps = new Map();
194
- for (const file of await usageFiles(path, tagName)) {
195
- const found = literalUsageProps(await readTree(file), tagName, keys);
196
- if (found.size > best.size) {
197
- best = found;
198
- }
199
- }
200
- // The author's own sample wins over a page's value for the same prop.
201
- return new Map([...best, ...examples]);
202
- }
203
-
204
- /** A sample-row document, and the file whose list it draws (its relative refs resolve there). */
205
- export interface SampleRow {
206
- doc: JxDocument;
207
- file: string;
208
- }
209
-
210
- /** The state key a repeater reads its whole list from (`#/state/shows`), or null. */
211
- function listKeyOf(repeater: JxMutableNode): string | null {
212
- const { items } = repeater;
213
- if (!isRef(items) || !items.$ref.startsWith("#/state/")) {
214
- return null;
215
- }
216
- const key = items.$ref.slice("#/state/".length);
217
- return key !== "" && !key.includes("/") ? key : null;
218
- }
219
-
220
- /**
221
- * A document that draws the first row of a list using the component (source 3 of the module
222
- * comment), or null when no file that uses it has one: the list's repeater, its data resolved the
223
- * way the canvas resolves it for Edit (`canvas/repeater-sources.ts`) and cut to its first item, in
224
- * the page's own state and `$site` / `$page`.
225
- */
226
- export async function componentSampleRow(
227
- definition: JxDocument,
228
- path: string,
229
- ): Promise<SampleRow | null> {
230
- const tagName = isString(definition.tagName) ? definition.tagName : null;
231
- if (!tagName) {
232
- return null;
233
- }
234
- for (const file of await usageFiles(path, tagName)) {
235
- const tree = await readTree(file);
236
- const lists: JxMutableNode[] = [];
237
- eachNode(tree, (node, repeater) => {
238
- if (node.tagName === tagName && repeater && listKeyOf(repeater)) {
239
- lists.push(repeater);
240
- }
241
- });
242
- const [list] = lists;
243
- const key = list ? listKeyOf(list) : null;
244
- if (!tree || !list || !key) {
245
- continue;
246
- }
247
- const baked = await bakeRepeaterSources(
248
- tree,
249
- [key],
250
- new URL(file, documentBase(projectState?.projectRoot)).href,
251
- );
252
- const rows = baked.state?.[key];
253
- if (!Array.isArray(rows) || rows.length === 0) {
254
- continue;
255
- }
256
- const context = file.startsWith("pages/")
257
- ? await pageContextFor(
258
- { documentPath: file, isEntry: false, lang: tree.$lang, title: tree.title },
259
- projectState?.projectConfig,
260
- )
261
- : null;
262
- return {
263
- doc: {
264
- children: [list],
265
- state: { ...baked.state, [key]: rows.slice(0, 1), ...context },
266
- tagName: "div",
267
- },
268
- file,
269
- };
270
- }
271
- return null;
272
- }
@@ -39,8 +39,8 @@ import { componentRegistry } from "../files/components";
39
39
  import { getPlatform } from "../platform";
40
40
  import { loadFormats, formatForPath } from "../format/format-host";
41
41
  import { parseSourceForPath } from "../files/file-ops";
42
- import { defineComponent, renderComponentPreview } from "../panels/component-preview";
43
- import { componentExampleProps, componentSampleRow } from "./component-examples";
42
+ import { renderComponentPreview } from "../panels/component-preview";
43
+ import { componentExampleProps } from "./component-examples";
44
44
  import { loadProjectPreviewSheets } from "./preview-style";
45
45
  import type { ComponentEntry } from "../files/components";
46
46
  import type { JsonValue, JxDocument } from "@jxsuite/schema/types";
@@ -170,7 +170,7 @@ export async function renderDocPreview(filePath: string): Promise<HTMLElement |
170
170
  async function renderDocument(
171
171
  document_: JxDocument,
172
172
  filePath: string,
173
- context = assetContextFor(filePath, hostAssetDeclarations()),
173
+ context: ReturnType<typeof assetContextFor>,
174
174
  ): Promise<HTMLElement | null> {
175
175
  setSkipServerFunctions(true);
176
176
  /* The document's own URL, as the canvas spells it, and the canvas's dev-proxy token. With the
@@ -195,8 +195,8 @@ async function renderDocument(
195
195
  }
196
196
 
197
197
  /**
198
- * A project component's preview: an instance given its sample props when it renders with them, else
199
- * the first row of a list that draws it (`component-examples.ts`), else none.
198
+ * A project component's preview: an instance given its declared sample props
199
+ * (`component-examples.ts`), or none when the definition does not render with them.
200
200
  */
201
201
  async function projectComponentPreview(
202
202
  component: ComponentEntry,
@@ -206,14 +206,7 @@ async function projectComponentPreview(
206
206
  if (!definition) {
207
207
  return null;
208
208
  }
209
- const props = await componentExampleProps(definition, path);
210
- if (props.size > 0 && (await rendersWith(definition, path, props))) {
211
- return renderComponentPreview(component, props);
212
- }
213
- const sample = await componentSampleRow(definition, path);
214
- if (sample && (await defineComponent(component))) {
215
- return renderDocument(sample.doc, sample.file);
216
- }
209
+ const props = componentExampleProps(definition);
217
210
  return (await rendersWith(definition, path, props))
218
211
  ? renderComponentPreview(component, props)
219
212
  : null;
@@ -14,10 +14,13 @@ import { isFunction } from "@jxsuite/schema/guards";
14
14
  import { projectPreviewSheets } from "./preview-style";
15
15
 
16
16
  /**
17
- * The one rule a preview needs that its box cannot give it from outside: an image whose prop
18
- * defaults to nothing draws a broken-image icon and its alt text, which in a thumbnail is noise,
19
- * not the component. Everything else about the box (its size, its scale, its light surface) is the
20
- * surface document's, on the island itself.
17
+ * The one rule a preview needs that its box cannot give it from outside: an image with no source
18
+ * draws a broken-image icon and its alt text, which in a thumbnail is noise. A COMPONENT preview
19
+ * never reaches it — `renderComponentPreview` fills such an image with the preview placeholder
20
+ * (`canvas/preview-placeholder.ts`), whose `data:` source this selector does not match — so it
21
+ * hides the empty images of DOCUMENT previews (the Content home's cards), which draw what the page
22
+ * wrote. Everything else about the box (its size, its scale, its light surface) is the surface
23
+ * document's, on the island itself.
21
24
  */
22
25
  const PREVIEW_RULES = 'img:not([src]), img[src=""] { visibility: hidden; }';
23
26
 
@@ -925,6 +925,17 @@ export const CANVAS_MODES = [
925
925
 
926
926
  export type CanvasMode = (typeof CANVAS_MODES)[number];
927
927
 
928
+ /**
929
+ * The modes a person, the assistant or a script may ASK for by name: every mode but `git-diff`.
930
+ *
931
+ * Diff is implicit (studio.md §3): a comparison is entered by choosing a changed file or a version
932
+ * in Publish, which says what it is compared against, and left by Back. Named here it would be a
933
+ * comparison of whatever file happened to be open against whatever HEAD happened to be, which is
934
+ * not a thing anyone asked to see. The mode stays in {@link CANVAS_MODES} because the stage draws
935
+ * it; the Source Control panel enters it through `setCanvasMode` directly.
936
+ */
937
+ export const SELECTABLE_CANVAS_MODES = CANVAS_MODES.filter((mode) => mode !== "git-diff");
938
+
928
939
  /** The base modes the preview flag composes with — the two the Canvas view axis shows it beside. */
929
940
  const PREVIEWABLE_BASE_MODES = new Set(["edit", "design"]);
930
941
 
@@ -1166,7 +1177,10 @@ export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
1166
1177
  return [
1167
1178
  {
1168
1179
  args: argsSchema({
1169
- mode: enumProperty(CANVAS_MODES, "The canvas mode to switch the active pane to."),
1180
+ mode: enumProperty(
1181
+ SELECTABLE_CANVAS_MODES,
1182
+ "The canvas mode to switch the active pane to.",
1183
+ ),
1170
1184
  }),
1171
1185
  category: "View",
1172
1186
  id: "canvas.setMode",
@@ -1189,7 +1203,7 @@ export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
1189
1203
  description:
1190
1204
  "Switch the active document's editor mode. edit and design put its element tree on " +
1191
1205
  "the canvas, where the document tools work; preview renders it as a visitor sees it; " +
1192
- "source shows the JSON; stylebook, tokens, grid and git-diff open other editors. Use it to " +
1206
+ "source shows the JSON; stylebook, tokens and grid open other editors. Use it to " +
1193
1207
  "return a document to edit or design when the tree tools are refused.",
1194
1208
  name: "set_canvas_mode",
1195
1209
  /* `after.editor.kind` rather than the tab's own field: it is the gate's read (`treeEditable`
@@ -1197,7 +1211,7 @@ export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
1197
1211
  cannot disagree. The mode named is the one requested — `run` resolved, so it is the mode
1198
1212
  the tab is in. */
1199
1213
  report: ({ after, args }) => {
1200
- const mode = enumArg("canvas.setMode", args, "mode", CANVAS_MODES);
1214
+ const mode = enumArg("canvas.setMode", args, "mode", SELECTABLE_CANVAS_MODES);
1201
1215
  const reach =
1202
1216
  after.editor.kind === "canvas"
1203
1217
  ? "the document tools address its element tree"
@@ -1206,7 +1220,7 @@ export function canvasViewCommands(deps: CanvasCommandDeps): AnyCommand[] {
1206
1220
  },
1207
1221
  },
1208
1222
  run: (_commandCtx, args) => {
1209
- const mode = enumArg("canvas.setMode", args, "mode", CANVAS_MODES);
1223
+ const mode = enumArg("canvas.setMode", args, "mode", SELECTABLE_CANVAS_MODES);
1210
1224
  const tab = requireTab("canvas.setMode");
1211
1225
  const { modes } = tab.capabilities;
1212
1226
  if (!modes.includes(mode)) {
@@ -439,14 +439,20 @@ export function startEditableRoot(
439
439
  * cannot put a caret in them. Capture-phase pointerdown gives the bridge a chance to open a
440
440
  * nested editing host BEFORE the browser computes the caret, so the click still lands where it
441
441
  * was aimed.
442
+ *
443
+ * Any other press inside uneditable content (a component's own internals, frozen layout chrome)
444
+ * drops the selection. The browser leaves the selection where it was when the press lands on
445
+ * something no caret can enter, so the caret went on blinking in the block clicked BEFORE, with
446
+ * the component now selected in the editor and every keystroke still typed into that block.
442
447
  */
443
448
  const onPointerDownCapture = (e: Event) => {
444
- if (!editingAllowed() || !deps.onPropActivate) {
449
+ if (!editingAllowed()) {
445
450
  return;
446
451
  }
447
- let el = e.target instanceof Element ? e.target : null;
452
+ const target = e.target instanceof Element ? e.target : null;
453
+ let el = deps.onPropActivate ? target : null;
448
454
  while (el && el !== container) {
449
- if (hasDataset(el) && el.dataset.jxBoundProp) {
455
+ if (hasDataset(el) && el.dataset.jxBoundProp && deps.onPropActivate) {
450
456
  /* SAFETY: The runtime builds SVG and MathML nodes as `createElementNS(...) as HTMLElement`
451
457
  and stamps `data-jx-bound-prop` through that same `HTMLElement`-typed path, so a
452
458
  foreign-namespace prop host already travels downstream as an `HTMLElement`. */
@@ -463,10 +469,13 @@ export function startEditableRoot(
463
469
  }
464
470
  // A stamped page block first means this is ordinary page DOM, not component internals.
465
471
  if (hasDataset(el) && el.dataset.jxPath) {
466
- return;
472
+ break;
467
473
  }
468
474
  el = el.parentElement;
469
475
  }
476
+ if (target?.closest("[contenteditable]")?.getAttribute("contenteditable") === "false") {
477
+ doc.getSelection()?.removeAllRanges();
478
+ }
470
479
  };
471
480
 
472
481
  /**
@@ -16,6 +16,7 @@ import {
16
16
  captureLocaleBaseline,
17
17
  applySiteStyle,
18
18
  installCanvasImageRetry,
19
+ isRetryableImage,
19
20
  renderResolvedDocument,
20
21
  } from "./iframe-render";
21
22
  import { measureHits, startInteraction } from "./iframe-interaction";
@@ -613,7 +614,8 @@ export function startCanvasIframe(opts: {
613
614
  }
614
615
 
615
616
  const onImageError = (event: Event): void => {
616
- if (event.target instanceof HTMLImageElement) {
617
+ // Only a failure `installCanvasImageRetry` will retry is pending; an empty source settles broken.
618
+ if (event.target instanceof HTMLImageElement && isRetryableImage(event.target)) {
617
619
  retryingImages.set(event.target, Date.now() + IMAGE_RETRY_WINDOW_MS);
618
620
  armIdleWatch();
619
621
  }
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { jxPathSelector, parseJxPath, serializeJxPath } from "./path-mapping";
10
10
  import { isFunction } from "@jxsuite/schema/guards";
11
- import { rectOf } from "../utils/geometry";
11
+ import { isDisplayContents, rectOf } from "../utils/geometry";
12
12
  import { computeInsertZones, insertZonesKey } from "./iframe-insert";
13
13
  import { getActiveElement, isEditing } from "../editor/inline-edit";
14
14
  import type { IframeChannel } from "./iframe-channel";
@@ -76,11 +76,20 @@ export function nearestHit(target: EventTarget | null): NodeHit | null {
76
76
  *
77
77
  * An engine without `checkVisibility` gets the benefit of the doubt. Withholding a selection box
78
78
  * from a node that is on screen is a worse failure than drawing one for a node that is not.
79
+ *
80
+ * `checkVisibility` also answers false for an element with no box of its own, and a `display:
81
+ * contents` element never has one. A component host styled that way (its rendered root does the
82
+ * layout) is on screen whenever anything inside it is, so its children are asked instead. Without
83
+ * this, selecting such a component drew no selection box and no action bar, because every one of
84
+ * them was reported hidden.
79
85
  */
80
86
  function isRendered(el: Element): boolean {
81
87
  // Absent on engines that predate it, whatever lib.dom says.
82
88
  const check: Element["checkVisibility"] | undefined = el.checkVisibility;
83
- return isFunction(check) ? check.call(el, { checkVisibilityCSS: true }) : true;
89
+ if (!isFunction(check) || check.call(el, { checkVisibilityCSS: true })) {
90
+ return true;
91
+ }
92
+ return isDisplayContents(el) && [...el.children].some((child) => isRendered(child));
84
93
  }
85
94
 
86
95
  /**
@@ -30,6 +30,7 @@ import {
30
30
  setStampPropBindings,
31
31
  } from "@jxsuite/runtime";
32
32
  import { resolveAssetRef } from "./asset-resolve";
33
+ import { fillPreviewImages } from "./preview-placeholder";
33
34
  import { classifyRenderNode, jxPathSelector, serializeJxPath } from "./path-mapping";
34
35
  /* No cycle: `iframe-position.ts` imports only `path-mapping` and a type. It already owns the
35
36
  stamped-attribute lookup, escaping included, so a second query built here would be a second
@@ -994,6 +995,20 @@ export function makeStamper(ctx: PathMapCtx) {
994
995
  * so they're untouched. Returns a teardown that removes the listener. <img> error events don't
995
996
  * bubble, so listen in CAPTURE.
996
997
  */
998
+ /**
999
+ * Whether a failed `<img>` is one a retry could ever fix.
1000
+ *
1001
+ * Read from the AUTHORED attribute, not `img.src`: the property is the RESOLVED url, and an empty
1002
+ * `src=""` resolves to the document's own url, so a check on the property never saw the empty
1003
+ * source it meant to skip. The frame then re-requested `canvas.html` as an image three times, and
1004
+ * its quiescence report counted each failure as a pending retry, holding Studio's idle probe open
1005
+ * for a component drawn without its image prop. A data: URL never errors transiently either.
1006
+ */
1007
+ export function isRetryableImage(img: HTMLImageElement): boolean {
1008
+ const raw = img.getAttribute("src")?.trim() ?? "";
1009
+ return raw !== "" && !raw.startsWith("data:");
1010
+ }
1011
+
997
1012
  export function installCanvasImageRetry(root: HTMLElement, maxAttempts = 3): () => void {
998
1013
  const attempts = new WeakMap<HTMLImageElement, number>();
999
1014
  const onError = (event: Event): void => {
@@ -1001,8 +1016,8 @@ export function installCanvasImageRetry(root: HTMLElement, maxAttempts = 3): ()
1001
1016
  if (!(img instanceof HTMLImageElement)) {
1002
1017
  return;
1003
1018
  }
1004
- // A data: URL never errors (and an empty src isn't a real request) — nothing to retry.
1005
- if (!img.src || img.src.startsWith("data:")) {
1019
+ // An empty or data: source is not a request a retry could fix.
1020
+ if (!isRetryableImage(img)) {
1006
1021
  return;
1007
1022
  }
1008
1023
  const attempt = (attempts.get(img) ?? 0) + 1;
@@ -1127,8 +1142,15 @@ export async function renderResolvedDocument(opts: {
1127
1142
  // Marks last, and for the same reason: they are resolved by querying stamped attributes, which
1128
1143
  // Only exist once the tree the stamper walked is actually in the container.
1129
1144
  applyDiffMarks(opts.container, opts.diffMarks ?? null);
1145
+ /* Stylebook specimens are component previews: an image a specimen has no source for draws as a
1146
+ placeholder (`preview-placeholder.ts`). Never in a page's canvas, where an empty image is
1147
+ something its author should see. */
1148
+ const stopPlaceholders = opts.mode === "stylebook" ? fillPreviewImages(opts.container) : null;
1130
1149
  return {
1131
1150
  ctx: { defs: $defs, docBase: opts.docBase, mapperCtx: opts.mapperCtx, mode: opts.mode },
1132
- dispose: stop,
1151
+ dispose: () => {
1152
+ stopPlaceholders?.();
1153
+ stop();
1154
+ },
1133
1155
  };
1134
1156
  }