@jxsuite/studio 10.3.0 → 10.5.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 (275) hide show
  1. package/dist/chunks/2020-7sx2vexa.js +4 -0
  2. package/dist/chunks/adapter-12dxr3z7.js +4 -0
  3. package/dist/chunks/{add-new-flow-3tkn72ey.js → add-new-flow-any4pw2q.js} +3 -3
  4. package/dist/chunks/{cf-account-picker-xb8vvt2h.js → cf-account-picker-ajh60k54.js} +3 -3
  5. package/dist/chunks/cf-settings-hbj16kh3.js +4 -0
  6. package/dist/chunks/{color-area-h8dbasp1.js → color-area-gbcftzh3.js} +2 -2
  7. package/dist/chunks/{color-field-qv5r2b16.js → color-field-85136598.js} +3 -3
  8. package/dist/chunks/{color-slider-nz2mrzky.js → color-slider-w1s3ndd8.js} +2 -2
  9. package/dist/chunks/color-thzpppyc.js +4 -0
  10. package/dist/chunks/{combobox-6hs00996.js → combobox-dk98wc5x.js} +3 -3
  11. package/dist/chunks/components-a98hzqw9.js +4 -0
  12. package/dist/chunks/content-source-jq3fjs3w.js +4 -0
  13. package/dist/chunks/{convert-to-repeater-gwzh9gar.js → convert-to-repeater-g66r6qp1.js} +3 -3
  14. package/dist/chunks/editor-rth1rzj8.js +4 -0
  15. package/dist/chunks/entry-commands-02j0s19w.js +4 -0
  16. package/dist/chunks/{extension-sections-338ee35t.js → extension-sections-rqrs7dr0.js} +3 -3
  17. package/dist/chunks/file-ops-6a49er4c.js +4 -0
  18. package/dist/chunks/files-b2p9y0yz.js +4 -0
  19. package/dist/chunks/format-host-7xe57k1s.js +4 -0
  20. package/dist/chunks/fs-events-0xsy168g.js +4 -0
  21. package/dist/chunks/grid-open-h3q4fjy5.js +4 -0
  22. package/dist/chunks/{grid-view-w9nfy203.js → grid-view-0v4hjspb.js} +17 -17
  23. package/dist/chunks/{icons-gb967qc8.js → icons-j9eksgew.js} +2 -2
  24. package/dist/chunks/{iframe-entry-btck0e9s.js → iframe-entry-4zqtn3ry.js} +4 -4
  25. package/dist/chunks/{iframe-entry-b5qgggyp.js → iframe-entry-54kwwhmg.js} +4 -4
  26. package/dist/chunks/{iframe-entry-42hpmwz5.js → iframe-entry-m24fza30.js} +4 -4
  27. package/dist/chunks/iframe-host-8jcmaktg.js +4 -0
  28. package/dist/chunks/{image-bake-4afr4y6a.js → image-bake-m8gzaan9.js} +3 -3
  29. package/dist/chunks/{image-editor-x7t8jbjn.js → image-editor-0afyws7c.js} +3 -3
  30. package/dist/chunks/{index-yjrfeks8.js → index-2q88g57p.js} +3 -3
  31. package/dist/chunks/index-7nj2375a.js +4 -0
  32. package/dist/chunks/{javascript-hy2mws8v.js → javascript-5sxtr1g7.js} +3 -3
  33. package/dist/chunks/jsonMode-m6sf47hq.js +13 -0
  34. package/dist/chunks/jx-validate-808zgbgy.js +4 -0
  35. package/dist/chunks/layers-0ffdtqjn.js +4 -0
  36. package/dist/chunks/layers-panel-rw62wtvf.js +4 -0
  37. package/dist/chunks/{listbox-7gka1h86.js → listbox-k5p41049.js} +2 -2
  38. package/dist/chunks/lit-html-9bz9rcfv.js +4 -0
  39. package/dist/chunks/media-library-2a5hdbek.js +4 -0
  40. package/dist/chunks/media-open-069kzhj4.js +4 -0
  41. package/dist/chunks/media-picker-4p4pd28y.js +4 -0
  42. package/dist/chunks/menu-26nez152.js +4 -0
  43. package/dist/chunks/{monaco-binding-a51py33c.js → monaco-binding-1s2b8snq.js} +3 -3
  44. package/dist/chunks/monaco-buffer-m9fd2dka.js +4 -0
  45. package/dist/chunks/monaco-setup-e0crh3v6.js +4 -0
  46. package/dist/chunks/notify-re3js157.js +4 -0
  47. package/dist/chunks/platform-rywy3tkg.js +4 -0
  48. package/dist/chunks/popover-fhdnt4be.js +4 -0
  49. package/dist/chunks/publish-panel-czkpg8m4.js +4 -0
  50. package/dist/chunks/{publish-quick-w3nxd7q4.js → publish-quick-b9wbhqj6.js} +3 -3
  51. package/dist/chunks/publish-workspace-yy6d23b7.js +4 -0
  52. package/dist/chunks/quick-search-tp7pjnmv.js +4 -0
  53. package/dist/chunks/references-wd2yj8s1.js +4 -0
  54. package/dist/chunks/right-panel-x971k0zc.js +4 -0
  55. package/dist/chunks/source-control-k4p00hpe.js +4 -0
  56. package/dist/chunks/{split-m5rwn31z.js → split-qh21j2b0.js} +2 -2
  57. package/dist/chunks/{studio-v5wktj6f.js → studio-151kgn69.js} +4 -4
  58. package/dist/chunks/studio-15mfah02.js +10 -0
  59. package/dist/chunks/studio-1a7zshnv.js +5 -0
  60. package/dist/chunks/studio-1d8m5s38.js +16 -0
  61. package/dist/chunks/studio-35pmfjtx.js +5 -0
  62. package/dist/chunks/{studio-3na1r48j.js → studio-3yr1zdp9.js} +8 -8
  63. package/dist/chunks/studio-43pynnhz.js +5 -0
  64. package/dist/chunks/studio-44xqe0b7.js +4 -0
  65. package/dist/chunks/{studio-hh394phe.js → studio-48yzqzqr.js} +4 -4
  66. package/dist/chunks/{studio-nskxe9q1.js → studio-4jy967rj.js} +5 -5
  67. package/dist/chunks/{studio-waekn290.js → studio-4ncxzw5m.js} +4 -4
  68. package/dist/chunks/{studio-pm17wdc4.js → studio-4s5bka8t.js} +4 -4
  69. package/dist/chunks/{studio-5tet77de.js → studio-4z0cdh16.js} +3 -3
  70. package/dist/chunks/studio-5rq3e4q5.js +5 -0
  71. package/dist/chunks/{studio-2ggfzrrd.js → studio-69g6a0jn.js} +4 -4
  72. package/dist/chunks/studio-6hcfm9q3.js +5 -0
  73. package/dist/chunks/studio-6mf31e16.js +5 -0
  74. package/dist/chunks/studio-6sxnqqw8.js +5 -0
  75. package/dist/chunks/{studio-0939t1q1.js → studio-7qqb7hb8.js} +4 -4
  76. package/dist/chunks/{studio-5n193zqd.js → studio-7qr8k0v2.js} +4 -4
  77. package/dist/chunks/{studio-0x6f3nk9.js → studio-8ncm9zwx.js} +4 -4
  78. package/dist/chunks/studio-8w5pa88p.js +7 -0
  79. package/dist/chunks/{studio-3qr08sxq.js → studio-9f5vcb4w.js} +3 -3
  80. package/dist/chunks/studio-aaw7wxv8.js +5 -0
  81. package/dist/chunks/studio-afx9brsc.js +5 -0
  82. package/dist/chunks/studio-ajfq4fmr.js +5 -0
  83. package/dist/chunks/{studio-nn4f9m9c.js → studio-ber1dgte.js} +4 -4
  84. package/dist/chunks/{studio-sce30r7b.js → studio-bk0anq3b.js} +3 -3
  85. package/dist/chunks/{studio-0gvvqj3h.js → studio-bmwgj8ez.js} +4 -4
  86. package/dist/chunks/{studio-321ym1tt.js → studio-bpzwhn88.js} +4 -4
  87. package/dist/chunks/{studio-g9brbewz.js → studio-c72v1kwc.js} +4 -4
  88. package/dist/chunks/{studio-tpxcryf2.js → studio-cv6gnjqd.js} +4 -4
  89. package/dist/chunks/{studio-90zkgne5.js → studio-czpmzs9r.js} +7 -7
  90. package/dist/chunks/{studio-3c10v2q1.js → studio-d5f7sskn.js} +4 -4
  91. package/dist/chunks/studio-dmgh2rrj.js +5 -0
  92. package/dist/chunks/{studio-b68k0z00.js → studio-dst8y8vh.js} +4 -4
  93. package/dist/chunks/studio-ehj3csbm.js +6 -0
  94. package/dist/chunks/{studio-thz5t120.js → studio-ek4fb4eb.js} +4 -4
  95. package/dist/chunks/studio-ep1384t9.js +5 -0
  96. package/dist/chunks/{studio-abh9kfdh.js → studio-eq5vqn1q.js} +4 -4
  97. package/dist/chunks/studio-erjccagy.js +5 -0
  98. package/dist/chunks/studio-ex1644g2.js +5 -0
  99. package/dist/chunks/studio-f3awr5nc.js +12 -0
  100. package/dist/chunks/studio-f69wcajk.js +9 -0
  101. package/dist/chunks/{studio-p2a03pgm.js → studio-g65t6svn.js} +4 -4
  102. package/dist/chunks/{studio-3q7mhbgx.js → studio-gw83z798.js} +5 -5
  103. package/dist/chunks/{studio-nwynxqtb.js → studio-hy936516.js} +4 -4
  104. package/dist/chunks/studio-j3bq30yz.js +5 -0
  105. package/dist/chunks/studio-j52ks8q7.js +5 -0
  106. package/dist/chunks/{studio-g0hxtcga.js → studio-jqvke4ac.js} +4 -4
  107. package/dist/chunks/{studio-bct64zmj.js → studio-k05g7efh.js} +4 -4
  108. package/dist/chunks/studio-kefs4sz4.js +5 -0
  109. package/dist/chunks/{studio-s7t4vp1d.js → studio-ketrgtph.js} +5 -5
  110. package/dist/chunks/studio-m3jrjfhy.js +14 -0
  111. package/dist/chunks/{studio-45mj4jqe.js → studio-m8hesdrw.js} +4 -4
  112. package/dist/chunks/{studio-k0xzrse2.js → studio-n271vdsg.js} +36 -36
  113. package/dist/chunks/studio-n4pnf0sw.js +5 -0
  114. package/dist/chunks/{studio-wfct4h6n.js → studio-ncwx602y.js} +4 -4
  115. package/dist/chunks/{studio-423tfdte.js → studio-nngc45ct.js} +4 -4
  116. package/dist/chunks/{studio-eqcddvc2.js → studio-nv5eq0f7.js} +173 -173
  117. package/dist/chunks/{studio-ea6r1zyr.js → studio-nzp0sqjg.js} +4 -4
  118. package/dist/chunks/{studio-k2evngrj.js → studio-pgze9p34.js} +5 -5
  119. package/dist/chunks/studio-ph17an19.js +5 -0
  120. package/dist/chunks/studio-pte5w7rf.js +9 -0
  121. package/dist/chunks/studio-pv1ts08p.js +6 -0
  122. package/dist/chunks/studio-q509sqt8.js +5 -0
  123. package/dist/chunks/{studio-stahswgb.js → studio-qarg1xf9.js} +6 -6
  124. package/dist/chunks/studio-r5g7fx7q.js +9 -0
  125. package/dist/chunks/studio-s49y6m5b.js +5 -0
  126. package/dist/chunks/studio-s4k1v7dt.js +7 -0
  127. package/dist/chunks/studio-t46rt6gx.js +5 -0
  128. package/dist/chunks/{studio-y1b86ast.js → studio-td3h1644.js} +4 -4
  129. package/dist/chunks/{studio-a65kbs2e.js → studio-thgexj3w.js} +7 -7
  130. package/dist/chunks/{studio-vpevkfxw.js → studio-veh91hjv.js} +4 -4
  131. package/dist/chunks/studio-vswmz40d.js +6 -0
  132. package/dist/chunks/{studio-daksw8kh.js → studio-w3aafbpk.js} +4 -4
  133. package/dist/chunks/studio-w4mfaaam.js +5 -0
  134. package/dist/chunks/{studio-4zd6an2b.js → studio-wn2cmf9q.js} +4 -4
  135. package/dist/chunks/studio-x5ffyssw.js +5 -0
  136. package/dist/chunks/{studio-swedpd82.js → studio-x5py815g.js} +4 -4
  137. package/dist/chunks/{studio-4fnb5648.js → studio-xh6rf05g.js} +4 -4
  138. package/dist/chunks/studio-z6s56dzz.js +5 -0
  139. package/dist/chunks/{studio-nvcn2ykc.js → studio-zav9hgb3.js} +4 -4
  140. package/dist/chunks/{studio-wse1rs8t.js → studio-ze1sm147.js} +6 -6
  141. package/dist/chunks/{studio-hfxttkyr.js → studio-zkkt13qd.js} +4 -4
  142. package/dist/chunks/studio-zn7ysyzb.js +5 -0
  143. package/dist/chunks/{studio-nf8esxxc.js → studio-ztzf8mce.js} +4 -4
  144. package/dist/chunks/{studio-1410q5dw.js → studio-zzgp5yka.js} +4 -4
  145. package/dist/chunks/{tooltip-dj1pvgz7.js → tooltip-gtx40bjg.js} +3 -3
  146. package/dist/chunks/{tree-j0ebwr5s.js → tree-a19s1c05.js} +2 -2
  147. package/dist/chunks/tsMode-3f116hth.js +14 -0
  148. package/dist/chunks/workspace-home-61n1jz3q.js +4 -0
  149. package/dist/chunks/workspace-sheek6zw.js +4 -0
  150. package/dist/chunks/{ws-client-arz2qsy9.js → ws-client-cb49m87s.js} +3 -3
  151. package/dist/iframe-entry.js +14 -14
  152. package/dist/manifest.json +82 -82
  153. package/dist/studio.js +97 -96
  154. package/dist/workers/ts.worker.js +3 -3
  155. package/package.json +11 -11
  156. package/src/browse/library-layouts.ts +9 -12
  157. package/src/browse/library-pane.ts +22 -16
  158. package/src/browse/library-source.ts +3 -4
  159. package/src/canvas/canvas-live-render.ts +2 -5
  160. package/src/canvas/canvas-page-context.ts +5 -0
  161. package/src/content/entry-commands.ts +32 -27
  162. package/src/content/entry-editor.ts +46 -44
  163. package/src/content/entry-inspector.ts +15 -12
  164. package/src/content/entry-model.ts +3 -3
  165. package/src/content/entry-summaries.ts +5 -5
  166. package/src/content/hold-state.ts +172 -0
  167. package/src/content/page-summaries.ts +10 -14
  168. package/src/editor/inline-edit.ts +6 -1
  169. package/src/editor/repeater-scope.ts +333 -74
  170. package/src/grid/sources/content-source.ts +42 -66
  171. package/src/panels/block-action-bar.ts +3 -4
  172. package/src/panels/events-panel.ts +6 -2
  173. package/src/panels/formula-workspace.ts +9 -0
  174. package/src/panels/properties-panel.ts +127 -35
  175. package/src/panels/statement-editor.ts +4 -0
  176. package/src/panels/tab-strip.ts +29 -26
  177. package/src/services/ai-project-tools.ts +3 -3
  178. package/src/services/ai-tools.ts +5 -5
  179. package/src/services/ai-write-report.ts +22 -4
  180. package/src/services/context-manager.ts +110 -2
  181. package/src/services/document-assistant.ts +2 -2
  182. package/src/services/harness/chat-reducer.ts +9 -4
  183. package/src/services/render-critic.ts +234 -12
  184. package/src/services/tool-executor.ts +16 -8
  185. package/src/surfaces/collections-panel.json +1 -1
  186. package/src/surfaces/collections-panel.ts +1 -1
  187. package/src/surfaces/entry-editor.json +10 -10
  188. package/src/surfaces/entry-editor.ts +22 -19
  189. package/src/surfaces/entry-inspector.json +11 -11
  190. package/src/surfaces/entry-inspector.ts +17 -10
  191. package/src/surfaces/inspector-dock.json +3 -3
  192. package/src/surfaces/tab-strip.json +5 -5
  193. package/src/surfaces/tab-strip.ts +2 -2
  194. package/src/types.ts +2 -0
  195. package/src/ui/expression-editor.ts +10 -5
  196. package/src/ui/schema-form.ts +18 -1
  197. package/src/utils/edit-display.ts +27 -1
  198. package/dist/chunks/2020-906vx2dq.js +0 -4
  199. package/dist/chunks/adapter-hnvad61x.js +0 -4
  200. package/dist/chunks/cf-settings-mkbcz4z2.js +0 -4
  201. package/dist/chunks/color-04361ttk.js +0 -4
  202. package/dist/chunks/components-w5zqwva1.js +0 -4
  203. package/dist/chunks/content-source-1vzxq1ba.js +0 -4
  204. package/dist/chunks/editor-wvf5e6j9.js +0 -4
  205. package/dist/chunks/entry-commands-mx2whrjx.js +0 -4
  206. package/dist/chunks/file-ops-9bxvk59j.js +0 -4
  207. package/dist/chunks/files-t07yqqej.js +0 -4
  208. package/dist/chunks/format-host-5ja5gxyk.js +0 -4
  209. package/dist/chunks/fs-events-3424ezp3.js +0 -4
  210. package/dist/chunks/grid-open-1mpq9mzb.js +0 -4
  211. package/dist/chunks/iframe-host-wsg0bn73.js +0 -4
  212. package/dist/chunks/index-f9qr7kzh.js +0 -4
  213. package/dist/chunks/jsonMode-vj30te2h.js +0 -13
  214. package/dist/chunks/jx-validate-ttr5ewf0.js +0 -4
  215. package/dist/chunks/layers-m49nmwvx.js +0 -4
  216. package/dist/chunks/layers-panel-hvy2e549.js +0 -4
  217. package/dist/chunks/lit-html-k2mb9k0r.js +0 -4
  218. package/dist/chunks/media-library-ae67q4at.js +0 -4
  219. package/dist/chunks/media-open-yp9jm834.js +0 -4
  220. package/dist/chunks/media-picker-s41ngdfr.js +0 -4
  221. package/dist/chunks/menu-d2jxpgma.js +0 -4
  222. package/dist/chunks/monaco-buffer-pa0g6ax7.js +0 -4
  223. package/dist/chunks/monaco-setup-g9kterhx.js +0 -4
  224. package/dist/chunks/notify-bwehrp7q.js +0 -4
  225. package/dist/chunks/platform-kv0k65cy.js +0 -4
  226. package/dist/chunks/popover-9tf4cjrk.js +0 -4
  227. package/dist/chunks/publish-panel-gzfxcm0t.js +0 -4
  228. package/dist/chunks/publish-workspace-dppd82tc.js +0 -4
  229. package/dist/chunks/quick-search-tdxaam55.js +0 -4
  230. package/dist/chunks/references-vqyr5s9x.js +0 -4
  231. package/dist/chunks/right-panel-0yrw4cgz.js +0 -4
  232. package/dist/chunks/source-control-6fk72me2.js +0 -4
  233. package/dist/chunks/studio-0khd8xtx.js +0 -4
  234. package/dist/chunks/studio-0w3541dg.js +0 -7
  235. package/dist/chunks/studio-15hwqgd8.js +0 -5
  236. package/dist/chunks/studio-1ks5d9y4.js +0 -5
  237. package/dist/chunks/studio-3xk9zdgb.js +0 -5
  238. package/dist/chunks/studio-4f7thq0p.js +0 -5
  239. package/dist/chunks/studio-53wy9ek3.js +0 -5
  240. package/dist/chunks/studio-7rrdbvex.js +0 -5
  241. package/dist/chunks/studio-8kx6m0rg.js +0 -5
  242. package/dist/chunks/studio-9dq7rwc7.js +0 -5
  243. package/dist/chunks/studio-9qx1ywbv.js +0 -9
  244. package/dist/chunks/studio-aam8zt8z.js +0 -5
  245. package/dist/chunks/studio-b4e0ftkt.js +0 -5
  246. package/dist/chunks/studio-bz0b5brj.js +0 -6
  247. package/dist/chunks/studio-cfnn70h1.js +0 -5
  248. package/dist/chunks/studio-e7vjyp04.js +0 -10
  249. package/dist/chunks/studio-egnjpyde.js +0 -9
  250. package/dist/chunks/studio-ejrypvmq.js +0 -5
  251. package/dist/chunks/studio-f8tyeyzt.js +0 -5
  252. package/dist/chunks/studio-fj6c905s.js +0 -5
  253. package/dist/chunks/studio-fkrr7kgg.js +0 -9
  254. package/dist/chunks/studio-fspe7m9t.js +0 -5
  255. package/dist/chunks/studio-ggx3j7nd.js +0 -5
  256. package/dist/chunks/studio-j0zf5x2x.js +0 -5
  257. package/dist/chunks/studio-jeb7hth3.js +0 -5
  258. package/dist/chunks/studio-k1xqrgm5.js +0 -5
  259. package/dist/chunks/studio-k3we3kqb.js +0 -5
  260. package/dist/chunks/studio-n9qba0gx.js +0 -5
  261. package/dist/chunks/studio-nzemfbkk.js +0 -14
  262. package/dist/chunks/studio-q80fg1d6.js +0 -12
  263. package/dist/chunks/studio-r050sez0.js +0 -6
  264. package/dist/chunks/studio-s9fng7m8.js +0 -7
  265. package/dist/chunks/studio-wgc54sxf.js +0 -5
  266. package/dist/chunks/studio-wqfaqw4m.js +0 -5
  267. package/dist/chunks/studio-x0gsfr1d.js +0 -16
  268. package/dist/chunks/studio-xpx91fby.js +0 -6
  269. package/dist/chunks/studio-yrcrncev.js +0 -5
  270. package/dist/chunks/studio-yzpskb0j.js +0 -5
  271. package/dist/chunks/studio-zfdbmhp3.js +0 -5
  272. package/dist/chunks/tsMode-jg1dtphj.js +0 -14
  273. package/dist/chunks/workspace-9btkc8tk.js +0 -4
  274. package/dist/chunks/workspace-home-qcc4z5ag.js +0 -4
  275. package/src/content/draft-state.ts +0 -108
@@ -76,6 +76,7 @@ import {
76
76
  bindableSignalNames,
77
77
  defaultAsString,
78
78
  isInspectorSectionOpen,
79
+ mapListSignalsFor,
79
80
  mapSignalsFor,
80
81
  setInspectorSection,
81
82
  } from "./properties-panel";
@@ -348,6 +349,7 @@ function addField(
348
349
  (next) => spec.onChange({ $expression: next } as JsonValue),
349
350
  {
350
351
  allowEventRef: false,
352
+ extraRefs: spec.extraSignals,
351
353
  stateDefs: [...spec.stateDefs],
352
354
  stateEntries: null,
353
355
  },
@@ -452,7 +454,7 @@ export function projectLogicPanel(): LogicView {
452
454
  view.repeaterFields = [
453
455
  addField(fields, `prop|${key}|items`, "items", {
454
456
  caps: "repeaterItems",
455
- extraSignals: null,
457
+ extraSignals: mapListSignalsFor(doc, selection),
456
458
  label: "Items",
457
459
  literalDefault: literalDefaultOf(node.items),
458
460
  onChange: (v?: JsonValue) =>
@@ -475,7 +477,7 @@ export function projectLogicPanel(): LogicView {
475
477
  // `SwitchDef` is a $ref and nothing else, so From data… is the only rung — a $switch is
476
478
  // Inherently dynamic, and de-escalating it would delete the key and demote the node.
477
479
  caps: "switchDiscriminant",
478
- extraSignals: mapSignalsFor(selection),
480
+ extraSignals: mapSignalsFor(doc, selection),
479
481
  label: "Expression",
480
482
  literalDefault: literalDefaultOf(node.$switch),
481
483
  onChange: (v?: JsonValue) =>
@@ -620,6 +622,7 @@ function bindingView(
620
622
  {
621
623
  allowEventRef: true,
622
624
  emits: fn.emits ?? [],
625
+ extraRefs: mapSignalsFor(tab.doc.document, selection),
623
626
  /* The Inspector's copy, named by the handler it edits. It answered to
624
627
  `navigator/statements` until the docks were split, and then to a bare
625
628
  `inspector/statements` — which the binding list stamps once per structured handler, so
@@ -657,6 +660,7 @@ function bindingView(
657
660
  plan.paint = (host) =>
658
661
  mountExpressionEditor(host, expression, commitExpression, {
659
662
  allowEventRef: true,
663
+ extraRefs: mapSignalsFor(tab.doc.document, selection),
660
664
  onInsertDef: insertDef,
661
665
  preview,
662
666
  stateDefs: Object.keys(defs),
@@ -59,6 +59,7 @@ import { livePreviewExpression } from "../services/live-preview";
59
59
  import { closeFunctionEditor, syncFunctionEditor } from "./editors";
60
60
  import { registerPanel } from "./panel-registry";
61
61
  import { dataTypeLabel, paintDataTree, unwrapSignal } from "./data-explorer";
62
+ import { mapSignalsFor } from "./properties-panel";
62
63
  import { disposeDetachedDataTrees } from "../surfaces/panel-data";
63
64
  import { emptyLogicWorkspaceView, mountLogicWorkspace } from "../surfaces/logic-workspace";
64
65
 
@@ -72,6 +73,7 @@ import {
72
73
 
73
74
  import type { AnyCommand, CommandRegistry } from "../commands/registry";
74
75
  import type { PanelBody } from "./panel-registry";
76
+ import type { SurfaceOption } from "../ui/surface";
75
77
  import type { ExpressionPreview } from "../services/preview-eval";
76
78
  import type { FormulaEditDef, FunctionEditDef } from "../types";
77
79
  import type { Tab } from "../tabs/tab";
@@ -314,6 +316,8 @@ interface Resolved {
314
316
  stateEntries: Record<string, JxStateDefinition>;
315
317
  preview: ExpressionPreview | null;
316
318
  allowEventRef: boolean;
319
+ /** The repeater item's fields, for an event handler inside a template. */
320
+ extraRefs: SurfaceOption[] | null;
317
321
  /** Chip key → node path, so a chip click needs no path parsing (a `switch` case may hold "/"). */
318
322
  chipPaths: Map<string, NodePath>;
319
323
  /** Rail entry name → the value its tree draws. */
@@ -475,6 +479,10 @@ export function workspaceView(): LogicWorkspaceView {
475
479
  allowEventRef: editing.type === "event",
476
480
  chipPaths,
477
481
  editing,
482
+ extraRefs:
483
+ editing.type === "event" && editing.path
484
+ ? mapSignalsFor(tab.doc.document, editing.path)
485
+ : null,
478
486
  preview,
479
487
  root,
480
488
  scopeValues: rail.values,
@@ -528,6 +536,7 @@ function paintEditor(): void {
528
536
  mountExpressionEditor(host, held.selected, writeSelected, {
529
537
  allowEventRef: held.allowEventRef,
530
538
  depth: 1,
539
+ extraRefs: held.extraRefs,
531
540
  onInsertDef: insertDef,
532
541
  path: held.selectedPath,
533
542
  preview: held.preview,
@@ -41,7 +41,7 @@
41
41
  * @docs studio/design/properties
42
42
  */
43
43
 
44
- import { getNodeAtPath } from "../store";
44
+ import { getNodeAtPath, projectState } from "../store";
45
45
  import { effect, effectScope, reactive } from "../reactivity";
46
46
  import {
47
47
  displayTagName,
@@ -50,6 +50,7 @@ import {
50
50
  isRef,
51
51
  isString,
52
52
  isTagExpression,
53
+ isTemplateString,
53
54
  } from "@jxsuite/schema/guards";
54
55
  import type { JsonValue } from "../types";
55
56
  import {
@@ -114,6 +115,13 @@ import type { LinkKind } from "../utils/link-target";
114
115
  import { clickAnythingTo, openPageAction, staleSelectionMessage } from "./empty-state";
115
116
  import { mountMediaPicker, unmountMediaPicker } from "../ui/media-picker";
116
117
  import { colorTokens, resolvedColor } from "../ui/color-selector";
118
+ import {
119
+ childrenTemplate,
120
+ fieldOfChildrenTemplate,
121
+ mapRefLabel,
122
+ repeaterItemFields,
123
+ } from "../editor/repeater-scope";
124
+ import type { RepeaterField } from "../editor/repeater-scope";
117
125
  import { mountExpressionEditor } from "../ui/expression-editor";
118
126
  import type { ExpressionEditorOpts } from "../ui/expression-editor";
119
127
  import {
@@ -182,20 +190,45 @@ function nodeAtPath(doc: JxMutableNode, path: JxPath): JxMutableNode | undefined
182
190
  // ─── The binding vocabulary, shared with the Logic tab ───────────────────────
183
191
 
184
192
  /**
185
- * The two extra signals a position inside a repeater template can bind to, or `null` outside one.
193
+ * The extra signals a position inside a repeater template can bind to, or `null` outside one: the
194
+ * item, every field of the item the document and project declare, and its index.
186
195
  *
187
196
  * A "map" segment addresses a repeater template (`[…, "children", i, "map", …]`, or the legacy `[…,
188
197
  * "children", "map", …]`), so anything at or below it is inside one.
189
198
  *
190
199
  * Exported because Content and Logic both offer them and there is one right answer: a `$switch`
191
- * inside a `$map` can read `$map/item`, and so can the `alt` attribute two nodes below it.
200
+ * inside a `$map` can read `$map/item/data/title`, and so can the `alt` attribute two nodes below
201
+ * it. `$map/item` stays first, because a fresh binding lands on the first signal offered.
202
+ *
203
+ * `withNodes` adds the fields holding child nodes, for the one position that can draw them.
204
+ */
205
+ export function mapSignalsFor(
206
+ doc: JxMutableNode,
207
+ path: JxPath,
208
+ options: { withNodes?: boolean } = {},
209
+ ): SurfaceOption[] | null {
210
+ if (!path.includes("map")) {
211
+ return null;
212
+ }
213
+ const fields = repeaterItemFields(doc, path, projectState?.projectConfig, options);
214
+ return [
215
+ { label: "item", value: "$map/item" },
216
+ ...fields.map((field) => ({ label: field.token, value: field.pointer })),
217
+ { label: "index", value: "$map/index" },
218
+ ];
219
+ }
220
+
221
+ /**
222
+ * What a repeater's own Items can read from the repeater around it: the lists on the outer item.
223
+ * Only those, because an Items position takes a list, and the index or a text field would render
224
+ * nothing.
192
225
  */
193
- export function mapSignalsFor(path: JxPath): SurfaceOption[] | null {
194
- return path.includes("map")
195
- ? [
196
- { label: "$map/item", value: "$map/item" },
197
- { label: "$map/index", value: "$map/index" },
198
- ]
226
+ export function mapListSignalsFor(doc: JxMutableNode, path: JxPath): SurfaceOption[] | null {
227
+ const lists = repeaterItemFields(doc, path, projectState?.projectConfig).filter(
228
+ (field) => field.isArray,
229
+ );
230
+ return lists.length > 0
231
+ ? lists.map((field) => ({ label: field.token, value: field.pointer }))
199
232
  : null;
200
233
  }
201
234
 
@@ -228,7 +261,7 @@ export function defaultAsString(def: JxStateDefinition | undefined) {
228
261
  function bindingDonor(value: JxDocumentValue | undefined): string {
229
262
  if (isRef(value)) {
230
263
  const ref = value.$ref;
231
- return ref.startsWith("#/state/") ? ref.slice(8) : ref || "nothing yet";
264
+ return ref.startsWith("#/state/") ? ref.slice(8) : mapRefLabel(ref) || ref || "nothing yet";
232
265
  }
233
266
  return isString(value) ? "a template" : "a formula";
234
267
  }
@@ -1098,6 +1131,7 @@ function applyLadder(row: ContentRowView, plan: RowPlan, opts: LadderOpts): void
1098
1131
  plan.draft = undefined;
1099
1132
  const editorOpts: ExpressionEditorOpts = {
1100
1133
  allowEventRef: false,
1134
+ extraRefs: opts.extraSignals,
1101
1135
  preview: null,
1102
1136
  stateDefs: opts.stateDefs,
1103
1137
  stateEntries: null,
@@ -1230,7 +1264,7 @@ function sectionsFor(node: JxMutableNode, path: JxPath): ContentSectionView[] {
1230
1264
  const tagName = displayTagName(node.tagName) || "div";
1231
1265
  const isCustomInstance = displayTagName(node.tagName).includes("-");
1232
1266
  const attrs = node.attributes || {};
1233
- const mapSignals = mapSignalsFor(path);
1267
+ const mapSignals = mapSignalsFor(doc, path);
1234
1268
  const bindableSignals = bindableSignalNames(doc);
1235
1269
 
1236
1270
  // ── Which attributes this tag has, by section ──
@@ -1276,7 +1310,7 @@ function sectionsFor(node: JxMutableNode, path: JxPath): ContentSectionView[] {
1276
1310
  const sections: ContentSectionView[] = [
1277
1311
  sectionView("__element", "Element", isInspectorSectionOpen("__element", false), [], 0),
1278
1312
  ];
1279
- sections[0]!.rows = elementRows(node, path, tagName, mapSignals, bindableSignals);
1313
+ sections[0]!.rows = elementRows(node, path, tagName, bindableSignals);
1280
1314
 
1281
1315
  if (isCustomInstance) {
1282
1316
  const definitionOfThis = isComponentDefinitionOpen(node);
@@ -1366,11 +1400,90 @@ function draftKey(path: JxPath, name: string): string {
1366
1400
  return `prop:${path.join("/")}:${name}`;
1367
1401
  }
1368
1402
 
1403
+ /**
1404
+ * What a write to the content row leaves in `children`: the template to hold, or `null` for none.
1405
+ *
1406
+ * `$children` is a list of nodes, so it cannot be held in `textContent`, which would draw it as
1407
+ * `[object Object]`. Picking it, or typing a template that reads it, moves the binding to the
1408
+ * computed-children form the compiler already expands (spec.md §8.4). A template the row already
1409
+ * held there that names no such field (`${state.entry.$children}`) stays there as it is edited.
1410
+ */
1411
+ function computedChildrenFrom(
1412
+ value: JsonValue | undefined,
1413
+ nodeFields: readonly RepeaterField[],
1414
+ keepsTemplate: boolean,
1415
+ ): string | null {
1416
+ if (isRef(value)) {
1417
+ const field = nodeFields.find((f) => f.pointer === value.$ref);
1418
+ return field ? childrenTemplate(field) : null;
1419
+ }
1420
+ if (!isTemplateString(value)) {
1421
+ return null;
1422
+ }
1423
+ return keepsTemplate || fieldOfChildrenTemplate(value, nodeFields) ? value : null;
1424
+ }
1425
+
1426
+ /**
1427
+ * The Text Content row, which is also where an element's child nodes are bound.
1428
+ *
1429
+ * One row for two properties, because they are one question: what fills this element. A node with
1430
+ * computed children has no text to show, and it used to draw this row empty and on Fixed value, so
1431
+ * the binding could not be seen and typing here wrote text the compiled page never drew.
1432
+ */
1433
+ function contentRow(
1434
+ node: JxMutableNode,
1435
+ path: JxPath,
1436
+ bindableSignals: string[],
1437
+ propertyChip: (name: string, human: string) => FieldProvenance,
1438
+ ): ContentRowView {
1439
+ const doc = activeTab.value!.doc.document;
1440
+ const nodeFields = repeaterItemFields(doc, path, projectState?.projectConfig, {
1441
+ withNodes: true,
1442
+ }).filter((field) => field.holdsNodes);
1443
+ const computed = isTemplateString(node.children) ? node.children : null;
1444
+ const bound = computed === null ? undefined : fieldOfChildrenTemplate(computed, nodeFields);
1445
+ const value = bound ? { $ref: bound.pointer } : (computed ?? node.textContent);
1446
+ const write = (next?: JsonValue): void => {
1447
+ const template = computedChildrenFrom(next, nodeFields, computed !== null && !bound);
1448
+ transactDoc(activeTab.value, (t) => {
1449
+ if (template === null || node.textContent !== undefined) {
1450
+ mutateUpdateProperty(t, path, "textContent", template === null ? next : undefined);
1451
+ }
1452
+ if (template !== computed) {
1453
+ mutateUpdateProperty(t, path, "children", template ?? undefined);
1454
+ }
1455
+ });
1456
+ };
1457
+
1458
+ const textDraft = draft(draftKey(path, "textContent"));
1459
+ const text = blankRow(
1460
+ "prop|textContent",
1461
+ "textContent",
1462
+ computed === null ? "Text Content" : "Content",
1463
+ );
1464
+ text.kind = "textarea";
1465
+ text.value = getFieldValue(textDraft, isString(value) ? value : "");
1466
+ const plan: RowPlan = { commit: write, debounceMs: LIVE_PREVIEW, draft: textDraft };
1467
+ applyLadder(text, plan, {
1468
+ caps: "textProperty",
1469
+ extraSignals: mapSignalsFor(doc, path, { withNodes: true }),
1470
+ fieldKey: `prop|${path.join("/")}|textContent`,
1471
+ onChange: write,
1472
+ stateDefs: bindableSignals,
1473
+ value,
1474
+ });
1475
+ if (computed !== null) {
1476
+ applyChip(text, plan, "textContent", propertyChip("children", "content"));
1477
+ } else if (node.textContent !== undefined) {
1478
+ applyChip(text, plan, "textContent", propertyChip("textContent", "text"));
1479
+ }
1480
+ return withPlan(text, plan);
1481
+ }
1482
+
1369
1483
  function elementRows(
1370
1484
  node: JxMutableNode,
1371
1485
  path: JxPath,
1372
1486
  tagName: string,
1373
- mapSignals: SurfaceOption[] | null,
1374
1487
  bindableSignals: string[],
1375
1488
  ): ContentRowView[] {
1376
1489
  const chosenTag = isTagExpression(node.tagName) ? node.tagName.$expression : null;
@@ -1459,28 +1572,7 @@ function elementRows(
1459
1572
  rows.push(withPlan(cls.row, cls.plan));
1460
1573
 
1461
1574
  if (!Array.isArray(node.children) || node.children.length === 0) {
1462
- const textDraft = draft(draftKey(path, "textContent"));
1463
- const text = blankRow("prop|textContent", "textContent", "Text Content");
1464
- text.kind = "textarea";
1465
- text.value = getFieldValue(textDraft, isString(node.textContent) ? node.textContent : "");
1466
- const plan: RowPlan = {
1467
- commit: writeProperty("textContent"),
1468
- debounceMs: LIVE_PREVIEW,
1469
- draft: textDraft,
1470
- };
1471
- applyLadder(text, plan, {
1472
- caps: "textProperty",
1473
- extraSignals: mapSignals,
1474
- fieldKey: `prop|${path.join("/")}|textContent`,
1475
- onChange: (v) =>
1476
- transactDoc(activeTab.value, (t) => mutateUpdateProperty(t, path, "textContent", v)),
1477
- stateDefs: bindableSignals,
1478
- value: node.textContent,
1479
- });
1480
- if (node.textContent !== undefined) {
1481
- applyChip(text, plan, "textContent", propertyChip("textContent", "text"));
1482
- }
1483
- rows.push(withPlan(text, plan));
1575
+ rows.push(contentRow(node, path, bindableSignals, propertyChip));
1484
1576
  }
1485
1577
 
1486
1578
  const hidden = blankRow("prop|hidden", "hidden", "Hidden");
@@ -62,6 +62,8 @@ export interface StatementEditorOpts {
62
62
  stateDefs: string[];
63
63
  stateEntries?: Record<string, JxStateDefinition> | null;
64
64
  allowEventRef: boolean;
65
+ /** Pointers beyond the document's own signals: inside a repeater, its item and that item's fields. */
66
+ extraRefs?: readonly SurfaceOption[] | null | undefined;
65
67
  /** The entry's declared CEM events — offered as dispatchEvent name completions. */
66
68
  emits?: CemEvent[];
67
69
  /**
@@ -342,6 +344,7 @@ function operandOpts(opts: StatementEditorOpts) {
342
344
  return {
343
345
  allowEventRef: opts.allowEventRef,
344
346
  depth: 0,
347
+ extraRefs: opts.extraRefs,
345
348
  stateDefs: opts.stateDefs,
346
349
  stateEntries: opts.stateEntries ?? null,
347
350
  };
@@ -578,6 +581,7 @@ export function flattenStatements(
578
581
  (n) => commit(n as JxStatement),
579
582
  {
580
583
  allowEventRef: opts.allowEventRef,
584
+ extraRefs: opts.extraRefs,
581
585
  stateDefs: opts.stateDefs,
582
586
  stateEntries: opts.stateEntries ?? null,
583
587
  },
@@ -16,7 +16,7 @@
16
16
  * **The chip is a `jx-tab` now, and three of the things it used to draw by hand are the kit's**:
17
17
  * the selected state (`aria-selected` on a real `tab` inside a real `tablist`, where an `.active`
18
18
  * class used to be), the close button, and the dirty dot. The three that are Studio's own live in
19
- * `jx-tab`'s slots — the drill-in marker in `icon`, the draft pill in `status`, the pin toggle in
19
+ * `jx-tab`'s slots — the drill-in marker in `icon`, the hold pill in `status`, the pin toggle in
20
20
  * `actions` — which is the gap that kept this strip out of the tab pattern (`ui.md` §5.4). With it
21
21
  * closed the strip is one stop in the tab order, the arrows walk it, Home and End reach its ends,
22
22
  * and Delete closes a tab: a keyboard contract the × has never had.
@@ -57,8 +57,8 @@ import { gridTabLabel } from "../grid/grid-source";
57
57
  import { workspaceTabOrder } from "../workspace/tab-workspace";
58
58
  import { workspaceHomeLabel } from "../workspace/workspace-ids";
59
59
  import { shell, workspaceDrawsTabStrip } from "../shell";
60
- import { entryDraftPill } from "../content/entry-editor";
61
- import { DRAFT_FIELD, isDraftEntry } from "../content/draft-state";
60
+ import { entryHoldPill } from "../content/entry-editor";
61
+ import { holdFields, holdSwitch } from "../content/hold-state";
62
62
  import { entryFields } from "../content/entry-fields";
63
63
  import { collectionOfPath } from "../content/entry-model";
64
64
  import { activeRegistry } from "../commands/active-registry";
@@ -262,9 +262,11 @@ export function mount(host: HTMLElement) {
262
262
  void tab.pinned;
263
263
  void tab.preview;
264
264
  void tab.session.openedFrom;
265
- // The draft pill is on the chip, so the chip has to repaint when the flag changes — reading
266
- // The absent key tracks its ADDITION too, which is the transition that matters most.
267
- void tab.doc.content.frontmatter[DRAFT_FIELD];
265
+ // The hold pill is on the chip, so the chip has to repaint when a field its rules read
266
+ // Changes. Reading an absent key tracks its ADDITION too, which matters most.
267
+ for (const field of holdFields(collectionOfPath(tab.documentPath)?.def)) {
268
+ void tab.doc.content.frontmatter[field];
269
+ }
268
270
  }
269
271
  // An edit is a commitment: a preview tab with unsaved changes is no longer disposable, and
270
272
  // The dirty flags this effect already tracks are exactly the signal.
@@ -685,15 +687,15 @@ function chipView(
685
687
  if (!tab) {
686
688
  return [];
687
689
  }
688
- /* The draft pill (`site-architecture.md` §7.6). On the CHIP, not only inside the entry editor:
690
+ /* The hold pill (`site-architecture.md` §7.6). On the CHIP, not only inside the entry editor:
689
691
  the mistake this prevents — publishing something you believed was private — is made while
690
692
  glancing at a row of tabs, and it is made about a document that may not even be the active
691
693
  one. */
692
- const pill = entryDraftPill(tab);
694
+ const pill = entryHoldPill(tab);
693
695
  return [
694
696
  {
695
697
  dirty: tab.doc.dirty || imageEditorDirty(tab),
696
- draft: pill?.draft ?? false,
698
+ held: pill?.held ?? false,
697
699
  index,
698
700
  key: id,
699
701
  label: labels.get(id) ?? "Untitled",
@@ -1011,24 +1013,25 @@ export function dismissTabContextMenu() {
1011
1013
  * A tab addresses ONE document, and this is everything the chip can say about it — keyed by the
1012
1014
  * ARGUMENT NAME that asks for it.
1013
1015
  *
1014
- * The whole `context/tab` placement asks for exactly one argument today: `content.setDraft
1015
- * {draft}`. The other five records take none, because they read the ACTIVE document — which is why
1016
+ * The whole `context/tab` placement asks for exactly one argument today: `content.setHold {hold}`.
1017
+ * The other five records take none, because they read the ACTIVE document — which is why
1016
1018
  * {@link openTabContextMenu} activates the chip before it builds a single row.
1017
1019
  *
1018
1020
  * A fact is stated ONLY when it is true of this tab, and that is what decides whether a command
1019
- * appears at all: `styles/main.css` is an entry of no collection, so it states no `draft`, so "Set
1020
- * Draft" is not offered on it. The condition is `collectionOfPath` — the same question
1021
- * `content.setDraft`'s own `enablement` asks — so the menu never invents a rule the command does
1022
- * not have, and never renders a row into a refusal.
1021
+ * appears at all: `styles/main.css` is an entry of no collection, so it has no hold rules, so "Set
1022
+ * Hold" is not offered on it. The condition is `collectionOfPath` — the same question
1023
+ * `content.setHold`'s own `enablement` asks — so the menu never invents a rule the command does not
1024
+ * have, and never renders a row into a refusal.
1023
1025
  *
1024
1026
  * The VALUE is the state the row would reach, not the state the tab is in: a setter is named for
1025
- * where it lands (`content/draft-state.ts` says why), so the row offers the flip of what is true
1026
- * now and {@link statedChecked} reads the current state back out of it for the checkmark.
1027
+ * where it lands (`content/hold-state.ts` says why), so the row offers the flip of what is true now
1028
+ * and {@link statedChecked} reads the current state back out of it for the checkmark.
1027
1029
  */
1028
1030
  function tabRowFacts(tab: Tab): CommandArgValues {
1029
1031
  const facts: CommandArgValues = {};
1030
- if (collectionOfPath(tab.documentPath)) {
1031
- facts.draft = !isDraftEntry(entryFields(tab));
1032
+ const toggle = holdSwitch(collectionOfPath(tab.documentPath)?.def);
1033
+ if (toggle) {
1034
+ facts.hold = entryFields(tab)[toggle.field] !== true;
1032
1035
  }
1033
1036
  return facts;
1034
1037
  }
@@ -1040,16 +1043,16 @@ function tabRowFacts(tab: Tab): CommandArgValues {
1040
1043
  * A menu is the one surface that is READ before it is used, so it is the one surface that can show
1041
1044
  * a boolean instead of asking the author to remember it. That is an argument for the SETTER over
1042
1045
  * the toggle, and it is why this is derived from the args rather than from a list of command ids:
1043
- * `content.setDraft {draft}` names the state it reaches, so the row can say which state the tab is
1044
- * in now and still land somewhere definite when clicked. `document.togglePinned` names no state —
1045
- * "Pin / Unpin Document" is a title that admits it does not know which it will do — so it renders
1046
- * as a plain row here. Its idempotent sibling `document.setPinned {pinned}` would state its own
1047
- * value, with no edit to this file, on the day its record declares `context/tab`; that declaration
1048
- * lives in `workspace/workspace.ts`, which is the only place it can be made.
1046
+ * `content.setHold {hold}` names the state it reaches, so the row can say which state the tab is in
1047
+ * now and still land somewhere definite when clicked. `document.togglePinned` names no state — "Pin
1048
+ * / Unpin Document" is a title that admits it does not know which it will do — so it renders as a
1049
+ * plain row here. Its idempotent sibling `document.setPinned {pinned}` would state its own value,
1050
+ * with no edit to this file, on the day its record declares `context/tab`; that declaration lives
1051
+ * in `workspace/workspace.ts`, which is the only place it can be made.
1049
1052
  *
1050
1053
  * **It is a real checkbox row now, which it could not be under Spectrum.** `sp-menu` reassigned
1051
1054
  * every item's role one frame after connect whenever the menu declared no `selects`, so
1052
- * `role="menuitemcheckbox"` did not survive and the state had to be printed as a sentence ("Draft:
1055
+ * `role="menuitemcheckbox"` did not survive and the state had to be printed as a sentence ("Held:
1053
1056
  * no") in the description line instead. `jx-menu-item` derives `role` and `aria-checked` from
1054
1057
  * `checked` and nothing rewrites them, so the row states the boolean the way a screen reader
1055
1058
  * already knows how to announce — and the five rows that carry no state stay plain `menuitem`s,
@@ -28,7 +28,7 @@ import { adoptCreatedProject } from "./project-adoption";
28
28
  import { translateValidationError } from "./ai-tools";
29
29
  import { validateDoc, validateProjectConfig } from "./jx-validate";
30
30
  import { flagHardcodedTokens, formatTokenHints } from "./token-lint";
31
- import type { CandidateDocument } from "./ai-write-report";
31
+ import type { CandidateDocument, RenderProbe } from "./ai-write-report";
32
32
  import { invalidPathResult, normalizeRelPath } from "./project-path";
33
33
 
34
34
  /** Directories the file tools never descend into or report. */
@@ -71,7 +71,7 @@ export interface ProjectToolsCtx {
71
71
  * judges the live configuration and a candidate parsed from the model's text alike.
72
72
  */
73
73
  validateProject?: (config: ProjectConfig | JsonValue) => Promise<string[]>;
74
- renderCheck?: (doc: CandidateDocument) => Promise<{ ok: true } | { ok: false; error: string }>;
74
+ renderCheck?: RenderProbe;
75
75
  /** Live getter — the project style appears only after a project is open/bootstrapped. */
76
76
  getProjectStyle?: () => Record<string, string> | undefined;
77
77
  /** The open tab whose `documentPath` equals the given project-relative path, if any. */
@@ -323,7 +323,7 @@ export function registerProjectTools(
323
323
  };
324
324
  }
325
325
  if (renderCheck) {
326
- const renderResult = await renderCheck(parsed);
326
+ const renderResult = await renderCheck(parsed, { documentPath: relPath });
327
327
  if (!renderResult.ok) {
328
328
  return {
329
329
  success: false,
@@ -18,7 +18,7 @@ import { validateDoc } from "./jx-validate";
18
18
  import { invalidPathResult, normalizeRelPath } from "./project-path";
19
19
  import { serializeJson } from "@jxsuite/schema/json-layout";
20
20
  import { translateValidationError } from "./ai-write-report";
21
- import type { CandidateDocument } from "./ai-write-report";
21
+ import type { CandidateDocument, RenderProbe } from "./ai-write-report";
22
22
  import { studioDocumentHost, studioWriteReporter } from "./harness/studio-documents";
23
23
 
24
24
  /* Re-exported from its new home so `ai-project-tools.ts` and the tests keep their import: the
@@ -34,7 +34,7 @@ export { translateValidationError } from "./ai-write-report";
34
34
  * getTab: () => import("../tabs/tab").Tab | null;
35
35
  * validate?: (doc: CandidateDocument) => Promise<string[]>;
36
36
  * saveFile?: (relPath: string, content: string) => Promise<void>;
37
- * renderCheck?: (doc: CandidateDocument) => Promise<{ ok: true } | { ok: false; error: string }>;
37
+ * renderCheck?: RenderProbe;
38
38
  * projectStyle?: Record<string, string>;
39
39
  * getProjectStyle?: () => Record<string, string> | undefined;
40
40
  * findOpenTab?: (path: string) => import("../tabs/tab").Tab | null;
@@ -56,7 +56,7 @@ export function registerAiTools(
56
56
  getTab: () => Tab | null;
57
57
  validate?: (doc: CandidateDocument) => Promise<string[]>;
58
58
  saveFile?: (relPath: string, content: string) => Promise<void>;
59
- renderCheck?: (doc: CandidateDocument) => Promise<{ ok: true } | { ok: false; error: string }>;
59
+ renderCheck?: RenderProbe;
60
60
  projectStyle?: Record<string, string> | undefined;
61
61
  /**
62
62
  * Live variant of `projectStyle` — takes precedence; re-read per tool call so a project
@@ -164,7 +164,7 @@ export function registerAiTools(
164
164
  };
165
165
  }
166
166
  if (renderCheck) {
167
- const renderResult = await renderCheck(content);
167
+ const renderResult = await renderCheck(content, { documentPath: relPath });
168
168
  if (!renderResult.ok) {
169
169
  return {
170
170
  success: false,
@@ -252,7 +252,7 @@ export function registerAiTools(
252
252
  };
253
253
  }
254
254
  if (renderCheck) {
255
- const renderResult = await renderCheck(content);
255
+ const renderResult = await renderCheck(content, { documentPath: relPath });
256
256
  if (!renderResult.ok) {
257
257
  return {
258
258
  success: false,
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import type { ToolResult } from "@jxsuite/ai/tools";
25
- import type { JsonValue, JxMutableNode } from "@jxsuite/schema/types";
25
+ import type { JsonValue, JxDocumentValue, JxMutableNode } from "@jxsuite/schema/types";
26
26
  import { toRaw } from "../reactivity";
27
27
  import type { Tab } from "../tabs/tab";
28
28
  import { flagHardcodedTokens, formatTokenHints } from "./token-lint";
@@ -37,12 +37,28 @@ export type RenderVerdict = { ok: true } | { ok: false; error: string };
37
37
  */
38
38
  export type CandidateDocument = JxMutableNode | JsonValue;
39
39
 
40
+ /** Where a document under a render check lives: what its route and relative references resolve from. */
41
+ export interface RenderSource {
42
+ /** Its project-relative path, once it has one. */
43
+ documentPath?: string | null | undefined;
44
+ /** A format-class page's frontmatter, which is where its `$paths` is. */
45
+ frontmatter?: Record<string, JxDocumentValue | undefined> | null | undefined;
46
+ }
47
+
48
+ /** A detached-DOM render probe. */
49
+ export type RenderProbe = (doc: CandidateDocument, source?: RenderSource) => Promise<RenderVerdict>;
50
+
51
+ /** Where an open tab's document lives, as a render check reads it. */
52
+ function renderSourceOf(tab: Tab): RenderSource {
53
+ return { documentPath: tab.documentPath, frontmatter: tab.doc.content.frontmatter };
54
+ }
55
+
40
56
  /** What a write reporter needs. Every member is what `registerAiTools` already takes by injection. */
41
57
  export interface WriteReportDeps {
42
58
  /** Schema errors for a document, as raw ajv sentences. */
43
59
  validate: (doc: CandidateDocument) => Promise<string[]>;
44
60
  /** The detached-DOM render probe, when the host has one. */
45
- renderCheck?: ((doc: CandidateDocument) => Promise<RenderVerdict>) | undefined;
61
+ renderCheck?: RenderProbe | undefined;
46
62
  /** The project's design tokens, read per call so a project opened mid-session contributes. */
47
63
  getProjectStyle?: (() => Record<string, string> | undefined) | undefined;
48
64
  }
@@ -122,7 +138,9 @@ export function translateValidationError(rawError: string): string {
122
138
  export async function snapshotBeforeWrite(tab: Tab, deps: WriteReportDeps): Promise<WriteSnapshot> {
123
139
  const rawBefore = toRaw(tab.doc.document);
124
140
  const errors = new Set(await deps.validate(rawBefore));
125
- const rendered = deps.renderCheck ? await deps.renderCheck(rawBefore) : { ok: true };
141
+ const rendered = deps.renderCheck
142
+ ? await deps.renderCheck(rawBefore, renderSourceOf(tab))
143
+ : { ok: true };
126
144
  return { errors, renderOk: rendered.ok };
127
145
  }
128
146
 
@@ -159,7 +177,7 @@ export async function reportDocumentWrite(
159
177
  }
160
178
 
161
179
  if (deps.renderCheck && before.renderOk) {
162
- const renderResult = await deps.renderCheck(rawAfter);
180
+ const renderResult = await deps.renderCheck(rawAfter, renderSourceOf(tab));
163
181
  if (!renderResult.ok) {
164
182
  return {
165
183
  success: false,