@jxsuite/studio 8.0.1 → 8.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 (219) hide show
  1. package/dist/chunks/{2020-5wtzce5g.js → 2020-dg097k3e.js} +3 -3
  2. package/dist/chunks/adapter-tck81zft.js +4 -0
  3. package/dist/chunks/{add-new-flow-psym14sn.js → add-new-flow-a1w0bnm6.js} +3 -3
  4. package/dist/chunks/{cf-account-picker-haqazq5r.js → cf-account-picker-ce3kp3x1.js} +3 -3
  5. package/dist/chunks/cf-settings-7ye6x7g2.js +4 -0
  6. package/dist/chunks/color-4gj49xq6.js +4 -0
  7. package/dist/chunks/{color-area-6rbra5dy.js → color-area-vh9cew51.js} +2 -2
  8. package/dist/chunks/{color-field-gy56c94g.js → color-field-e78f16bs.js} +3 -3
  9. package/dist/chunks/{color-slider-sqqdyf6c.js → color-slider-fg4yhvk6.js} +2 -2
  10. package/dist/chunks/{combobox-72h32ejk.js → combobox-6hs00996.js} +2 -2
  11. package/dist/chunks/components-qce0v93f.js +4 -0
  12. package/dist/chunks/content-source-jjx8kgzr.js +4 -0
  13. package/dist/chunks/{convert-to-repeater-he70fc2b.js → convert-to-repeater-grnmea6c.js} +3 -3
  14. package/dist/chunks/editor-bkja5dbv.js +4 -0
  15. package/dist/chunks/entry-commands-1wh0btvg.js +4 -0
  16. package/dist/chunks/{extension-sections-0egb88dm.js → extension-sections-rp7fkega.js} +3 -3
  17. package/dist/chunks/file-ops-6pe4z7an.js +4 -0
  18. package/dist/chunks/files-ybpp3j38.js +4 -0
  19. package/dist/chunks/format-host-3mtbhxk1.js +4 -0
  20. package/dist/chunks/fs-events-xfanfxds.js +4 -0
  21. package/dist/chunks/grid-open-dg5qmnaj.js +4 -0
  22. package/dist/chunks/{grid-view-qg1b5eqx.js → grid-view-x26rfpz1.js} +16 -15
  23. package/dist/chunks/{icons-63bjtbs7.js → icons-853n6qt3.js} +2 -2
  24. package/dist/chunks/{iframe-entry-2zggzajv.js → iframe-entry-02wzcyvy.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-host-j2bk2xkc.js +4 -0
  28. package/dist/chunks/{index-32eqzv1k.js → index-203h71wy.js} +3 -3
  29. package/dist/chunks/index-fkrm7pdk.js +4 -0
  30. package/dist/chunks/{javascript-pjp734q4.js → javascript-ww5pv5ae.js} +2 -2
  31. package/dist/chunks/{jsonMode-njkvsa02.js → jsonMode-vwbdwwq3.js} +6 -6
  32. package/dist/chunks/jx-validate-w7v32f6a.js +4 -0
  33. package/dist/chunks/layers-g7j732cm.js +4 -0
  34. package/dist/chunks/layers-panel-gjrsxcck.js +4 -0
  35. package/dist/chunks/{listbox-c421cf8c.js → listbox-7gka1h86.js} +2 -2
  36. package/dist/chunks/lit-html-jdfz9rhc.js +4 -0
  37. package/dist/chunks/media-library-9j0v4wmk.js +4 -0
  38. package/dist/chunks/media-picker-cpyyt2m0.js +4 -0
  39. package/dist/chunks/{menu-bdpzbfvs.js → menu-d2jxpgma.js} +2 -2
  40. package/dist/chunks/{monaco-binding-vq1hgnn0.js → monaco-binding-63tqze27.js} +3 -3
  41. package/dist/chunks/monaco-buffer-9wcme176.js +4 -0
  42. package/dist/chunks/{monaco-setup-xrd2cbkp.js → monaco-setup-zyyynnj8.js} +3 -3
  43. package/dist/chunks/notify-rahnnnv4.js +4 -0
  44. package/dist/chunks/platform-b2aqv7v5.js +4 -0
  45. package/dist/chunks/{popover-b89sfej7.js → popover-9tf4cjrk.js} +2 -2
  46. package/dist/chunks/publish-panel-4yk2461b.js +4 -0
  47. package/dist/chunks/{publish-quick-qx2455cj.js → publish-quick-ajzdhhs6.js} +3 -3
  48. package/dist/chunks/publish-workspace-mxestzcf.js +4 -0
  49. package/dist/chunks/quick-search-9g6z65b0.js +4 -0
  50. package/dist/chunks/references-c26enpxs.js +4 -0
  51. package/dist/chunks/right-panel-9d0e0yg7.js +4 -0
  52. package/dist/chunks/source-control-xyn3a8ds.js +4 -0
  53. package/dist/chunks/{split-c9zfdgxf.js → split-m5rwn31z.js} +2 -2
  54. package/dist/chunks/{studio-x9hsze61.js → studio-02phtmqn.js} +4 -4
  55. package/dist/chunks/{studio-4hp8pkyt.js → studio-0khd0xdw.js} +4 -4
  56. package/dist/chunks/studio-1fvzq9vs.js +5 -0
  57. package/dist/chunks/studio-1gdfsr49.js +12 -0
  58. package/dist/chunks/{studio-3q40e3rj.js → studio-1meqbxns.js} +4 -4
  59. package/dist/chunks/{studio-q0tccd43.js → studio-1yhz0s8p.js} +4 -4
  60. package/dist/chunks/studio-25a5by3h.js +5 -0
  61. package/dist/chunks/{studio-k3xxbagr.js → studio-2b8443ms.js} +3 -3
  62. package/dist/chunks/{studio-3qqjeep5.js → studio-2c2mjyzj.js} +5 -5
  63. package/dist/chunks/studio-2dtc90zs.js +10 -0
  64. package/dist/chunks/{studio-9tmebf0s.js → studio-332hesdk.js} +4 -4
  65. package/dist/chunks/{studio-03ct6yfv.js → studio-3pndhat0.js} +8 -8
  66. package/dist/chunks/{studio-s32578pn.js → studio-3q6n9qvq.js} +4 -4
  67. package/dist/chunks/studio-4dsdnan2.js +9 -0
  68. package/dist/chunks/{studio-ed1hp6zf.js → studio-4fjqdzrb.js} +5 -5
  69. package/dist/chunks/{studio-jcnsdaep.js → studio-533py3yy.js} +4 -4
  70. package/dist/chunks/studio-5abge3wy.js +5 -0
  71. package/dist/chunks/{studio-8tr6cfrc.js → studio-6vrxzkba.js} +4 -4
  72. package/dist/chunks/{studio-jaaz8ce9.js → studio-73wz63nn.js} +4 -4
  73. package/dist/chunks/{studio-bavb31r0.js → studio-74nzv280.js} +4 -4
  74. package/dist/chunks/{studio-q7rtx6zq.js → studio-7cg75gtj.js} +7 -7
  75. package/dist/chunks/studio-89cwmnae.js +5 -0
  76. package/dist/chunks/studio-8m1t83kn.js +7 -0
  77. package/dist/chunks/{studio-q0mvbaq6.js → studio-8nyw10dp.js} +4 -4
  78. package/dist/chunks/{studio-83mk4bgg.js → studio-94ppxeqy.js} +8 -8
  79. package/dist/chunks/{studio-qmn1xc24.js → studio-957jg8g3.js} +4 -4
  80. package/dist/chunks/{studio-r5qy30vx.js → studio-9wyqyfqy.js} +4 -4
  81. package/dist/chunks/studio-a3evp7f6.js +6 -0
  82. package/dist/chunks/{studio-n0c5h6mf.js → studio-a6x058ew.js} +4 -4
  83. package/dist/chunks/studio-ana67wax.js +5 -0
  84. package/dist/chunks/studio-b4xhpcwy.js +4 -0
  85. package/dist/chunks/studio-bf0rgyf7.js +5 -0
  86. package/dist/chunks/{studio-x1x167jh.js → studio-bqgs90r8.js} +4 -4
  87. package/dist/chunks/{studio-qwdqwkj0.js → studio-c4rcaftm.js} +4 -4
  88. package/dist/chunks/{studio-mn0476ag.js → studio-c5dj3dzk.js} +4 -4
  89. package/dist/chunks/studio-cgjt1zce.js +5 -0
  90. package/dist/chunks/{studio-bbe4c90a.js → studio-cpdzrdgp.js} +4 -4
  91. package/dist/chunks/studio-cq87hree.js +5 -0
  92. package/dist/chunks/studio-d140sy8e.js +5 -0
  93. package/dist/chunks/{studio-jsgf2q5k.js → studio-dmn7gpba.js} +5 -5
  94. package/dist/chunks/studio-e8pzjjhf.js +5 -0
  95. package/dist/chunks/{studio-dgk4dqtx.js → studio-f8kz3krj.js} +5 -5
  96. package/dist/chunks/{studio-qgh13jtz.js → studio-fqs1a22a.js} +4 -4
  97. package/dist/chunks/{studio-syreb644.js → studio-fw3jp9m7.js} +4 -4
  98. package/dist/chunks/{studio-ed3xx40h.js → studio-g6ysm05p.js} +6 -6
  99. package/dist/chunks/studio-gmhd0j3f.js +5 -0
  100. package/dist/chunks/{studio-p4nrkvqr.js → studio-gqpty626.js} +4 -4
  101. package/dist/chunks/{studio-kt3pjkwd.js → studio-grp684wx.js} +4 -4
  102. package/dist/chunks/{studio-df0baea9.js → studio-h98520qr.js} +4 -4
  103. package/dist/chunks/{studio-8m0y2qe7.js → studio-hnx1h1pr.js} +4 -4
  104. package/dist/chunks/studio-hvxt8z79.js +5 -0
  105. package/dist/chunks/{studio-43sd2mam.js → studio-hw60n5dh.js} +3 -3
  106. package/dist/chunks/{studio-wbcjbhy1.js → studio-hxq1ebp0.js} +6 -6
  107. package/dist/chunks/{studio-wys23mxz.js → studio-j4e627w1.js} +4 -4
  108. package/dist/chunks/{studio-ccv2svc6.js → studio-j5jqvyn3.js} +4 -4
  109. package/dist/chunks/{studio-881z1xrx.js → studio-jqrg9xxg.js} +4 -4
  110. package/dist/chunks/{studio-b7y5mr30.js → studio-kdseew9m.js} +4 -4
  111. package/dist/chunks/studio-kt2j51sa.js +5 -0
  112. package/dist/chunks/{studio-d5grtb9e.js → studio-m4w6sygj.js} +9 -9
  113. package/dist/chunks/{studio-zz9qe3rw.js → studio-mmqr9awv.js} +37 -37
  114. package/dist/chunks/{studio-5g0rprdt.js → studio-nm9qev4p.js} +5 -5
  115. package/dist/chunks/{studio-tft6r105.js → studio-nsv7g1qn.js} +4 -4
  116. package/dist/chunks/{studio-3e54qh39.js → studio-p89w12s2.js} +5 -5
  117. package/dist/chunks/{studio-y3ahhdpx.js → studio-pbagym4j.js} +4 -4
  118. package/dist/chunks/{studio-w4rz9hdk.js → studio-pr23qhrw.js} +4 -4
  119. package/dist/chunks/{studio-6m380wp7.js → studio-r4ya3sb2.js} +4 -4
  120. package/dist/chunks/studio-rdnezzkd.js +6 -0
  121. package/dist/chunks/{studio-f14e1zdj.js → studio-rm9m3z60.js} +4 -4
  122. package/dist/chunks/studio-tpmnbgmt.js +5 -0
  123. package/dist/chunks/studio-ttxhq5yt.js +5 -0
  124. package/dist/chunks/{studio-nh6d342n.js → studio-txkx6586.js} +6 -6
  125. package/dist/chunks/{studio-a2bvaptz.js → studio-v0gsjav0.js} +4 -4
  126. package/dist/chunks/{studio-fbhxxc5f.js → studio-v2ek69cb.js} +4 -4
  127. package/dist/chunks/{studio-y0n7yxea.js → studio-wcakp5bt.js} +7 -7
  128. package/dist/chunks/{studio-k7evxqh4.js → studio-x3ge4rts.js} +4 -4
  129. package/dist/chunks/{studio-pbxa5fpc.js → studio-xddgbwhe.js} +3 -3
  130. package/dist/chunks/{studio-c05wdnt3.js → studio-y1x65h5r.js} +4 -4
  131. package/dist/chunks/{studio-rthyg42z.js → studio-z42kysg7.js} +169 -169
  132. package/dist/chunks/studio-zf188hnd.js +5 -0
  133. package/dist/chunks/{studio-tfkznv39.js → studio-zht0x0v3.js} +4 -4
  134. package/dist/chunks/{studio-svb7yyve.js → studio-zm7dnr9g.js} +4 -4
  135. package/dist/chunks/{tooltip-yypz28gq.js → tooltip-dj1pvgz7.js} +2 -2
  136. package/dist/chunks/{tree-9xa4hm7n.js → tree-j0ebwr5s.js} +2 -2
  137. package/dist/chunks/{tsMode-epyfz1hv.js → tsMode-vra2hh7y.js} +5 -5
  138. package/dist/chunks/workspace-3nt93k48.js +4 -0
  139. package/dist/chunks/workspace-home-zbgyqqws.js +4 -0
  140. package/dist/chunks/{ws-client-bv552sfx.js → ws-client-72x5n99j.js} +3 -3
  141. package/dist/iframe-entry.js +14 -14
  142. package/dist/manifest.json +75 -75
  143. package/dist/studio.js +80 -80
  144. package/package.json +11 -11
  145. package/src/browse/component-examples.ts +15 -224
  146. package/src/browse/library-preview.ts +6 -13
  147. package/src/browse/preview-root.ts +7 -4
  148. package/src/canvas/canvas-utils.ts +18 -4
  149. package/src/canvas/iframe-editable-root.ts +13 -4
  150. package/src/canvas/iframe-entry.ts +3 -1
  151. package/src/canvas/iframe-interaction.ts +11 -2
  152. package/src/canvas/iframe-render.ts +25 -3
  153. package/src/canvas/preview-placeholder.ts +120 -0
  154. package/src/editor/shortcuts.ts +3 -0
  155. package/src/panels/component-preview.ts +5 -0
  156. package/src/panels/pane-context.ts +43 -18
  157. package/src/panels/site-model.ts +7 -19
  158. package/src/panels/site-panel.ts +2 -25
  159. package/src/panels/stylebook-doc.ts +2 -53
  160. package/src/services/references.ts +0 -0
  161. package/src/style/stylebook-specimens.ts +11 -15
  162. package/src/surfaces/pane-context.json +286 -265
  163. package/src/surfaces/pane-context.ts +17 -8
  164. package/src/tabs/tab.ts +5 -6
  165. package/src/utils/geometry.ts +49 -2
  166. package/dist/chunks/adapter-5tphg7xe.js +0 -4
  167. package/dist/chunks/cf-settings-h2wrh6hq.js +0 -4
  168. package/dist/chunks/color-m7r1vk54.js +0 -4
  169. package/dist/chunks/components-r22xca09.js +0 -4
  170. package/dist/chunks/content-source-mmppmcyg.js +0 -4
  171. package/dist/chunks/editor-ccd9jwsa.js +0 -4
  172. package/dist/chunks/entry-commands-6qxz9e8p.js +0 -4
  173. package/dist/chunks/file-ops-qnab36ek.js +0 -4
  174. package/dist/chunks/files-rcn56qvm.js +0 -4
  175. package/dist/chunks/format-host-yxz0kjjs.js +0 -4
  176. package/dist/chunks/fs-events-d1qxq4z4.js +0 -4
  177. package/dist/chunks/grid-open-4z1g062t.js +0 -4
  178. package/dist/chunks/iframe-host-ssv7kyj0.js +0 -4
  179. package/dist/chunks/index-f0jn7nnp.js +0 -4
  180. package/dist/chunks/jx-validate-ttasrbh7.js +0 -4
  181. package/dist/chunks/layers-bgyr11tr.js +0 -4
  182. package/dist/chunks/layers-panel-7ez66zeq.js +0 -4
  183. package/dist/chunks/lit-html-v9gh687x.js +0 -4
  184. package/dist/chunks/media-library-sq468wts.js +0 -4
  185. package/dist/chunks/media-picker-shfe59pt.js +0 -4
  186. package/dist/chunks/monaco-buffer-3afgwvbp.js +0 -4
  187. package/dist/chunks/notify-kzrjr3fv.js +0 -4
  188. package/dist/chunks/platform-7v4rtd7m.js +0 -4
  189. package/dist/chunks/publish-panel-2z9gw8ee.js +0 -4
  190. package/dist/chunks/publish-workspace-wrc1dgwx.js +0 -4
  191. package/dist/chunks/quick-search-n0t0eqxb.js +0 -4
  192. package/dist/chunks/references-9547rg5s.js +0 -4
  193. package/dist/chunks/right-panel-wsgnr9pn.js +0 -4
  194. package/dist/chunks/source-control-kwktr9y6.js +0 -4
  195. package/dist/chunks/studio-07rcfje3.js +0 -5
  196. package/dist/chunks/studio-3exxhz5g.js +0 -5
  197. package/dist/chunks/studio-3qeh9t0h.js +0 -6
  198. package/dist/chunks/studio-58xg6j2q.js +0 -9
  199. package/dist/chunks/studio-80jyh1xh.js +0 -4
  200. package/dist/chunks/studio-8vcvfs7r.js +0 -6
  201. package/dist/chunks/studio-fyh0jc96.js +0 -5
  202. package/dist/chunks/studio-gcxk20n7.js +0 -5
  203. package/dist/chunks/studio-m8g16bgz.js +0 -5
  204. package/dist/chunks/studio-mx1smyq0.js +0 -5
  205. package/dist/chunks/studio-pgmvdv7z.js +0 -7
  206. package/dist/chunks/studio-ph84avkf.js +0 -5
  207. package/dist/chunks/studio-rj9eamf8.js +0 -5
  208. package/dist/chunks/studio-rptgsk96.js +0 -5
  209. package/dist/chunks/studio-s19tp1xr.js +0 -5
  210. package/dist/chunks/studio-tf9jryb1.js +0 -10
  211. package/dist/chunks/studio-txf2kysx.js +0 -5
  212. package/dist/chunks/studio-v8867ddv.js +0 -12
  213. package/dist/chunks/studio-vxq7fcvv.js +0 -5
  214. package/dist/chunks/studio-wc649wss.js +0 -5
  215. package/dist/chunks/studio-z97c48fk.js +0 -5
  216. package/dist/chunks/studio-zat62m0c.js +0 -5
  217. package/dist/chunks/studio-zcd2e7yz.js +0 -5
  218. package/dist/chunks/workspace-dz7nxfhv.js +0 -4
  219. package/dist/chunks/workspace-home-xsd7hkjw.js +0 -4
@@ -0,0 +1,120 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * A stand-in picture for an image a component preview has no source for.
4
+ *
5
+ * Component previews draw a definition with only its own declared `examples` (`browse/
6
+ * component-examples.ts`), never with data borrowed from the project. A card component whose image
7
+ * prop defaults to `""` therefore drew a broken-image glyph — or, worse, an `<img src="">`, which
8
+ * resolves to the document's own url and fails as an image. Previews show the component's SHAPE, so
9
+ * an empty image is drawn as a neutral placeholder of a typical photo's proportions instead.
10
+ *
11
+ * Used by the two surfaces that draw components on their own: the parent-realm previews
12
+ * (`panels/component-preview.ts`: the Library, the Site panel's component cards, the palette) and
13
+ * the Stylebook's specimen render in the canvas frame (`canvas/iframe-render.ts`). Never the page
14
+ * canvas: an empty image on a page is something its author should see.
15
+ *
16
+ * Plain DOM with no realm-specific imports, so the frame and the editor share one definition.
17
+ */
18
+
19
+ /**
20
+ * A neutral landscape glyph: the canvas's one structural gray (`#808080`, as `STYLEBOOK_CSS` uses
21
+ * it) at low opacity, so it reads as a quiet tint on a light card and a dark one alike. A `data:`
22
+ * image is a document of its own, which no theme token or custom property can reach into.
23
+ */
24
+ export const PREVIEW_IMAGE_PLACEHOLDER = `data:image/svg+xml,${encodeURIComponent(
25
+ '<svg xmlns="http://www.w3.org/2000/svg" width="320" height="180" viewBox="0 0 320 180">' +
26
+ '<rect width="320" height="180" fill="#808080" fill-opacity="0.16"/>' +
27
+ '<g fill="#808080" fill-opacity="0.38">' +
28
+ '<circle cx="118" cy="64" r="14"/>' +
29
+ '<path d="M60 140l52-50 34 30 38-46 76 66z"/>' +
30
+ "</g></svg>",
31
+ )}`;
32
+
33
+ /** Marks an `<img>` whose source is the placeholder, so a later real source is told apart. */
34
+ export const PLACEHOLDER_ATTR = "data-jx-image-placeholder";
35
+
36
+ /** Whether an image names no source at all: a blank `src` (or none) and a blank `srcset`. */
37
+ export function hasNoImageSource(img: HTMLImageElement): boolean {
38
+ const src = img.getAttribute("src")?.trim() ?? "";
39
+ const srcset = img.getAttribute("srcset")?.trim() ?? "";
40
+ if (src !== "" || srcset !== "") {
41
+ return false;
42
+ }
43
+ // A <picture>'s <source srcset> picks the image even when the <img> itself names none.
44
+ const picture = img.parentElement?.localName === "picture" ? img.parentElement : null;
45
+ return !picture?.querySelector("source[srcset]:not([srcset=''])");
46
+ }
47
+
48
+ /** Give one image the placeholder when it has no source; clear the mark once it has a real one. */
49
+ function fill(img: HTMLImageElement): void {
50
+ if (hasNoImageSource(img)) {
51
+ img.setAttribute(PLACEHOLDER_ATTR, "");
52
+ img.setAttribute("src", PREVIEW_IMAGE_PLACEHOLDER);
53
+ } else if (
54
+ img.hasAttribute(PLACEHOLDER_ATTR) &&
55
+ img.getAttribute("src") !== PREVIEW_IMAGE_PLACEHOLDER
56
+ ) {
57
+ img.removeAttribute(PLACEHOLDER_ATTR);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Whether an element is an `<img>`. By tag name rather than `instanceof`: the Stylebook's frame is
63
+ * another realm, whose `HTMLImageElement` is not this module's.
64
+ */
65
+ function isImage(element: Element): element is HTMLImageElement {
66
+ return element.localName === "img";
67
+ }
68
+
69
+ /** Every `<img>` at or under `node`. */
70
+ function imagesIn(node: Node): HTMLImageElement[] {
71
+ if (node.nodeType !== node.ELEMENT_NODE || !("querySelectorAll" in node)) {
72
+ return [];
73
+ }
74
+ // SAFETY: an ELEMENT_NODE with `querySelectorAll` is an Element, in whichever realm it lives.
75
+ const element = node as Element;
76
+ const found = [...element.querySelectorAll("img")];
77
+ return isImage(element) ? [element, ...found] : found;
78
+ }
79
+
80
+ /**
81
+ * Fill every source-less image under `root` with {@link PREVIEW_IMAGE_PLACEHOLDER}, now and as the
82
+ * tree changes: a component draws its own children in its `connectedCallback`, after the preview
83
+ * element is created, and a binding can clear an image's source later. A real source always wins —
84
+ * the placeholder only ever replaces nothing. Returns a function that stops watching.
85
+ */
86
+ export function fillPreviewImages(root: Element): () => void {
87
+ for (const img of imagesIn(root)) {
88
+ fill(img);
89
+ }
90
+ /* The root's own realm's observer where it has one (the Stylebook's frame); this realm's for a
91
+ document with no window, such as a parsed one — an observer watches any same-origin node. */
92
+ const Observer = root.ownerDocument.defaultView?.MutationObserver ?? MutationObserver;
93
+ const observer = new Observer((records) => {
94
+ for (const record of records) {
95
+ if (record.type === "attributes") {
96
+ const { target } = record;
97
+ if (target.nodeType === target.ELEMENT_NODE && "localName" in target) {
98
+ // SAFETY: an ELEMENT_NODE target with `localName` is an Element (attributes only land on elements).
99
+ const element = target as Element;
100
+ if (isImage(element)) {
101
+ fill(element);
102
+ }
103
+ }
104
+ continue;
105
+ }
106
+ for (const added of record.addedNodes) {
107
+ for (const img of imagesIn(added)) {
108
+ fill(img);
109
+ }
110
+ }
111
+ }
112
+ });
113
+ observer.observe(root, {
114
+ attributeFilter: ["src", "srcset"],
115
+ attributes: true,
116
+ childList: true,
117
+ subtree: true,
118
+ });
119
+ return () => observer.disconnect();
120
+ }
@@ -1010,6 +1010,9 @@ const SELF_SCROLLING_MODES: ReadonlySet<string> = new Set([
1010
1010
  "media-home",
1011
1011
  "preview",
1012
1012
  "publish-home",
1013
+ /* The Styles workspace's Variables view — `style/variables-view.ts`. An `overflow-y: auto`
1014
+ column over the stage; without it here the wheel panned an artboard that does not exist. */
1015
+ "tokens",
1013
1016
  ]);
1014
1017
 
1015
1018
  /**
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { defineElement, setSkipServerFunctions } from "@jxsuite/runtime";
10
10
  import { projectState } from "../store";
11
+ import { fillPreviewImages } from "../canvas/preview-placeholder";
11
12
  import type { ComponentEntry } from "../files/components";
12
13
  import type { JsonValue } from "@jxsuite/schema/types";
13
14
 
@@ -43,6 +44,10 @@ export async function renderComponentPreview(
43
44
  for (const [name, value] of props) {
44
45
  properties[name] = value;
45
46
  }
47
+ /* Watched from now on: the instance draws its children when it connects, after this returns.
48
+ A preview shows the component's shape, so an image it has no source for is a placeholder,
49
+ not a broken glyph (`canvas/preview-placeholder.ts`). */
50
+ fillPreviewImages(el);
46
51
  return el;
47
52
  } catch (error) {
48
53
  console.warn("Component preview failed:", comp.tagName, error);
@@ -105,6 +105,7 @@ import { componentPropEntries, isComponentDoc } from "../component-props";
105
105
  import { mediaDisplayName } from "./shared";
106
106
  import type { CanvasView, FitMode } from "../canvas/canvas-utils";
107
107
  import { EDITOR_KIND_LABELS } from "../commands/context";
108
+ import type { EditorKind } from "../commands/context";
108
109
  import type { ParamValues } from "../page-params";
109
110
  import type { CommandArgValues } from "../commands/command-args";
110
111
  import type { Tab } from "../tabs/tab";
@@ -781,22 +782,40 @@ function lensSummary(paneId: string): string {
781
782
 
782
783
  // ─── Axis 1 · Editor kind ────────────────────────────────────────────────────
783
784
 
785
+ /**
786
+ * Editor kinds that are entered by a gesture elsewhere and never offered as a choice.
787
+ *
788
+ * A comparison (`diff`) is opened from Publish's changed files or its version history, which says
789
+ * what it is compared against; a Diff segment on every document would compare whatever is open
790
+ * against whatever HEAD is, and was a third way into a view two browsers already own. While a tab
791
+ * is IN one, the axis names it and offers nothing: the way out is Back, beside it.
792
+ */
793
+ const IMPLICIT_EDITOR_KINDS: ReadonlySet<EditorKind> = new Set(["diff"]);
794
+
784
795
  function editorAxis(tab: Tab): Partial<PaneContextView> {
785
796
  /* What the DOCUMENT declares, and nothing else. This read `hostableKindsOf` — the declared kinds
786
797
  narrowed by what the tab's pane was allowed to host — so a page in the side pane was offered
787
798
  Code and not Design. The pane cap is gone; a control that cannot go anywhere is still the
788
799
  defect this axis exists to remove, and one declared kind still renders as text. */
789
- const kinds = editorKindsOf(tab);
790
800
  const current = editorKindOf(tab);
801
+ const kinds = IMPLICIT_EDITOR_KINDS.has(current)
802
+ ? [current]
803
+ : editorKindsOf(tab).filter((kind) => !IMPLICIT_EDITOR_KINDS.has(kind));
791
804
  if (kinds.length < 2) {
792
- // One kind is not a choice. Rendering it as a dropdown would be a control that cannot move —
805
+ // One kind is not a choice. Rendering it as a segmented group would be a control that cannot move —
793
806
  // Which is the defect this axis exists to remove, in miniature.
794
- return { editorLabel: EDITOR_KIND_LABELS[current], editorMode: "static" };
807
+ return { editorLabel: EDITOR_KIND_LABELS[current], editorMode: "static", editorState: "shown" };
795
808
  }
796
809
  return {
797
810
  editorMode: "picker",
798
- editorOptions: kinds.map((kind) => ({ label: EDITOR_KIND_LABELS[kind], value: kind })),
799
- editorValue: current,
811
+ editorState: "shown",
812
+ editors: kinds.map((kind) => ({
813
+ checked: kind === current ? "true" : "false",
814
+ hint: `Open this document in ${EDITOR_KIND_LABELS[kind]}`,
815
+ key: kind,
816
+ label: EDITOR_KIND_LABELS[kind],
817
+ value: kind,
818
+ })),
800
819
  };
801
820
  }
802
821
 
@@ -895,25 +914,26 @@ function contextAxis(tab: Tab, paneId: string, ctx: PaneContextCtx): Partial<Pan
895
914
  const plainQueries = featureQueries.filter(({ query }) => !isSchemeQuery(query));
896
915
  const scheme = ui.previewColorScheme;
897
916
  const { activeMedia } = ui;
898
- const sizeLabel = activeMedia ? mediaDisplayName(activeMedia) : "Base";
899
- const schemeLabel = SCHEMES.find(([value]) => value === scheme)?.[1] ?? "Auto";
900
917
  const locale = localeOf(tab);
901
- /* The language joins the trigger ONLY when the pane is not drawing the document's own — a French
902
- page open in a French pane is not a rendering context worth three more characters, and a bar
903
- that grew a third term in every multilingual project would stop reading as a state. */
904
- const contextSummary = [
905
- sizeLabel,
906
- schemeQueries.length > 0 ? schemeLabel : null,
907
- locale?.overridden ? localeLabel(locale.effective) : null,
908
- ]
909
- .filter((part) => part !== null)
910
- .join(" · ");
911
918
  /* An entry written on the canvas is wrapped in its template's layout like a page (§22.5), so it
912
919
  offers the same "show layout" choice. Whether a layout resolves for it is read asynchronously
913
920
  by the canvas; the project default is the answer this bar can give synchronously. */
914
921
  const hasLayout =
915
922
  (isPageDoc(tab) && Boolean(getEffectiveLayoutPath(tab.doc.document.$layout))) ||
916
923
  (Boolean(projectState?.isSiteProject) && writesEntryOnCanvas(tab.documentPath));
924
+ /* The trigger names only what its popover holds and is NOT at rest. Size and scheme are on the bar
925
+ as groups now, so repeating them here would print the same state twice; and a trigger that read
926
+ "Base · Auto" over a popover that no longer offers either would be describing another control.
927
+ The language joins ONLY when the pane is not drawing the document's own — a French page open in
928
+ a French pane is not a rendering context worth three more characters. */
929
+ const featuresOn = plainQueries.filter(({ name }) => ui.featureToggles[name]).length;
930
+ const contextSummary = [
931
+ locale?.overridden ? localeLabel(locale.effective) : null,
932
+ featuresOn > 0 ? `${featuresOn} ${featuresOn === 1 ? "query" : "queries"}` : null,
933
+ hasLayout && ui.showLayout === false ? "No layout" : null,
934
+ ]
935
+ .filter((part) => part !== null)
936
+ .join(" · ");
917
937
  const page = isPageDoc(tab);
918
938
  const params = page ? paramRows(tab) : [];
919
939
  const props = page ? [] : propRows(tab, paneId);
@@ -952,6 +972,8 @@ function contextAxis(tab: Tab, paneId: string, ctx: PaneContextCtx): Partial<Pan
952
972
  })) satisfies PaneContextChoice[],
953
973
  schemeState: schemeQueries.length > 0 ? "shown" : "hidden",
954
974
  sizes: sizeRows(sizeBreakpoints, activeMedia),
975
+ // Base alone is not a choice: a one-segment group would be a control that cannot move.
976
+ sizesState: sizeBreakpoints.length > 0 ? "shown" : "hidden",
955
977
  };
956
978
  }
957
979
 
@@ -1134,7 +1156,10 @@ function actionsFor(paneId: string, ctx: PaneContextCtx): PaneContextActions {
1134
1156
  if (!tab) {
1135
1157
  return;
1136
1158
  }
1137
- const editorKind = editorKindsOf(tab).find((candidate) => candidate === kind);
1159
+ // An implicit kind is refused like an undeclared one: the axis never offers it.
1160
+ const editorKind = editorKindsOf(tab).find(
1161
+ (candidate) => candidate === kind && !IMPLICIT_EDITOR_KINDS.has(candidate),
1162
+ );
1138
1163
  const mode = editorKind && modeForEditorKind(tab, editorKind);
1139
1164
  if (!mode) {
1140
1165
  return;
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * The panel lists the project as an author thinks of it: pages by address, then the templates,
5
5
  * components and media pages are made of. Everything here turns facts the flow has already gathered
6
- * (paths, `$paths.contentType`, entry counts, usage counts) into the rows the surface draws, so the
7
- * ordering, the nesting, the labels and the filter are testable without a project.
6
+ * (paths, `$paths.contentType`, entry counts) into the rows the surface draws, so the ordering, the
7
+ * nesting, the labels and the filter are testable without a project.
8
8
  *
9
9
  * **Routes come from the router.** Rows are ordered and labelled with `fileToRoute` and
10
10
  * `compareRoutes` from `@jxsuite/site/routes` — the functions the build's page discovery sorts with
@@ -64,7 +64,7 @@ export interface SiteTemplateRow {
64
64
  key: string;
65
65
  path: string;
66
66
  label: string;
67
- /** "used by N pages", or empty where the host has no reference index (the count is omitted). */
67
+ /** "default layout" for the project's `defaults.layout`, else empty. */
68
68
  detail: string;
69
69
  selected: SiteSelected;
70
70
  }
@@ -320,28 +320,16 @@ export function filterPageRows(rows: readonly SitePageRow[], needle: string): Si
320
320
  return kept;
321
321
  }
322
322
 
323
- /** "used by 3 pages", "used by 1 page", "not used yet", or "" when the count is unknown. */
324
- export function usedByLabel(pages: number | null): string {
325
- if (pages === null) {
326
- return "";
327
- }
328
- if (pages === 0) {
329
- return "not used yet";
330
- }
331
- return `used by ${pages} ${pages === 1 ? "page" : "pages"}`;
332
- }
333
-
334
323
  /**
335
324
  * The Templates section's rows: every JSON layout under `layouts/`, by path. Layouts are JSON only
336
325
  * (the layout resolver parses nothing else), so a non-JSON file there is not a template.
337
326
  *
338
- * The project's default layout (`defaults.layout`) says so instead of a count: every page that
339
- * names no layout uses it without referencing it, so the reference index's count would read "not
340
- * used yet" for the layout most pages are poured into.
327
+ * The project's default layout (`defaults.layout`) says so. No row says how many pages use it:
328
+ * counting would ask the reference index once per template on every paint, which is a project-wide
329
+ * sweep per row (`specs/studio.md` §5.7).
341
330
  */
342
331
  export function templateRows(
343
332
  paths: readonly string[],
344
- usedBy: (path: string) => number | null,
345
333
  activePath: string | null,
346
334
  needle: string,
347
335
  defaultPath: string | null = null,
@@ -356,7 +344,7 @@ export function templateRows(
356
344
  }
357
345
  return [
358
346
  {
359
- detail: path === defaultPath ? "default layout" : usedByLabel(usedBy(path)),
347
+ detail: path === defaultPath ? "default layout" : "",
360
348
  key: path,
361
349
  label,
362
350
  path,
@@ -48,7 +48,6 @@ import { previewFileSrc } from "../files/media-paths";
48
48
  import { elementForAsset } from "../editor/file-drop-action";
49
49
  import { readPageFacts } from "../content/collection-pages";
50
50
  import { listCollectionEntryIds } from "../grid/sources/content-source";
51
- import { loadUsages, onUsagesInvalidated, peekUsages, splitUsages } from "../services/references";
52
51
  import { projectMediaFiles } from "../ui/media-picker";
53
52
  import { runActiveReported } from "../commands/run-reported";
54
53
  import { emptySitePanelValues, mountSitePanelSurface } from "../surfaces/site-panel";
@@ -312,16 +311,6 @@ function defaultLayoutPath(): string | null {
312
311
  return isString(layout) ? layout.replace(/^\.\//, "") : null;
313
312
  }
314
313
 
315
- /** How many pages use a template, from the reference index — `null` where it cannot say yet. */
316
- function templateUsage(path: string): number | null {
317
- const state = peekUsages({ path });
318
- if (state === null) {
319
- void loadUsages({ path }).then(repaint);
320
- return null;
321
- }
322
- return state.status === "ready" ? splitUsages(state.result).pages : null;
323
- }
324
-
325
314
  // ─── The view ────────────────────────────────────────────────────────────────
326
315
 
327
316
  /** The filter, as typed. One per window: the panel is project-level and there is one project. */
@@ -366,12 +355,8 @@ export function sitePanelValues(): SitePanelValues {
366
355
  : [];
367
356
  const pages = filterPageRows(allPages, needle);
368
357
  const defaultLayout = defaultLayoutPath();
369
- const allTemplates = index
370
- ? templateRows(index.layouts, templateUsage, activePath, "", defaultLayout)
371
- : [];
372
- const templates = index
373
- ? templateRows(index.layouts, templateUsage, activePath, needle, defaultLayout)
374
- : [];
358
+ const allTemplates = index ? templateRows(index.layouts, activePath, "", defaultLayout) : [];
359
+ const templates = index ? templateRows(index.layouts, activePath, needle, defaultLayout) : [];
375
360
  const registryLoading = !_componentRegistryLoaded;
376
361
  const allComponents = componentRows("", activePath);
377
362
  const components = componentRows(needle, activePath);
@@ -537,9 +522,6 @@ export function mediaDragData(path: string): BlockDragData | null {
537
522
 
538
523
  /** Mount the surface into `host`, or bring the one already there up to date. */
539
524
  function mountInto(host: HTMLElement): void {
540
- /* A page created, saved or deleted changes how many pages use a template, and the usage cache is
541
- dropped for it: the Templates rows ask again on the repaint this asks for. */
542
- _usagesUnlisten ??= onUsagesInvalidated(repaint);
543
525
  if (_standing && (_standing.host !== host || _standing.handle?.connected() === false)) {
544
526
  dispose(_standing);
545
527
  _standing = null;
@@ -585,9 +567,6 @@ function mountInto(host: HTMLElement): void {
585
567
  });
586
568
  }
587
569
 
588
- /** The usage-invalidation subscription, taken once the panel first mounts. */
589
- let _usagesUnlisten: (() => void) | null = null;
590
-
591
570
  /** Repaint the Navigator, which repaints this panel when it is the one showing. */
592
571
  function repaint(): void {
593
572
  renderOnly("leftPanel");
@@ -625,8 +604,6 @@ export function resetSitePanel(): void {
625
604
  dispose(_standing);
626
605
  }
627
606
  _standing = null;
628
- _usagesUnlisten?.();
629
- _usagesUnlisten = null;
630
607
  _filter = "";
631
608
  _epoch += 1;
632
609
  _index = null;
@@ -21,15 +21,9 @@ import { isKeyframesAtRule } from "@jxsuite/runtime/css";
21
21
  import type { StylebookEntry } from "./stylebook-panel";
22
22
  import type { ComponentEntry } from "../files/components";
23
23
  import type { JxPath } from "../state";
24
- import { isNonNullObject, isRef } from "@jxsuite/schema/guards";
24
+ import { isNonNullObject } from "@jxsuite/schema/guards";
25
25
  import type { StylebookSpecimens } from "../style/stylebook-specimens";
26
- import type {
27
- JxDocument,
28
- JxHeadEntry,
29
- JxMutableNode,
30
- JxStateDefinition,
31
- JxStyle,
32
- } from "@jxsuite/schema/types";
26
+ import type { JxHeadEntry, JxMutableNode, JxStyle } from "@jxsuite/schema/types";
33
27
 
34
28
  export interface StylebookDocResult {
35
29
  /** The specimen document (plain JSON — safe to post over the bridge). */
@@ -210,40 +204,6 @@ export function catalogueRows(
210
204
  return out;
211
205
  }
212
206
 
213
- /**
214
- * A card's specimen for a component drawn as the first row of a list
215
- * (`style/stylebook-specimens.ts`): the list's repeater, reading its row from `rootState[key]` so
216
- * two cards' lists cannot collide, and the rest of the page state the row was drawn in, the first
217
- * card's value winning a shared name (`$page`, `$site`). Null when the sample is not a repeater
218
- * over a state list.
219
- */
220
- export function sampleRowSpecimen(
221
- sample: JxDocument,
222
- key: string,
223
- rootState: Record<string, JxStateDefinition>,
224
- ): JxMutableNode | null {
225
- const [list] = Array.isArray(sample.children) ? sample.children : [];
226
- if (!isNonNullObject(list) || Array.isArray(list) || !isRef(list.items)) {
227
- return null;
228
- }
229
- const { $ref } = list.items;
230
- const listKey = $ref.startsWith("#/state/") ? $ref.slice("#/state/".length) : "";
231
- const rows = sample.state?.[listKey];
232
- if (!Array.isArray(rows)) {
233
- return null;
234
- }
235
- rootState[key] = rows;
236
- for (const [name, value] of Object.entries(sample.state ?? {})) {
237
- if (name !== listKey && !(name in rootState)) {
238
- rootState[name] = value;
239
- }
240
- }
241
- /* SAFETY: `list` is the repeater object a page's `children` held; only its `items` is replaced,
242
- with a ref of the same shape. */
243
- const repeater = { ...list, items: { $ref: `#/state/${key}` } } as JxMutableNode;
244
- return { children: [repeater], tagName: "div" };
245
- }
246
-
247
207
  /** Options for {@link buildStylebookDoc}. */
248
208
  export interface BuildStylebookDocOpts {
249
209
  meta: { $sections: { label: string; elements: StylebookEntry[] }[] };
@@ -374,8 +334,6 @@ export function buildStylebookDoc(opts: BuildStylebookDocOpts): StylebookDocResu
374
334
 
375
335
  // Custom components from the registry — live custom elements registered via $elements.
376
336
  const $elements: (string | { $ref: string })[] = [];
377
- // The data the sample-row cards read: each row's list, and the page context it was drawn in.
378
- const rootState: Record<string, JxStateDefinition> = {};
379
337
  {
380
338
  let comps = opts.components;
381
339
  if (filter) {
@@ -404,12 +362,6 @@ export function buildStylebookDoc(opts: BuildStylebookDocOpts): StylebookDocResu
404
362
  const specimen: JxMutableNode = { $props: drawn.props, tagName: comp.tagName };
405
363
  return cardNode(comp.tagName, `<${comp.tagName}>`, specimen, cardPath, null);
406
364
  }
407
- if (drawn?.kind === "row") {
408
- const row = sampleRowSpecimen(drawn.sample.doc, `stylebookRow${ci}`, rootState);
409
- if (row) {
410
- return cardNode(comp.tagName, `<${comp.tagName}>`, row, cardPath, null);
411
- }
412
- }
413
365
  if (drawn?.kind !== "none") {
414
366
  const attributes: Record<string, string> = {};
415
367
  for (const p of comp.props ?? []) {
@@ -455,9 +407,6 @@ export function buildStylebookDoc(opts: BuildStylebookDocOpts): StylebookDocResu
455
407
  doc.$head = opts.effectiveHead;
456
408
  }
457
409
  doc.$media = opts.effectiveMedia;
458
- if (Object.keys(rootState).length > 0) {
459
- doc.state = rootState;
460
- }
461
410
  doc.attributes = { class: "sb-root" };
462
411
  doc.children = sections;
463
412
  doc.style = transposeStylebookStyle(opts.effectiveStyle);
Binary file
@@ -5,14 +5,17 @@
5
5
  * component is rarely drawable that way: the museum starter's `<mu-exhibition-card>` formats a
6
6
  * `date` that defaults to `""`, so its card stayed empty and the canvas logged "was not rendered:
7
7
  * RangeError: Invalid time value" for it on every paint (`spec.md` §16.4). The Site panel's
8
- * previews already know where a component's sample props live (`browse/component-examples.ts`), so
9
- * a card asks the same three sources, in the same order, and draws:
8
+ * previews already know where a component's sample props live (`browse/component-examples.ts`: the
9
+ * definition's own `examples`), so a card asks the same source and draws:
10
10
  *
11
11
  * - `props`: an instance given its sample props, when the definition renders with them;
12
- * - `row`: the first row of a list that draws it, when it is only ever used as a list row;
13
- * - `bare`: the bare instance, when it renders with its defaults and nothing better was found;
14
- * - `none`: no live instance at all (the catalogue's placeholder box), when it renders with none of
15
- * the above, because an instance that throws draws nothing and reports an error nobody can fix.
12
+ * - `bare`: the bare instance, when it renders with its defaults and has no sample props it renders
13
+ * with;
14
+ * - `none`: no live instance at all (the catalogue's placeholder box), when it renders with neither,
15
+ * because an instance that throws draws nothing and reports an error nobody can fix.
16
+ *
17
+ * Nothing here reads the rest of the project, so a catalogue of N components costs N definition
18
+ * reads and no usage queries.
16
19
  *
17
20
  * Answers are cached per component registry: the registry is replaced whenever the project's
18
21
  * components are re-read, so a changed definition is asked again.
@@ -20,17 +23,14 @@
20
23
  * @docs studio/design/stylebook
21
24
  */
22
25
 
23
- import { componentExampleProps, componentSampleRow } from "../browse/component-examples";
24
- import type { SampleRow } from "../browse/component-examples";
26
+ import { componentExampleProps } from "../browse/component-examples";
25
27
  import { readDefinition, rendersWith } from "../browse/library-preview";
26
- import { defineComponent } from "../panels/component-preview";
27
28
  import type { ComponentEntry } from "../files/components";
28
29
  import type { JsonValue } from "@jxsuite/schema/types";
29
30
 
30
31
  /** How one component card is drawn; see the module comment. */
31
32
  export type StylebookSpecimen =
32
33
  | { kind: "props"; props: Record<string, JsonValue> }
33
- | { kind: "row"; sample: SampleRow }
34
34
  | { kind: "bare" }
35
35
  | { kind: "none" };
36
36
 
@@ -49,14 +49,10 @@ export async function componentSpecimen(component: ComponentEntry): Promise<Styl
49
49
  if (!definition) {
50
50
  return { kind: "bare" };
51
51
  }
52
- const props = await componentExampleProps(definition, path);
52
+ const props = componentExampleProps(definition);
53
53
  if (props.size > 0 && (await rendersWith(definition, path, props))) {
54
54
  return { kind: "props", props: Object.fromEntries(props) };
55
55
  }
56
- const sample = await componentSampleRow(definition, path);
57
- if (sample && (await defineComponent(component))) {
58
- return { kind: "row", sample };
59
- }
60
56
  return (await rendersWith(definition, path, new Map())) ? { kind: "bare" } : { kind: "none" };
61
57
  }
62
58