@jxsuite/studio 10.4.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 (274) 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-v0pm96mr.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-r8sfxjjm.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-vjzw46xb.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-2f03wcgw.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-4fwngwrw.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-9gr7xs9r.js → studio-4jy967rj.js} +5 -5
  67. package/dist/chunks/{studio-4bgvvzhv.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-wmjmsw0p.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-bg8pzprg.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-evq47j2z.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-s24mhq0z.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-2pk7kc7z.js → studio-d5f7sskn.js} +4 -4
  91. package/dist/chunks/studio-dmgh2rrj.js +5 -0
  92. package/dist/chunks/{studio-nwf0c32p.js → studio-dst8y8vh.js} +4 -4
  93. package/dist/chunks/studio-ehj3csbm.js +6 -0
  94. package/dist/chunks/{studio-hfqpfmpk.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-jma2q8r9.js → studio-g65t6svn.js} +4 -4
  102. package/dist/chunks/{studio-r6mx7vm5.js → studio-gw83z798.js} +5 -5
  103. package/dist/chunks/{studio-2x972v58.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-bnc50f9h.js → studio-k05g7efh.js} +4 -4
  108. package/dist/chunks/studio-kefs4sz4.js +5 -0
  109. package/dist/chunks/{studio-2e4hry72.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-gnazwtv3.js → studio-ncwx602y.js} +4 -4
  115. package/dist/chunks/{studio-amaz545p.js → studio-nngc45ct.js} +4 -4
  116. package/dist/chunks/{studio-eqcddvc2.js → studio-nv5eq0f7.js} +173 -173
  117. package/dist/chunks/{studio-az3wvyh2.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-rr00ex23.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-61rg2cz5.js → studio-td3h1644.js} +4 -4
  129. package/dist/chunks/{studio-v7a7yb5w.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-a88ej93p.js → studio-ze1sm147.js} +6 -6
  141. package/dist/chunks/{studio-pj216fmn.js → studio-zkkt13qd.js} +4 -4
  142. package/dist/chunks/studio-zn7ysyzb.js +5 -0
  143. package/dist/chunks/{studio-xx3r3sb0.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/tab-strip.json +5 -5
  192. package/src/surfaces/tab-strip.ts +2 -2
  193. package/src/types.ts +2 -0
  194. package/src/ui/expression-editor.ts +10 -5
  195. package/src/ui/schema-form.ts +18 -1
  196. package/src/utils/edit-display.ts +27 -1
  197. package/dist/chunks/2020-906vx2dq.js +0 -4
  198. package/dist/chunks/adapter-hnvad61x.js +0 -4
  199. package/dist/chunks/cf-settings-mkbcz4z2.js +0 -4
  200. package/dist/chunks/color-04361ttk.js +0 -4
  201. package/dist/chunks/components-w5zqwva1.js +0 -4
  202. package/dist/chunks/content-source-g38w0d3f.js +0 -4
  203. package/dist/chunks/editor-wvf5e6j9.js +0 -4
  204. package/dist/chunks/entry-commands-t13dyw3c.js +0 -4
  205. package/dist/chunks/file-ops-1sghzgcr.js +0 -4
  206. package/dist/chunks/files-zrqtgt41.js +0 -4
  207. package/dist/chunks/format-host-9gy76erh.js +0 -4
  208. package/dist/chunks/fs-events-45rhg3me.js +0 -4
  209. package/dist/chunks/grid-open-t4fq6bbm.js +0 -4
  210. package/dist/chunks/iframe-host-9aq8ze3q.js +0 -4
  211. package/dist/chunks/index-f9qr7kzh.js +0 -4
  212. package/dist/chunks/jsonMode-vj30te2h.js +0 -13
  213. package/dist/chunks/jx-validate-r4ax11e3.js +0 -4
  214. package/dist/chunks/layers-m49nmwvx.js +0 -4
  215. package/dist/chunks/layers-panel-zr92pwdp.js +0 -4
  216. package/dist/chunks/lit-html-k2mb9k0r.js +0 -4
  217. package/dist/chunks/media-library-j74rt0w4.js +0 -4
  218. package/dist/chunks/media-open-mypr0k6b.js +0 -4
  219. package/dist/chunks/media-picker-pemtbsjv.js +0 -4
  220. package/dist/chunks/menu-d2jxpgma.js +0 -4
  221. package/dist/chunks/monaco-buffer-pa0g6ax7.js +0 -4
  222. package/dist/chunks/monaco-setup-0hw9f835.js +0 -4
  223. package/dist/chunks/notify-bwehrp7q.js +0 -4
  224. package/dist/chunks/platform-kv0k65cy.js +0 -4
  225. package/dist/chunks/popover-9tf4cjrk.js +0 -4
  226. package/dist/chunks/publish-panel-vg1n880b.js +0 -4
  227. package/dist/chunks/publish-workspace-jv4w2bx2.js +0 -4
  228. package/dist/chunks/quick-search-nh29rt8a.js +0 -4
  229. package/dist/chunks/references-vqyr5s9x.js +0 -4
  230. package/dist/chunks/right-panel-3ws9f2w0.js +0 -4
  231. package/dist/chunks/source-control-h3hcs5gy.js +0 -4
  232. package/dist/chunks/studio-0khd8xtx.js +0 -4
  233. package/dist/chunks/studio-1ctnw3xv.js +0 -5
  234. package/dist/chunks/studio-1ks5d9y4.js +0 -5
  235. package/dist/chunks/studio-1y9mab9m.js +0 -5
  236. package/dist/chunks/studio-2kpxm0eh.js +0 -7
  237. package/dist/chunks/studio-2zz4qbv9.js +0 -5
  238. package/dist/chunks/studio-3pz4t5a2.js +0 -5
  239. package/dist/chunks/studio-4f7thq0p.js +0 -5
  240. package/dist/chunks/studio-7rrdbvex.js +0 -5
  241. package/dist/chunks/studio-9dq7rwc7.js +0 -5
  242. package/dist/chunks/studio-acer0g8w.js +0 -5
  243. package/dist/chunks/studio-brh8d2pr.js +0 -5
  244. package/dist/chunks/studio-c2fa783p.js +0 -9
  245. package/dist/chunks/studio-cfnn70h1.js +0 -5
  246. package/dist/chunks/studio-cqxj4fcy.js +0 -5
  247. package/dist/chunks/studio-cw11h078.js +0 -5
  248. package/dist/chunks/studio-dn56z7sn.js +0 -5
  249. package/dist/chunks/studio-egnjpyde.js +0 -9
  250. package/dist/chunks/studio-ejrypvmq.js +0 -5
  251. package/dist/chunks/studio-frc16j8v.js +0 -10
  252. package/dist/chunks/studio-j0zf5x2x.js +0 -5
  253. package/dist/chunks/studio-j5fcp6gj.js +0 -5
  254. package/dist/chunks/studio-jd1qb1st.js +0 -7
  255. package/dist/chunks/studio-k1xqrgm5.js +0 -5
  256. package/dist/chunks/studio-k3we3kqb.js +0 -5
  257. package/dist/chunks/studio-knsjyx7n.js +0 -5
  258. package/dist/chunks/studio-mrjtnm62.js +0 -5
  259. package/dist/chunks/studio-n9qba0gx.js +0 -5
  260. package/dist/chunks/studio-nzemfbkk.js +0 -14
  261. package/dist/chunks/studio-pwctzka3.js +0 -16
  262. package/dist/chunks/studio-q80fg1d6.js +0 -12
  263. package/dist/chunks/studio-q9hx2qp0.js +0 -5
  264. package/dist/chunks/studio-qrzmfaf6.js +0 -5
  265. package/dist/chunks/studio-tmfcn347.js +0 -6
  266. package/dist/chunks/studio-wqfaqw4m.js +0 -5
  267. package/dist/chunks/studio-xpx91fby.js +0 -6
  268. package/dist/chunks/studio-xva3hzkv.js +0 -5
  269. package/dist/chunks/studio-z3fztxrp.js +0 -9
  270. package/dist/chunks/studio-z9453ypn.js +0 -6
  271. package/dist/chunks/tsMode-jg1dtphj.js +0 -14
  272. package/dist/chunks/workspace-6ztqcrsh.js +0 -4
  273. package/dist/chunks/workspace-home-rse9h8a0.js +0 -4
  274. package/src/content/draft-state.ts +0 -108
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * **This module is the FLOW; `surfaces/entry-editor.json` is the markup.** Everything below decides
18
18
  * something — which pane holds which tab, which collection a path belongs to, which record the
19
- * fields live in, which required keys are absent, what a draft flag commits into — and hands the
19
+ * fields live in, which required keys are absent, what a hold switch commits into — and hands the
20
20
  * surface a projection with no decisions left in it. The form itself is an island the surface
21
21
  * renders empty: `ui/schema-form.ts` hands back a host ELEMENT, and {@link renderEntryMode} places
22
22
  * it in the node the document announces.
@@ -51,7 +51,7 @@ import { paneRegion } from "../ui/regions";
51
51
  import { activateTab, workspace } from "../workspace/workspace";
52
52
  import { commitEntryFields, entryFields, mutateEntryField } from "./entry-fields";
53
53
  import type { EntryFields } from "./entry-fields";
54
- import { DRAFT_FIELD, DRAFT_MEANING, hasDraftAxis, isDraftEntry } from "./draft-state";
54
+ import { hasHold, holdMeaning, holdOf, holdStatus, holdSwitch } from "./hold-state";
55
55
  import { collectionOfPath, missingRequired, recordCollectionOfPath } from "./entry-model";
56
56
  import { ENTRY_MODE, isRecordTab, recordPlace, showRecord } from "./record-entry";
57
57
  import { requestGridReveal } from "../grid/grid-reveal";
@@ -115,56 +115,55 @@ export function detachEntryPane(paneId: string): void {
115
115
  _active.delete(paneId);
116
116
  }
117
117
 
118
- // ─── Drafts ──────────────────────────────────────────────────────────────────
118
+ // ─── Holds ───────────────────────────────────────────────────────────────────
119
119
 
120
120
  /**
121
- * Set this entry's draft flag. A setter, never a toggle — the pill, the command and the assistant
122
- * all reach the same state whatever it was before.
121
+ * Flip the switch this entry's collection offers (`holdSwitch`). A setter, never a toggle: the
122
+ * pill, the command and the assistant all reach the same state whatever it was before. Does nothing
123
+ * on a collection whose rules offer no switch.
123
124
  *
124
- * `draft: false` is written rather than removed. The absence of the key and the value `false` mean
125
- * the same thing to a reader, but only one of them survives a glance at the file, and an author who
126
- * has explicitly published something should be able to see that they did.
125
+ * `false` is written rather than removed. The absence of the key and the value `false` mean the
126
+ * same thing to a reader, but only one of them survives a glance at the file, and an author who has
127
+ * explicitly released something should be able to see that they did.
127
128
  */
128
- export function setEntryDraft(tab: Tab, draft: boolean): void {
129
- transactDoc(tab, (t) => mutateEntryField(t, DRAFT_FIELD, draft));
129
+ export function setEntryHold(tab: Tab, on: boolean): void {
130
+ const toggle = holdSwitch(entryCollectionOf(tab)?.def);
131
+ if (toggle) {
132
+ transactDoc(tab, (t) => mutateEntryField(t, toggle.field, on));
133
+ }
130
134
  }
131
135
 
132
- /** What the tab chip's draft pill says. A value, drawn by `surfaces/tab-strip.json`. */
133
- export interface DraftPill {
134
- /** The word on the pill: "Draft" or "Published". */
136
+ /** What the tab chip's pill says. A value, drawn by `surfaces/tab-strip.json`. */
137
+ export interface HoldPill {
138
+ /** The word on the pill: the rule's label, or "Published". */
135
139
  text: string;
136
- /** Its tooltip: what being a draft actually excludes, or that it is not one. */
140
+ /** Its tooltip: what being held actually does, or that it is not. */
137
141
  title: string;
138
- draft: boolean;
142
+ held: boolean;
139
143
  }
140
144
 
141
145
  /**
142
- * The draft pill for a tab, or `null` when its document has no draft axis at all.
146
+ * The pill for a tab, or `null` when its collection states no rules.
143
147
  *
144
148
  * Drawn on the pane's tab chip (`panels/tab-strip.ts`) rather than only inside the editor, because
145
- * the failure this exists to prevent is publishing something you thought was private — and that
146
- * mistake is made while looking at a tab, not while looking at a form. It states its two words for
147
- * any entry whose collection declares the field, so "Published" is as visible as "Draft"; a
148
- * collection with no draft workflow states neither, which is what `null` means.
149
+ * the failure this exists to prevent is publishing something you thought was private, and that
150
+ * mistake is made while looking at a tab, not while looking at a form. It states its state for any
151
+ * entry whose collection has rules, so "Published" is as visible as "Draft"; a collection with none
152
+ * states neither, which is what `null` means.
149
153
  *
150
154
  * **It was the ONE piece of markup left in this file, and it was not this surface's.** It was a
151
- * fragment of the tab CHIP — a `<span>` lit interpolated into a `repeat()` that rebuilt on every
152
- * strip repaint — and it said so, and that it would convert when `panels/tab-strip.ts` converted.
153
- * The strip is a Jx document now, so this is a projection: two strings and a flag, drawn by the
154
- * chip's `status` slot, and `.entry-pill`'s rules in `styles/overlays.css` are dead with it.
155
+ * fragment of the tab CHIP, a `<span>` lit interpolated into a `repeat()` that rebuilt on every
156
+ * strip repaint. The strip is a Jx document now, so this is a projection: two strings and a flag,
157
+ * drawn by the chip's `status` slot.
155
158
  */
156
- export function entryDraftPill(tab: Tab): DraftPill | null {
157
- const collection = entryCollectionOf(tab);
158
- const fields = entryFields(tab);
159
- if (!collection || !hasDraftAxis(collection.schema, fields)) {
159
+ export function entryHoldPill(tab: Tab): HoldPill | null {
160
+ const def = entryCollectionOf(tab)?.def;
161
+ if (!hasHold(def)) {
160
162
  return null;
161
163
  }
162
- const draft = isDraftEntry(fields);
163
- return {
164
- draft,
165
- text: draft ? "Draft" : "Published",
166
- title: draft ? DRAFT_MEANING : "Not marked a draft.",
167
- };
164
+ const fields = entryFields(tab);
165
+ const label = holdOf(def, fields);
166
+ return { held: label !== null, text: holdStatus(def, fields), title: holdMeaning(label) };
168
167
  }
169
168
 
170
169
  // ─── The form ────────────────────────────────────────────────────────────────
@@ -255,11 +254,12 @@ function entryView(paneId: string, tab: Tab, collection: EntryCollection | null)
255
254
  if (!collection) {
256
255
  return {
257
256
  collection: "",
258
- draft: false,
259
- draftAxis: "hidden",
260
- draftHint: DRAFT_MEANING,
261
257
  emptyLine: emptyLine(tab),
262
258
  fieldsRegion,
259
+ holdHint: holdMeaning(null),
260
+ holdLabel: "",
261
+ holdOn: false,
262
+ holdSwitch: "hidden",
263
263
  name: "",
264
264
  nextDisabled: true,
265
265
  note: "",
@@ -272,23 +272,25 @@ function entryView(paneId: string, tab: Tab, collection: EntryCollection | null)
272
272
  };
273
273
  }
274
274
  const fields = entryFields(tab);
275
- const draft = isDraftEntry(fields);
275
+ const label = holdOf(collection.def, fields);
276
+ const toggle = holdSwitch(collection.def);
276
277
  const record = isRecordTab(tab);
277
278
  const place = recordPlace(tab);
278
279
  return {
279
280
  collection: collection.name,
280
- draft,
281
- draftAxis: hasDraftAxis(collection.schema, fields) ? "shown" : "hidden",
282
- draftHint: DRAFT_MEANING,
283
281
  emptyLine: "",
284
282
  fieldsRegion,
283
+ holdHint: holdMeaning(label),
284
+ holdLabel: toggle?.label ?? "",
285
+ holdOn: toggle !== null && fields[toggle.field] === true,
286
+ holdSwitch: toggle ? "shown" : "hidden",
285
287
  // A record is called what the Content home calls it: its title field, else its row.
286
288
  name: record
287
289
  ? recordName(collection, fields, place.index)
288
290
  : (tab.documentPath?.split("/").pop() ?? "Untitled"),
289
291
  nextDisabled: place.next === null,
290
- note: DRAFT_MEANING,
291
- noteState: draft ? "shown" : "hidden",
292
+ note: holdMeaning(label),
293
+ noteState: label === null ? "hidden" : "shown",
292
294
  place: place.index === -1 ? "" : `${place.index + 1} of ${place.total}`,
293
295
  previousDisabled: place.previous === null,
294
296
  recordNav: record ? "shown" : "hidden",
@@ -401,7 +403,7 @@ export function renderEntryMode(surface: CanvasSurface, tab: Tab): void {
401
403
  }
402
404
  tab.session.ui.canvasMode = "grid";
403
405
  },
404
- setDraft: (draft) => setEntryDraft(tab, draft),
406
+ setHold: (on) => setEntryHold(tab, on),
405
407
  showNext: () => {
406
408
  const { next } = recordPlace(tab);
407
409
  if (next !== null) {
@@ -3,9 +3,9 @@
3
3
  * The Inspector's Entry tab: what the Page tab draws while a collection entry is the focused
4
4
  * document (`specs/studio.md` §6, §22.5; the Content-Entry board's sidebar).
5
5
  *
6
- * Four things, in the board's order: the entry's **status** (its draft flag, when the collection
7
- * has a draft workflow, `content/draft-state.ts`), its **fields** as the collection's schema types
8
- * them (`ui/schema-form.ts` over `content/entry-fields.ts`, the same form and the same transacted
6
+ * Four things, in the board's order: the entry's **status** (what its collection's `hold` rules
7
+ * say, `content/hold-state.ts`), its **fields** as the collection's schema types them
8
+ * (`ui/schema-form.ts` over `content/entry-fields.ts`, the same form and the same transacted
9
9
  * commits as the form-only editor, so ⌘Z undoes a field here exactly as it does there), the **URL**
10
10
  * its collection page publishes it at (`content/collection-pages.ts`), and the **file** it is
11
11
  * stored in, with what that file holds.
@@ -30,13 +30,13 @@ import {
30
30
  peekCollectionPage,
31
31
  } from "./collection-pages";
32
32
  import { commitEntryFields, entryFields } from "./entry-fields";
33
- import { DRAFT_MEANING, hasDraftAxis, isDraftEntry } from "./draft-state";
33
+ import { holdMeaning, holdOf, holdStatus, holdSwitch } from "./hold-state";
34
34
  import { entryCollection } from "./entry-model";
35
35
  import {
36
36
  absentRequiredErrors,
37
37
  fieldLabels,
38
38
  formContext,
39
- setEntryDraft,
39
+ setEntryHold,
40
40
  titleFirst,
41
41
  } from "./entry-editor";
42
42
  import { mountEntryInspectorSurface } from "../surfaces/entry-inspector";
@@ -64,20 +64,23 @@ export function entryInspectorView(tab: Tab): EntryInspectorView {
64
64
  const name = match?.name ?? "";
65
65
  const collection = entryCollection(name);
66
66
  const fields = entryFields(tab);
67
- const draftAxis = hasDraftAxis(collection?.schema, fields);
68
- const draft = isDraftEntry(fields);
67
+ const def = collection?.def;
68
+ const label = holdOf(def, fields);
69
+ const toggle = holdSwitch(def);
69
70
  const page = name === "" ? null : peekCollectionPage(name, entryLocale(path));
70
71
  const url = page ? entryUrl(page, path, fields) : null;
71
72
  const isJson = path.endsWith(".json");
72
73
  return {
73
- draft,
74
- draftAxis: draftAxis ? "shown" : "hidden",
75
- draftHint: DRAFT_MEANING,
76
74
  fieldsState: collection?.schema ? "form" : "none",
75
+ held: label !== null,
76
+ holdHint: holdMeaning(label),
77
+ holdLabel: toggle?.label ?? "",
78
+ holdOn: toggle !== null && fields[toggle.field] === true,
79
+ holdSwitch: toggle ? "shown" : "hidden",
77
80
  kind: capitalise(contentCollection(name)?.item ?? "entry"),
78
81
  path,
79
82
  region: ENTRY_INSPECTOR_REGION,
80
- status: draftAxis ? (draft ? "Draft" : "Published") : "",
83
+ status: holdStatus(def, fields),
81
84
  storage: isJson
82
85
  ? ". Its fields are the file itself."
83
86
  : ". Fields are its frontmatter, and the body is its Markdown.",
@@ -194,7 +197,7 @@ export function renderEntryInspector(host: HTMLElement, tab: Tab): void {
194
197
  openSeo: () => {
195
198
  void runActiveReported("document.openSeo", undefined, "Entry");
196
199
  },
197
- setDraft: (draft) => setEntryDraft(tab, draft),
200
+ setHold: (on) => setEntryHold(tab, on),
198
201
  },
199
202
  {
200
203
  fieldsSlot: (element) => {
@@ -18,9 +18,9 @@
18
18
  * `grid/sources/content-source.ts`'s `collectionInfo`, and `browse/library-model.ts`'s
19
19
  * `contentTypeFor`, since deleted); this module adds no fourth and calls the exported ones.
20
20
  *
21
- * **The draft axis is NOT here.** `content/draft-state.ts` owns what a draft is and whether drafts
22
- * are listed, because this module imports `content-source.ts` and the grid source must be able to
23
- * ask what a draft is — the reverse edge would be a cycle, and `import/no-cycle` is an error here.
21
+ * **The hold axis is NOT here.** `content/hold-state.ts` owns what a hold is and whether hrafts are
22
+ * listed, because this module imports `content-source.ts` and the grid source must be able to ask
23
+ * what a draft is — the reverse edge would be a cycle, and `import/no-cycle` is an error here.
24
24
  */
25
25
 
26
26
  import { collectionInfo } from "../grid/sources/content-source";
@@ -25,7 +25,7 @@ import {
25
25
  import { getPlatform, hasPlatform } from "../platform";
26
26
  import { isImage } from "../files/media-upload";
27
27
  import { previewFileSrc } from "../files/media-paths";
28
- import { hasDraftAxis, isDraftEntry } from "./draft-state";
28
+ import { holdStatus } from "./hold-state";
29
29
  import { entryCollection } from "./entry-model";
30
30
  import { PAGES_COLLECTION, collectionKeyOfPath } from "./content-collections";
31
31
  import type { EntryFields } from "./entry-fields";
@@ -36,7 +36,7 @@ export interface EntrySummary {
36
36
  path: string;
37
37
  /** The entry's `title`, or its file name without the extension. */
38
38
  title: string;
39
- /** "Draft", "Published", or "" for a collection with no draft workflow. */
39
+ /** A `hold` rule's label, "Published", or "" for a collection with no rules. */
40
40
  status: string;
41
41
  /** A preview URL for the entry's first image field, or "" for none. */
42
42
  thumb: string;
@@ -278,16 +278,16 @@ export function entryThumb(
278
278
 
279
279
  /** One entry's summary, from its fields. Pure. */
280
280
  export function summarize(path: string, collection: string, fields: EntryFields): EntrySummary {
281
- const schema = entryCollection(collection)?.schema ?? null;
281
+ const definition = entryCollection(collection);
282
+ const schema = definition?.schema ?? null;
282
283
  const titleField = titleFieldOf(schema, fields);
283
284
  const title = titleField === null ? undefined : fields[titleField];
284
- const draft = isDraftEntry(fields);
285
285
  const dateField = dateFieldOf(schema, fields);
286
286
  return {
287
287
  date: dateField === null ? "" : entryDay(fields[dateField]),
288
288
  fields: fieldTexts(schema, fields),
289
289
  path,
290
- status: hasDraftAxis(schema, fields) ? (draft ? "Draft" : "Published") : "",
290
+ status: holdStatus(definition?.def, fields),
291
291
  thumb: entryThumb(path, fields, schema),
292
292
  title: isString(title) && title.trim() !== "" ? title : entryStem(path),
293
293
  };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * The hold axis (site-architecture.md §7.6): what a project says keeps an entry back, what state an
3
+ * entry is in by those rules, and whether held entries are listed. One place, every list.
4
+ *
5
+ * Jx gives no field a meaning of its own. A project names its fields and writes `hold` rules on a
6
+ * content type, so everything here reads those rules and nothing here knows the word "draft". The
7
+ * loader answers the same rules with the same code (`@jxsuite/schema/content-query`), which is what
8
+ * lets a badge here say what the build will actually do.
9
+ *
10
+ * It is deliberately its own module rather than a field on a pane's state and rather than a section
11
+ * of `entry-model.ts`. Two reasons, and the second is why the definitions live here beside the
12
+ * flag:
13
+ *
14
+ * 1. **The perspective is a fact about the AUTHOR, not the surface.** A filter that belonged to the
15
+ * Library would answer for the Library only, and the same author would then be shown a held
16
+ * entry in the collection grid, in the pages grid and in Quick Open a second later.
17
+ * 2. **Every list that must obey it has to be able to import it.** `entry-model.ts` asks
18
+ * `grid/sources/content-source.ts` where a collection's entries live, so the grid source cannot
19
+ * ask `entry-model.ts` back what a hold is: `import/no-cycle` is an error in this repo, and the
20
+ * cycle would be real.
21
+ *
22
+ * @docs studio/projects/content-types
23
+ */
24
+
25
+ import { compileHold } from "@jxsuite/schema/content-query";
26
+ import type { HoldPredicate } from "@jxsuite/schema/content-query";
27
+ import { isJsonObject, isString } from "@jxsuite/schema/guards";
28
+ import type { JsonValue } from "@jxsuite/schema/types";
29
+ import { reactive } from "../reactivity";
30
+ import type { ContentSectionEntry } from "../types";
31
+ import type { EntryFields } from "./entry-fields";
32
+
33
+ /** What this module reads off a content type's definition. */
34
+ export type HoldBearer = Pick<ContentSectionEntry, "hold">;
35
+
36
+ /** The word for an entry no rule holds, on a collection that has rules. */
37
+ export const NOT_HELD = "Published";
38
+
39
+ /** The rules a definition states, or none: a value that is not a list has no rules to read. */
40
+ function rulesOf(def: HoldBearer | null | undefined): JsonValue[] {
41
+ /* SAFETY: `hold` is read straight out of `project.json`, so whatever it holds is JSON. */
42
+ return Array.isArray(def?.hold) ? (def.hold as JsonValue[]) : [];
43
+ }
44
+
45
+ /** Each rules list's compiled answer, kept for as long as the list itself is. */
46
+ const compiled = new WeakMap<object, HoldPredicate>();
47
+
48
+ /** The compiled rules, answering nothing if they cannot compile: the build is who reports those. */
49
+ function predicateOf(def: HoldBearer | null | undefined): HoldPredicate {
50
+ const rules = rulesOf(def);
51
+ const known = compiled.get(rules);
52
+ if (known) {
53
+ return known;
54
+ }
55
+ let predicate: HoldPredicate;
56
+ try {
57
+ predicate = compileHold(rules);
58
+ } catch {
59
+ predicate = () => {};
60
+ }
61
+ compiled.set(rules, predicate);
62
+ return predicate;
63
+ }
64
+
65
+ /** Whether this collection has rules at all. With none, it has no state to show. */
66
+ export function hasHold(def: HoldBearer | null | undefined): boolean {
67
+ return rulesOf(def).length > 0;
68
+ }
69
+
70
+ /**
71
+ * The label of the rule holding this entry, or null. Takes the entry's FIELDS, whichever record
72
+ * they live in: `content/entry-fields.ts` answers that, because a JSON collection's entry has no
73
+ * frontmatter and its fields are the document.
74
+ */
75
+ export function holdOf(
76
+ def: HoldBearer | null | undefined,
77
+ fields: EntryFields | null | undefined,
78
+ ): string | null {
79
+ if (!hasHold(def)) {
80
+ return null;
81
+ }
82
+ /* SAFETY: an entry's fields are its parsed frontmatter or its JSON, which is what a query reads
83
+ from the loader too. */
84
+ return predicateOf(def)((fields ?? {}) as Parameters<HoldPredicate>[0]) ?? null;
85
+ }
86
+
87
+ /** What an entry's status reads: its rule's label, {@link NOT_HELD}, or "" with no rules. */
88
+ export function holdStatus(
89
+ def: HoldBearer | null | undefined,
90
+ fields: EntryFields | null | undefined,
91
+ ): string {
92
+ return hasHold(def) ? (holdOf(def, fields) ?? NOT_HELD) : "";
93
+ }
94
+
95
+ /** A rule an author can flip: one field, equal to `true`. */
96
+ export interface HoldSwitch {
97
+ field: string;
98
+ label: string;
99
+ }
100
+
101
+ /**
102
+ * The switch a collection's rules offer, or null. A rule on one boolean field is a thing an author
103
+ * can flip. A rule on a date or a list of values has no single other state to flip to, so it is
104
+ * shown as a status and set by editing its field.
105
+ */
106
+ export function holdSwitch(def: HoldBearer | null | undefined): HoldSwitch | null {
107
+ for (const rule of rulesOf(def)) {
108
+ if (isJsonObject(rule) && isString(rule.label) && isJsonObject(rule.when)) {
109
+ const fields = Object.entries(rule.when);
110
+ const [first] = fields;
111
+ if (fields.length === 1 && first?.[1] === true) {
112
+ return { field: first[0], label: rule.label };
113
+ }
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /** The fields a collection's rules read, which is what a surface must repaint on. */
120
+ export function holdFields(def: HoldBearer | null | undefined): string[] {
121
+ return rulesOf(def).flatMap((rule) =>
122
+ isJsonObject(rule) && isJsonObject(rule.when) ? Object.keys(rule.when) : [],
123
+ );
124
+ }
125
+
126
+ /** What being held means, in one sentence, for every surface that shows the state. */
127
+ export function holdMeaning(label: string | null): string {
128
+ return label === null
129
+ ? "No hold rule matches this entry, so a production build publishes it."
130
+ : `Held: ${label}. A production build leaves it out; jx dev, Preview and preview builds keep it.`;
131
+ }
132
+
133
+ // ─── The perspective ─────────────────────────────────────────────────────────
134
+
135
+ /** The perspective. `includeHeld` false, the default, hides entries a rule holds. */
136
+ export const holdView = reactive({ includeHeld: false });
137
+
138
+ /** Whether held entries are currently listed. */
139
+ export function includingHeld(): boolean {
140
+ return holdView.includeHeld;
141
+ }
142
+
143
+ /**
144
+ * Set the perspective.
145
+ *
146
+ * A setter, never a toggle: `content.setIncludeHeld` names the STATE it reaches, so a manifest
147
+ * step, an assistant call and a checkbox all land on the same value however many times they run
148
+ * (§13.5's idempotence rule, which `tests/app-commands.test.ts` enforces).
149
+ *
150
+ * Setting the flag is all this does. Repainting the lists that read it belongs to the command, in
151
+ * `content/entry-commands.ts`, because a data module may not reach into the workspace.
152
+ */
153
+ export function setIncludeHeld(include: boolean): void {
154
+ holdView.includeHeld = include;
155
+ }
156
+
157
+ /**
158
+ * Apply the perspective to a list.
159
+ *
160
+ * `includeHeld` true, or a collection with no rules, returns the input array unchanged (identity,
161
+ * not a copy), so a caller can cheaply tell that nothing was filtered.
162
+ */
163
+ export function applyHoldFilter<T>(
164
+ items: readonly T[],
165
+ def: HoldBearer | null | undefined,
166
+ fieldsOf: (item: T) => EntryFields | null | undefined,
167
+ includeHeld: boolean,
168
+ ): readonly T[] {
169
+ return includeHeld || !hasHold(def)
170
+ ? items
171
+ : items.filter((item) => holdOf(def, fieldsOf(item)) === null);
172
+ }
@@ -1,13 +1,13 @@
1
1
  /**
2
2
  * What the Content workspace's Pages list says about each page beyond its file: the template it is
3
- * drawn in, the collection a dynamic page expands over, and whether it is marked a draft
4
- * (`specs/studio.md` §22.5, the Content-Pages board: Title, URL, Template, Status, Edited).
3
+ * drawn in, the collection a dynamic page expands over (`specs/studio.md` §22.5, the Content-Pages
4
+ * board: Title, URL, Template, Status, Edited).
5
5
  *
6
- * The scan behind the Content home knows each page's FILE; its `$layout`, `$paths` and `draft` are
7
- * inside it. So this reads each page once, through the same parse the editor uses, and keeps the
8
- * answer until a file event drops it ({@link invalidatePageSummaries}, wired beside the
9
- * template-page index in `studio.ts`). The read never rejects: a page that cannot be read states
10
- * nothing, and is listed by its route alone.
6
+ * The scan behind the Content home knows each page's FILE; its `$layout` and `$paths` are inside
7
+ * it. So this reads each page once, through the same parse the editor uses, and keeps the answer
8
+ * until a file event drops it ({@link invalidatePageSummaries}, wired beside the template-page
9
+ * index in `studio.ts`). The read never rejects: a page that cannot be read states nothing, and is
10
+ * listed by its route alone.
11
11
  *
12
12
  * @docs studio/projects/browse
13
13
  */
@@ -15,7 +15,6 @@
15
15
  import { isNonNullObject } from "@jxsuite/schema/guards";
16
16
  import { getPlatform, hasPlatform } from "../platform";
17
17
  import { pageFactsOf } from "./collection-pages";
18
- import { DRAFT_FIELD } from "./draft-state";
19
18
  import type { FormatFrontmatter } from "../format/format-host";
20
19
  import type { JxMutableNode } from "@jxsuite/schema/types";
21
20
 
@@ -26,13 +25,11 @@ export interface PageSummary {
26
25
  layout: string | false | undefined;
27
26
  /** The collection its `$paths` expands over, or `null` for a page of its own. */
28
27
  contentType: string | null;
29
- /** Whether the page carries `draft: true`, the one draft marker a page can have. */
30
- draft: boolean;
31
28
  }
32
29
 
33
30
  /** What a page that could not be read states: nothing. */
34
31
  function unread(path: string): PageSummary {
35
- return { contentType: null, draft: false, layout: undefined, path };
32
+ return { contentType: null, layout: undefined, path };
36
33
  }
37
34
 
38
35
  /** The summary a parsed page states. Exported so the rule is assertable without a platform. */
@@ -42,8 +39,7 @@ export function pageSummaryOf(
42
39
  frontmatter: FormatFrontmatter | null,
43
40
  ): PageSummary {
44
41
  const { contentType, layout } = pageFactsOf(document, frontmatter);
45
- const draft = frontmatter?.[DRAFT_FIELD] === true || document?.[DRAFT_FIELD] === true;
46
- return { contentType, draft, layout, path };
42
+ return { contentType, layout, path };
47
43
  }
48
44
 
49
45
  /** Read one page. */
@@ -61,7 +57,7 @@ async function readSummary(path: string): Promise<PageSummary> {
61
57
  const { parseSourceForPath } = await import("../files/file-ops");
62
58
  const parsed = await parseSourceForPath(path, source);
63
59
  /* SAFETY: a format class's parser returns its file as a Jx element tree (the format contract);
64
- only `$layout`, `$paths` and `draft` are read off it. */
60
+ only `$layout` and `$paths` are read off it. */
65
61
  return pageSummaryOf(path, parsed.document as JxMutableNode, parsed.frontmatter);
66
62
  } catch {
67
63
  return unread(path);
@@ -234,7 +234,12 @@ export function setEditableVerdicts(verdicts: EditableVerdicts): void {
234
234
  * {@link file://./editable-tags.ts}.
235
235
  */
236
236
  export function isEditableBlock(el: HTMLElement) {
237
- return isEditableTag(el.tagName, editableVerdicts);
237
+ /* Closed explicitly, whatever its tag. Edit mode closes the label it draws for computed children
238
+ (`prepareForEditMode`), because activating that label commits its words as the element's
239
+ `textContent` when the caret leaves, and the binding is gone without a key being pressed. */
240
+ return (
241
+ el.getAttribute("contenteditable") !== "false" && isEditableTag(el.tagName, editableVerdicts)
242
+ );
238
243
  }
239
244
 
240
245
  /**