@panphora/bevel 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (539) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +226 -0
  3. package/bevel-fonts.css +16 -0
  4. package/bevel.css +3075 -0
  5. package/bevel.js +2126 -0
  6. package/dist/react/components/activity-feed.d.ts +89 -0
  7. package/dist/react/components/activity-feed.js +54 -0
  8. package/dist/react/components/alert-dialog.d.ts +14 -0
  9. package/dist/react/components/alert-dialog.js +19 -0
  10. package/dist/react/components/app-shell.d.ts +98 -0
  11. package/dist/react/components/app-shell.js +86 -0
  12. package/dist/react/components/app-toolbar.d.ts +19 -0
  13. package/dist/react/components/app-toolbar.js +22 -0
  14. package/dist/react/components/avatar-group.d.ts +67 -0
  15. package/dist/react/components/avatar-group.js +49 -0
  16. package/dist/react/components/badge-and-pill.d.ts +34 -0
  17. package/dist/react/components/badge-and-pill.js +12 -0
  18. package/dist/react/components/breadcrumbs.d.ts +56 -0
  19. package/dist/react/components/breadcrumbs.js +44 -0
  20. package/dist/react/components/bulk-bar.d.ts +22 -0
  21. package/dist/react/components/bulk-bar.js +59 -0
  22. package/dist/react/components/button.d.ts +15 -0
  23. package/dist/react/components/button.js +20 -0
  24. package/dist/react/components/callout-and-hint.d.ts +17 -0
  25. package/dist/react/components/callout-and-hint.js +8 -0
  26. package/dist/react/components/checkbox.d.ts +5 -0
  27. package/dist/react/components/checkbox.js +13 -0
  28. package/dist/react/components/chip.d.ts +149 -0
  29. package/dist/react/components/chip.js +29 -0
  30. package/dist/react/components/code-block.d.ts +50 -0
  31. package/dist/react/components/code-block.js +77 -0
  32. package/dist/react/components/code-editor.d.ts +122 -0
  33. package/dist/react/components/code-editor.js +695 -0
  34. package/dist/react/components/column-manager.d.ts +28 -0
  35. package/dist/react/components/column-manager.js +85 -0
  36. package/dist/react/components/combobox.d.ts +32 -0
  37. package/dist/react/components/combobox.js +37 -0
  38. package/dist/react/components/data-grid.d.ts +108 -0
  39. package/dist/react/components/data-grid.js +159 -0
  40. package/dist/react/components/data-table.d.ts +2 -0
  41. package/dist/react/components/data-table.js +6 -0
  42. package/dist/react/components/date.d.ts +23 -0
  43. package/dist/react/components/date.js +11 -0
  44. package/dist/react/components/details.d.ts +13 -0
  45. package/dist/react/components/details.js +20 -0
  46. package/dist/react/components/dialog.d.ts +74 -0
  47. package/dist/react/components/dialog.js +31 -0
  48. package/dist/react/components/diff-view.d.ts +73 -0
  49. package/dist/react/components/diff-view.js +68 -0
  50. package/dist/react/components/documents.d.ts +70 -0
  51. package/dist/react/components/documents.js +10 -0
  52. package/dist/react/components/drawer.d.ts +45 -0
  53. package/dist/react/components/drawer.js +35 -0
  54. package/dist/react/components/editable-grid-cells.d.ts +32 -0
  55. package/dist/react/components/editable-grid-cells.js +116 -0
  56. package/dist/react/components/eyebrow-kicker-lede-pull-quote.d.ts +39 -0
  57. package/dist/react/components/eyebrow-kicker-lede-pull-quote.js +12 -0
  58. package/dist/react/components/field.d.ts +32 -0
  59. package/dist/react/components/field.js +27 -0
  60. package/dist/react/components/file-tree.d.ts +75 -0
  61. package/dist/react/components/file-tree.js +247 -0
  62. package/dist/react/components/file.d.ts +43 -0
  63. package/dist/react/components/file.js +73 -0
  64. package/dist/react/components/files-table.d.ts +174 -0
  65. package/dist/react/components/files-table.js +237 -0
  66. package/dist/react/components/filter-builder.d.ts +37 -0
  67. package/dist/react/components/filter-builder.js +109 -0
  68. package/dist/react/components/form-error-summary.d.ts +16 -0
  69. package/dist/react/components/form-error-summary.js +49 -0
  70. package/dist/react/components/icon-button.d.ts +8 -0
  71. package/dist/react/components/icon-button.js +14 -0
  72. package/dist/react/components/icon.d.ts +7 -0
  73. package/dist/react/components/icon.js +15 -0
  74. package/dist/react/components/image.d.ts +72 -0
  75. package/dist/react/components/image.js +24 -0
  76. package/dist/react/components/inline-edit.d.ts +70 -0
  77. package/dist/react/components/inline-edit.js +149 -0
  78. package/dist/react/components/input.d.ts +29 -0
  79. package/dist/react/components/input.js +20 -0
  80. package/dist/react/components/kanban-board.d.ts +140 -0
  81. package/dist/react/components/kanban-board.js +374 -0
  82. package/dist/react/components/layout.d.ts +51 -0
  83. package/dist/react/components/layout.js +28 -0
  84. package/dist/react/components/list.d.ts +28 -0
  85. package/dist/react/components/list.js +6 -0
  86. package/dist/react/components/masthead-and-verdict.d.ts +60 -0
  87. package/dist/react/components/masthead-and-verdict.js +8 -0
  88. package/dist/react/components/menu.d.ts +26 -0
  89. package/dist/react/components/menu.js +31 -0
  90. package/dist/react/components/multi-select-picker.d.ts +49 -0
  91. package/dist/react/components/multi-select-picker.js +20 -0
  92. package/dist/react/components/note.d.ts +33 -0
  93. package/dist/react/components/note.js +8 -0
  94. package/dist/react/components/number-field.d.ts +16 -0
  95. package/dist/react/components/number-field.js +17 -0
  96. package/dist/react/components/pagination.d.ts +45 -0
  97. package/dist/react/components/pagination.js +87 -0
  98. package/dist/react/components/persistent-action-banner.d.ts +71 -0
  99. package/dist/react/components/persistent-action-banner.js +39 -0
  100. package/dist/react/components/popconfirm.d.ts +42 -0
  101. package/dist/react/components/popconfirm.js +59 -0
  102. package/dist/react/components/popover.d.ts +16 -0
  103. package/dist/react/components/popover.js +21 -0
  104. package/dist/react/components/progress-and-gauge.d.ts +44 -0
  105. package/dist/react/components/progress-and-gauge.js +19 -0
  106. package/dist/react/components/radio.d.ts +7 -0
  107. package/dist/react/components/radio.js +13 -0
  108. package/dist/react/components/rich-text-toolbar.d.ts +26 -0
  109. package/dist/react/components/rich-text-toolbar.js +64 -0
  110. package/dist/react/components/rows-and-card.d.ts +88 -0
  111. package/dist/react/components/rows-and-card.js +10 -0
  112. package/dist/react/components/save-status-indicator.d.ts +11 -0
  113. package/dist/react/components/save-status-indicator.js +33 -0
  114. package/dist/react/components/segmented-control.d.ts +21 -0
  115. package/dist/react/components/segmented-control.js +15 -0
  116. package/dist/react/components/select.d.ts +29 -0
  117. package/dist/react/components/select.js +29 -0
  118. package/dist/react/components/selectable-cards.d.ts +30 -0
  119. package/dist/react/components/selectable-cards.js +29 -0
  120. package/dist/react/components/settings-rows.d.ts +51 -0
  121. package/dist/react/components/settings-rows.js +8 -0
  122. package/dist/react/components/sidebar-nav.d.ts +77 -0
  123. package/dist/react/components/sidebar-nav.js +41 -0
  124. package/dist/react/components/skeleton-and-empty.d.ts +51 -0
  125. package/dist/react/components/skeleton-and-empty.js +14 -0
  126. package/dist/react/components/slider.d.ts +19 -0
  127. package/dist/react/components/slider.js +17 -0
  128. package/dist/react/components/small-charts.d.ts +60 -0
  129. package/dist/react/components/small-charts.js +76 -0
  130. package/dist/react/components/sortable.d.ts +30 -0
  131. package/dist/react/components/sortable.js +85 -0
  132. package/dist/react/components/split-button.d.ts +38 -0
  133. package/dist/react/components/split-button.js +43 -0
  134. package/dist/react/components/split-panes.d.ts +35 -0
  135. package/dist/react/components/split-panes.js +102 -0
  136. package/dist/react/components/stat-tiles.d.ts +54 -0
  137. package/dist/react/components/stat-tiles.js +8 -0
  138. package/dist/react/components/steps.d.ts +11 -0
  139. package/dist/react/components/steps.js +13 -0
  140. package/dist/react/components/switch.d.ts +6 -0
  141. package/dist/react/components/switch.js +13 -0
  142. package/dist/react/components/table.d.ts +61 -0
  143. package/dist/react/components/table.js +6 -0
  144. package/dist/react/components/tabs.d.ts +7 -0
  145. package/dist/react/components/tabs.js +13 -0
  146. package/dist/react/components/tags.d.ts +33 -0
  147. package/dist/react/components/tags.js +83 -0
  148. package/dist/react/components/textarea.d.ts +10 -0
  149. package/dist/react/components/textarea.js +6 -0
  150. package/dist/react/components/toast.d.ts +35 -0
  151. package/dist/react/components/toast.js +90 -0
  152. package/dist/react/components/tooltip-and-key.d.ts +20 -0
  153. package/dist/react/components/tooltip-and-key.js +20 -0
  154. package/dist/react/components/tree-view.d.ts +34 -0
  155. package/dist/react/components/tree-view.js +118 -0
  156. package/dist/react/components/upload-dropzone.d.ts +92 -0
  157. package/dist/react/components/upload-dropzone.js +116 -0
  158. package/dist/react/components/wizard-shell.d.ts +78 -0
  159. package/dist/react/components/wizard-shell.js +176 -0
  160. package/dist/react/fonts.css.d.ts +1 -0
  161. package/dist/react/index.d.ts +76 -0
  162. package/dist/react/index.js +77 -0
  163. package/dist/react/integrations/quickcrop.d.ts +58 -0
  164. package/dist/react/integrations/quickcrop.js +137 -0
  165. package/dist/react/integrations/richclay.d.ts +43 -0
  166. package/dist/react/integrations/richclay.js +129 -0
  167. package/dist/react/internal/classes.d.ts +3 -0
  168. package/dist/react/internal/classes.js +10 -0
  169. package/dist/react/internal/close-mark.d.ts +1 -0
  170. package/dist/react/internal/close-mark.js +10 -0
  171. package/dist/react/internal/controllable-state.d.ts +9 -0
  172. package/dist/react/internal/controllable-state.js +22 -0
  173. package/dist/react/internal/drop-effect.d.ts +28 -0
  174. package/dist/react/internal/drop-effect.js +8 -0
  175. package/dist/react/internal/form-reset.d.ts +2 -0
  176. package/dist/react/internal/form-reset.js +22 -0
  177. package/dist/react/internal/format.d.ts +1 -0
  178. package/dist/react/internal/format.js +14 -0
  179. package/dist/react/internal/link.d.ts +2 -0
  180. package/dist/react/internal/link.js +8 -0
  181. package/dist/react/internal/part.d.ts +20 -0
  182. package/dist/react/internal/part.js +47 -0
  183. package/dist/react/internal/pointer-session.d.ts +8 -0
  184. package/dist/react/internal/pointer-session.js +60 -0
  185. package/dist/react/internal/refs.d.ts +2 -0
  186. package/dist/react/internal/refs.js +22 -0
  187. package/dist/react/internal/rich-text.d.ts +28 -0
  188. package/dist/react/internal/rich-text.js +4 -0
  189. package/dist/react/internal/table-context.d.ts +39 -0
  190. package/dist/react/internal/table-context.js +12 -0
  191. package/dist/react/internal/use-object-url.d.ts +1 -0
  192. package/dist/react/internal/use-object-url.js +15 -0
  193. package/dist/react/layout.css +274 -0
  194. package/dist/react/layout.css.d.ts +1 -0
  195. package/dist/react/models/filter.d.ts +31 -0
  196. package/dist/react/models/filter.js +79 -0
  197. package/dist/react/models/kanban.d.ts +51 -0
  198. package/dist/react/models/kanban.js +35 -0
  199. package/dist/react/models/reorder.d.ts +9 -0
  200. package/dist/react/models/reorder.js +23 -0
  201. package/dist/react/models/table.d.ts +85 -0
  202. package/dist/react/models/table.js +144 -0
  203. package/dist/react/models/tree.d.ts +16 -0
  204. package/dist/react/models/tree.js +26 -0
  205. package/dist/react/provider.d.ts +18 -0
  206. package/dist/react/provider.js +31 -0
  207. package/dist/react/recipes/context-hypercms.d.ts +17 -0
  208. package/dist/react/recipes/context-hypercms.js +71 -0
  209. package/dist/react/recipes/context-quickcrop.d.ts +8 -0
  210. package/dist/react/recipes/context-quickcrop.js +49 -0
  211. package/dist/react/theme.css +8004 -0
  212. package/dist/react/theme.css.d.ts +1 -0
  213. package/examples/README.md +34 -0
  214. package/examples/components/activity-feed.html +28 -0
  215. package/examples/components/app-shell.html +12 -0
  216. package/examples/components/app-toolbar.html +38 -0
  217. package/examples/components/avatar-group.html +27 -0
  218. package/examples/components/breadcrumbs.html +26 -0
  219. package/examples/components/bulk-bar.html +22 -0
  220. package/examples/components/code-block.html +12 -0
  221. package/examples/components/code-editor.html +25 -0
  222. package/examples/components/column-manager.html +9 -0
  223. package/examples/components/combobox.html +13 -0
  224. package/examples/components/context-hypercms.css +41 -0
  225. package/examples/components/context-hypercms.html +40 -0
  226. package/examples/components/context-hypercms.js +62 -0
  227. package/examples/components/context-quickcrop.html +137 -0
  228. package/examples/components/diff-view.html +9 -0
  229. package/examples/components/drawer.html +11 -0
  230. package/examples/components/editable-grid-cells.html +9 -0
  231. package/examples/components/file-browser.html +53 -0
  232. package/examples/components/file-tree.html +34 -0
  233. package/examples/components/files-table.html +80 -0
  234. package/examples/components/filter-builder.html +11 -0
  235. package/examples/components/form-error-summary.html +11 -0
  236. package/examples/components/index.html +51 -0
  237. package/examples/components/inline-edit.html +42 -0
  238. package/examples/components/kanban-board.html +42 -0
  239. package/examples/components/manifest.json +3279 -0
  240. package/examples/components/modal.html +11 -0
  241. package/examples/components/multi-select-picker.html +13 -0
  242. package/examples/components/pagination.html +32 -0
  243. package/examples/components/persistent-action-banner.html +23 -0
  244. package/examples/components/popconfirm.html +13 -0
  245. package/examples/components/popover-menu.html +13 -0
  246. package/examples/components/reference/baseline/DECISIONS.md +85 -0
  247. package/examples/components/reference/baseline/README.md +58 -0
  248. package/examples/components/reference/baseline/bevel-fonts.css +16 -0
  249. package/examples/components/reference/baseline/bevel.css +1339 -0
  250. package/examples/components/reference/baseline/bevel.js +496 -0
  251. package/examples/components/reference/baseline/fonts/LICENSE-ibm-plex-mono.txt +93 -0
  252. package/examples/components/reference/baseline/fonts/LICENSE-ibm-plex-sans.txt +93 -0
  253. package/examples/components/reference/baseline/fonts/LICENSE-newsreader.txt +93 -0
  254. package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  255. package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  256. package/examples/components/reference/baseline/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  257. package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
  258. package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  259. package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  260. package/examples/components/reference/baseline/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  261. package/examples/components/reference/baseline/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  262. package/examples/components/reference/baseline/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  263. package/examples/components/reference/fable/IMPROVEMENTS.md +106 -0
  264. package/examples/components/reference/fable/PARENT-REPAIRS.md +17 -0
  265. package/examples/components/reference/fable/__pycache__/build_repairs.cpython-311.pyc +0 -0
  266. package/examples/components/reference/fable/__pycache__/kanban.cpython-311.pyc +0 -0
  267. package/examples/components/reference/fable/as-authored/IMPROVEMENTS-completed.md +106 -0
  268. package/examples/components/reference/fable/as-authored/base-astra.css +17 -0
  269. package/examples/components/reference/fable/as-authored/base-parent.css +416 -0
  270. package/examples/components/reference/fable/as-authored/build.py +858 -0
  271. package/examples/components/reference/fable/as-authored/fable.css +823 -0
  272. package/examples/components/reference/fable/as-authored/kanban-completed.css +235 -0
  273. package/examples/components/reference/fable/as-authored/kanban-completed.html +53 -0
  274. package/examples/components/reference/fable/as-authored/kanban-completed.py +212 -0
  275. package/examples/components/reference/fable/as-authored/kanban.py +207 -0
  276. package/examples/components/reference/fable/base-astra.css +17 -0
  277. package/examples/components/reference/fable/base-parent.css +416 -0
  278. package/examples/components/reference/fable/build.py +858 -0
  279. package/examples/components/reference/fable/build_repairs.py +25 -0
  280. package/examples/components/reference/fable/components/activity-feed.html +3 -0
  281. package/examples/components/reference/fable/components/app-toolbar.html +3 -0
  282. package/examples/components/reference/fable/components/avatar-group.html +4 -0
  283. package/examples/components/reference/fable/components/breadcrumbs.html +3 -0
  284. package/examples/components/reference/fable/components/code-block.html +9 -0
  285. package/examples/components/reference/fable/components/column-manager.html +3 -0
  286. package/examples/components/reference/fable/components/combobox.html +6 -0
  287. package/examples/components/reference/fable/components/context-hypercms.html +3 -0
  288. package/examples/components/reference/fable/components/context-quickcrop.html +4 -0
  289. package/examples/components/reference/fable/components/diff-view.html +13 -0
  290. package/examples/components/reference/fable/components/drawer.html +3 -0
  291. package/examples/components/reference/fable/components/editable-grid-cells.html +3 -0
  292. package/examples/components/reference/fable/components/filter-builder.html +3 -0
  293. package/examples/components/reference/fable/components/form-error-summary.html +4 -0
  294. package/examples/components/reference/fable/components/inline-edit.html +3 -0
  295. package/examples/components/reference/fable/components/kanban-board.html +53 -0
  296. package/examples/components/reference/fable/components/modal.html +3 -0
  297. package/examples/components/reference/fable/components/multi-select-picker.html +5 -0
  298. package/examples/components/reference/fable/components/pagination.html +3 -0
  299. package/examples/components/reference/fable/components/persistent-action-banner.html +4 -0
  300. package/examples/components/reference/fable/components/popconfirm.html +5 -0
  301. package/examples/components/reference/fable/components/popover-menu.html +3 -0
  302. package/examples/components/reference/fable/components/rich-text-toolbar.html +3 -0
  303. package/examples/components/reference/fable/components/save-status-indicator.html +2 -0
  304. package/examples/components/reference/fable/components/selectable-cards.html +3 -0
  305. package/examples/components/reference/fable/components/settings-rows.html +3 -0
  306. package/examples/components/reference/fable/components/sidebar-nav.html +4 -0
  307. package/examples/components/reference/fable/components/small-charts.html +5 -0
  308. package/examples/components/reference/fable/components/split-button.html +4 -0
  309. package/examples/components/reference/fable/components/split-panes.html +8 -0
  310. package/examples/components/reference/fable/components/stat-tiles.html +3 -0
  311. package/examples/components/reference/fable/components/toast-stack.html +5 -0
  312. package/examples/components/reference/fable/components/tree-view.html +4 -0
  313. package/examples/components/reference/fable/components/upload-dropzone.html +6 -0
  314. package/examples/components/reference/fable/components/wizard-shell.html +3 -0
  315. package/examples/components/reference/fable/coverage.json +414 -0
  316. package/examples/components/reference/fable/fable.css +823 -0
  317. package/examples/components/reference/fable/index.html +5 -0
  318. package/examples/components/reference/fable/kanban.css +237 -0
  319. package/examples/components/reference/fable/kanban.py +212 -0
  320. package/examples/components/reference/fable/parent-repairs.css +9 -0
  321. package/examples/components/reference/fixtures/data.json +102 -0
  322. package/examples/components/reference/fixtures/host.js +47 -0
  323. package/examples/components/reference/fixtures/static-host.js +39 -0
  324. package/examples/components/reference/fixtures/workspace.jpg +0 -0
  325. package/examples/components/reference/parent/CHANGES.md +179 -0
  326. package/examples/components/reference/parent/__pycache__/kanban.cpython-311.pyc +0 -0
  327. package/examples/components/reference/parent/base-astra.css +17 -0
  328. package/examples/components/reference/parent/base-parent.css +416 -0
  329. package/examples/components/reference/parent/build.py +148 -0
  330. package/examples/components/reference/parent/components/activity-feed.html +2 -0
  331. package/examples/components/reference/parent/components/app-toolbar.html +2 -0
  332. package/examples/components/reference/parent/components/avatar-group.html +4 -0
  333. package/examples/components/reference/parent/components/breadcrumbs.html +2 -0
  334. package/examples/components/reference/parent/components/code-block.html +8 -0
  335. package/examples/components/reference/parent/components/column-manager.html +2 -0
  336. package/examples/components/reference/parent/components/combobox.html +6 -0
  337. package/examples/components/reference/parent/components/context-hypercms.html +2 -0
  338. package/examples/components/reference/parent/components/context-quickcrop.html +4 -0
  339. package/examples/components/reference/parent/components/diff-view.html +4 -0
  340. package/examples/components/reference/parent/components/drawer.html +2 -0
  341. package/examples/components/reference/parent/components/editable-grid-cells.html +2 -0
  342. package/examples/components/reference/parent/components/filter-builder.html +2 -0
  343. package/examples/components/reference/parent/components/form-error-summary.html +2 -0
  344. package/examples/components/reference/parent/components/inline-edit.html +2 -0
  345. package/examples/components/reference/parent/components/kanban-board.html +84 -0
  346. package/examples/components/reference/parent/components/modal.html +2 -0
  347. package/examples/components/reference/parent/components/multi-select-picker.html +5 -0
  348. package/examples/components/reference/parent/components/pagination.html +2 -0
  349. package/examples/components/reference/parent/components/persistent-action-banner.html +4 -0
  350. package/examples/components/reference/parent/components/popconfirm.html +5 -0
  351. package/examples/components/reference/parent/components/popover-menu.html +2 -0
  352. package/examples/components/reference/parent/components/rich-text-toolbar.html +2 -0
  353. package/examples/components/reference/parent/components/save-status-indicator.html +2 -0
  354. package/examples/components/reference/parent/components/selectable-cards.html +2 -0
  355. package/examples/components/reference/parent/components/settings-rows.html +2 -0
  356. package/examples/components/reference/parent/components/sidebar-nav.html +4 -0
  357. package/examples/components/reference/parent/components/small-charts.html +2 -0
  358. package/examples/components/reference/parent/components/split-button.html +5 -0
  359. package/examples/components/reference/parent/components/split-panes.html +8 -0
  360. package/examples/components/reference/parent/components/stat-tiles.html +2 -0
  361. package/examples/components/reference/parent/components/toast-stack.html +5 -0
  362. package/examples/components/reference/parent/components/tree-view.html +4 -0
  363. package/examples/components/reference/parent/components/upload-dropzone.html +6 -0
  364. package/examples/components/reference/parent/components/wizard-shell.html +2 -0
  365. package/examples/components/reference/parent/coverage.json +414 -0
  366. package/examples/components/reference/parent/index.html +1 -0
  367. package/examples/components/reference/parent/kanban.css +114 -0
  368. package/examples/components/reference/parent/kanban.py +55 -0
  369. package/examples/components/reference/parent/refinement.css +53 -0
  370. package/examples/components/reference/selected/build.py +70 -0
  371. package/examples/components/reference/selected/components/activity-feed.html +3 -0
  372. package/examples/components/reference/selected/components/app-toolbar.html +3 -0
  373. package/examples/components/reference/selected/components/avatar-group.html +4 -0
  374. package/examples/components/reference/selected/components/breadcrumbs.html +3 -0
  375. package/examples/components/reference/selected/components/code-block.html +9 -0
  376. package/examples/components/reference/selected/components/column-manager.html +3 -0
  377. package/examples/components/reference/selected/components/combobox.html +6 -0
  378. package/examples/components/reference/selected/components/context-hypercms.html +3 -0
  379. package/examples/components/reference/selected/components/context-quickcrop.html +4 -0
  380. package/examples/components/reference/selected/components/diff-view.html +13 -0
  381. package/examples/components/reference/selected/components/drawer.html +3 -0
  382. package/examples/components/reference/selected/components/editable-grid-cells.html +3 -0
  383. package/examples/components/reference/selected/components/filter-builder.html +3 -0
  384. package/examples/components/reference/selected/components/form-error-summary.html +4 -0
  385. package/examples/components/reference/selected/components/inline-edit.html +3 -0
  386. package/examples/components/reference/selected/components/kanban-board.html +53 -0
  387. package/examples/components/reference/selected/components/modal.html +2 -0
  388. package/examples/components/reference/selected/components/multi-select-picker.html +5 -0
  389. package/examples/components/reference/selected/components/pagination.html +3 -0
  390. package/examples/components/reference/selected/components/persistent-action-banner.html +4 -0
  391. package/examples/components/reference/selected/components/popconfirm.html +5 -0
  392. package/examples/components/reference/selected/components/popover-menu.html +3 -0
  393. package/examples/components/reference/selected/components/rich-text-toolbar.html +3 -0
  394. package/examples/components/reference/selected/components/save-status-indicator.html +2 -0
  395. package/examples/components/reference/selected/components/selectable-cards.html +3 -0
  396. package/examples/components/reference/selected/components/settings-rows.html +3 -0
  397. package/examples/components/reference/selected/components/sidebar-nav.html +4 -0
  398. package/examples/components/reference/selected/components/small-charts.html +5 -0
  399. package/examples/components/reference/selected/components/split-button.html +4 -0
  400. package/examples/components/reference/selected/components/split-panes.html +8 -0
  401. package/examples/components/reference/selected/components/stat-tiles.html +3 -0
  402. package/examples/components/reference/selected/components/toast-stack.html +5 -0
  403. package/examples/components/reference/selected/components/tree-view.html +4 -0
  404. package/examples/components/reference/selected/components/upload-dropzone.html +6 -0
  405. package/examples/components/reference/selected/components/wizard-shell.html +3 -0
  406. package/examples/components/reference/selected/corrections.css +11 -0
  407. package/examples/components/reference/selected/coverage.json +413 -0
  408. package/examples/components/reference/selected/gallery-build.mjs +49 -0
  409. package/examples/components/reference/selected/gallery-spec.json +12 -0
  410. package/examples/components/reference/selected/index.html +1 -0
  411. package/examples/components/reference/selected/kanban-comparison.json +6 -0
  412. package/examples/components/reference/selected/manifest.json +476 -0
  413. package/examples/components/reference/sha256.json +142 -0
  414. package/examples/components/reference/sources/diagonal-close.svg +1 -0
  415. package/examples/components/reference/sources/hyperclay/public-assets/css/hyperclay-app-source.css +614 -0
  416. package/examples/components/reference/sources/hyperclay/public-assets/js/app.js +94 -0
  417. package/examples/components/reference/sources/hyperclayjs/src/ui/theModal.js +897 -0
  418. package/examples/components/reference/sources/hypercms/scripts/build-theme.js +203 -0
  419. package/examples/components/reference/sources/hypercms/src/anchor.js +24 -0
  420. package/examples/components/reference/sources/hypercms/src/apply-loop.js +167 -0
  421. package/examples/components/reference/sources/hypercms/src/diagnostics.js +44 -0
  422. package/examples/components/reference/sources/hypercms/src/enhance.js +173 -0
  423. package/examples/components/reference/sources/hypercms/src/events.js +955 -0
  424. package/examples/components/reference/sources/hypercms/src/form-builder.js +518 -0
  425. package/examples/components/reference/sources/hypercms/src/form-rules.js +208 -0
  426. package/examples/components/reference/sources/hypercms/src/hooks.js +100 -0
  427. package/examples/components/reference/sources/hypercms/src/hypercms-bundle.js +10 -0
  428. package/examples/components/reference/sources/hypercms/src/hypercms.js +347 -0
  429. package/examples/components/reference/sources/hypercms/src/morph.js +10 -0
  430. package/examples/components/reference/sources/hypercms/src/path.js +61 -0
  431. package/examples/components/reference/sources/hypercms/src/place.js +131 -0
  432. package/examples/components/reference/sources/hypercms/src/platform.js +131 -0
  433. package/examples/components/reference/sources/hypercms/src/refresh.js +87 -0
  434. package/examples/components/reference/sources/hypercms/src/richclay-bridge.js +140 -0
  435. package/examples/components/reference/sources/hypercms/src/row-identity.js +111 -0
  436. package/examples/components/reference/sources/hypercms/src/scaffold.js +10 -0
  437. package/examples/components/reference/sources/hypercms/src/session.js +303 -0
  438. package/examples/components/reference/sources/hypercms/src/shell.js +248 -0
  439. package/examples/components/reference/sources/hypercms/src/styles.css +298 -0
  440. package/examples/components/reference/sources/hypercms/src/targets.js +144 -0
  441. package/examples/components/reference/sources/hypercms/src/templates.js +656 -0
  442. package/examples/components/reference/sources/hypercms/src/theme/pixel-quiet.overrides.css +761 -0
  443. package/examples/components/reference/sources/hypercms/src/theme.generated.css +1772 -0
  444. package/examples/components/reference/sources/hypercms/src/toggle.js +666 -0
  445. package/examples/components/reference/sources/hypercms/src/unresolved.js +211 -0
  446. package/examples/components/reference/sources/hypercms/src/vendor/mirk.vendor.js +212 -0
  447. package/examples/components/reference/sources/hypercms/src/views/inline-layer.js +421 -0
  448. package/examples/components/reference/sources/hypercms/src/views/inline.js +1072 -0
  449. package/examples/components/reference/sources/hypercms/src/views/sidebar.js +97 -0
  450. package/examples/components/reference/sources/modal-reference.html +301 -0
  451. package/examples/components/reference/sources/quickcrop/quickcrop.css +124 -0
  452. package/examples/components/reference/sources/quickcrop/quickcrop.js +400 -0
  453. package/examples/components/reference/sources/richclay/richclay.css +416 -0
  454. package/examples/components/reference/sources/richclay/richclay.js +2962 -0
  455. package/examples/components/reference/sources/richclay/src/a11y.js +67 -0
  456. package/examples/components/reference/sources/richclay/src/browser-global.js +7 -0
  457. package/examples/components/reference/sources/richclay/src/buttons.js +254 -0
  458. package/examples/components/reference/sources/richclay/src/hyperclay.js +470 -0
  459. package/examples/components/reference/sources/richclay/src/normalize.js +380 -0
  460. package/examples/components/reference/sources/richclay/src/richclay.js +1356 -0
  461. package/examples/components/reference/sources/richclay/src/sanitize.js +189 -0
  462. package/examples/components/reference/sources/richclay/src/styles.js +23 -0
  463. package/examples/components/reference/sources/richclay/src/toolbar-float.js +175 -0
  464. package/examples/components/reference/sources/richclay/src/toolbar.js +394 -0
  465. package/examples/components/reference/sources/richclay/src/vendor-bundle.js +15 -0
  466. package/examples/components/reference/sources/richclay/vendor/DOMPURIFY-LICENSE +202 -0
  467. package/examples/components/reference/sources/richclay/vendor/DOMPURIFY-LICENSE-MPL +373 -0
  468. package/examples/components/reference/sources/richclay/vendor/SQUIRE-LICENSE +21 -0
  469. package/examples/components/reference/sources/richclay/vendor/purify.es.mjs +2249 -0
  470. package/examples/components/reference/sources/richclay/vendor/purify.min.js +3 -0
  471. package/examples/components/reference/sources/richclay/vendor/squire.js +22 -0
  472. package/examples/components/reference/sources/richclay/vendor/squire.mjs +22 -0
  473. package/examples/components/reference/sources/richclay/vendor/squire.provenance.json +19 -0
  474. package/examples/components/rich-text-toolbar.html +43 -0
  475. package/examples/components/save-status-indicator.html +11 -0
  476. package/examples/components/selectable-cards.html +9 -0
  477. package/examples/components/settings-rows.html +10 -0
  478. package/examples/components/sidebar-nav.html +9 -0
  479. package/examples/components/small-charts.html +21 -0
  480. package/examples/components/split-button.html +13 -0
  481. package/examples/components/split-panes-editor.html +6 -0
  482. package/examples/components/split-panes.html +9 -0
  483. package/examples/components/stat-tiles.html +23 -0
  484. package/examples/components/toast-stack.html +21 -0
  485. package/examples/components/tree-view.html +9 -0
  486. package/examples/components/upload-dropzone.html +88 -0
  487. package/examples/components/wizard-shell.html +50 -0
  488. package/examples/datastar/index.html +38 -0
  489. package/examples/datastar/server.mjs +31 -0
  490. package/examples/fixtures/field-guide.pdf +14 -0
  491. package/examples/fixtures/workspace.jpg +0 -0
  492. package/examples/setup-peers.mjs +35 -0
  493. package/fonts/LICENSE-ibm-plex-mono.txt +93 -0
  494. package/fonts/LICENSE-ibm-plex-sans.txt +93 -0
  495. package/fonts/LICENSE-newsreader.txt +93 -0
  496. package/fonts/ibm-plex-mono-latin-400-normal.woff2 +0 -0
  497. package/fonts/ibm-plex-mono-latin-500-normal.woff2 +0 -0
  498. package/fonts/ibm-plex-mono-latin-600-normal.woff2 +0 -0
  499. package/fonts/ibm-plex-sans-latin-400-italic.woff2 +0 -0
  500. package/fonts/ibm-plex-sans-latin-400-normal.woff2 +0 -0
  501. package/fonts/ibm-plex-sans-latin-500-normal.woff2 +0 -0
  502. package/fonts/ibm-plex-sans-latin-600-normal.woff2 +0 -0
  503. package/fonts/newsreader-latin-opsz-italic.woff2 +0 -0
  504. package/fonts/newsreader-latin-opsz-normal.woff2 +0 -0
  505. package/icons/LICENSE +21 -0
  506. package/icons/house.json +71 -0
  507. package/icons/icons.d.ts +17 -0
  508. package/icons/icons.js +648 -0
  509. package/icons/icons.svg +126 -0
  510. package/icons/marks/build-tray.source.mjs +45 -0
  511. package/icons/marks/hyperclay-local.svg +11 -0
  512. package/icons/phosphor.json +53 -0
  513. package/icons/round.json +8 -0
  514. package/integrations/datastar.js +202 -0
  515. package/integrations/quickcrop.css +26 -0
  516. package/integrations/quickcrop.js +56 -0
  517. package/integrations/richclay.css +100 -0
  518. package/integrations/richclay.js +131 -0
  519. package/package.json +630 -0
  520. package/react/README.md +244 -0
  521. package/src/README.md +83 -0
  522. package/vendor/datastar/datastar-1.0.3.js +10 -0
  523. package/vendor/datastar/provenance.json +6 -0
  524. package/vendor/floating-ui/LICENSE-core +20 -0
  525. package/vendor/floating-ui/LICENSE-dom +20 -0
  526. package/vendor/floating-ui/floating-ui.core.umd.min.js +1 -0
  527. package/vendor/floating-ui/floating-ui.dom.umd.min.js +1 -0
  528. package/vendor/floating-ui/provenance.json +17 -0
  529. package/vendor/richclay/LICENSE +16 -0
  530. package/vendor/richclay/THIRD-PARTY-NOTICES.md +45 -0
  531. package/vendor/richclay/provenance.json +17 -0
  532. package/vendor/richclay/richclay.css +404 -0
  533. package/vendor/richclay/richclay.js +2940 -0
  534. package/vendor/richclay/vendor/DOMPURIFY-LICENSE +202 -0
  535. package/vendor/richclay/vendor/DOMPURIFY-LICENSE-MPL +373 -0
  536. package/vendor/richclay/vendor/SQUIRE-LICENSE +21 -0
  537. package/vendor/richclay/vendor/purify.min.js +3 -0
  538. package/vendor/richclay/vendor/squire.js +22 -0
  539. package/vendor/richclay/vendor/squire.provenance.json +30 -0
@@ -0,0 +1,1072 @@
1
+ // The inline view: the editing session rendered onto the page itself rather than
2
+ // into a side panel.
3
+ //
4
+ // Three ways to change one field, chosen per target: text is edited in place
5
+ // through richclay bound to the page element, a native control on the page is
6
+ // left alone to be used, and everything else gets a handle that opens the
7
+ // popover holding that field's real form control.
8
+ //
9
+ // The form is NOT a second representation of the data. It is the same form the
10
+ // sidebar builds, mounted inside the inline host with every field hidden; the
11
+ // popover shows one field at a time by revealing it in place. Nothing is ever
12
+ // moved in or out of the form tree, so engine.extract(formRoot, formRules) can
13
+ // never be missing a path. That is the failure mode of the obvious alternative,
14
+ // relocating a field node into a popover and back.
15
+ //
16
+ // A text edit is the one path that does not write the page through the engine:
17
+ // richclay has already written it. What the commit does is mirror that value
18
+ // into the form leaf and apply as usual, which the engine's compare-before-write
19
+ // guard turns into a no-op on the element under the caret.
20
+
21
+ import { buildForm } from '../form-builder.js'
22
+ import {
23
+ bindEvents,
24
+ commit,
25
+ cssEscape,
26
+ extractFormData,
27
+ findLeafField,
28
+ onAdd,
29
+ onMove,
30
+ requestRemove,
31
+ suppressUndo,
32
+ writeFieldValue,
33
+ } from '../events.js'
34
+ import { autosizeTextarea, enhanceFields, upgradeInlineTextRules } from '../enhance.js'
35
+ import { applyUnresolvedState } from '../unresolved.js'
36
+ import { refreshForm } from '../refresh.js'
37
+ import { clearSessionOpen, markSessionOpen, reensureStyles } from '../shell.js'
38
+ import { isAnchorable } from '../anchor.js'
39
+ import { resolveTargets } from '../targets.js'
40
+ import { place } from '../place.js'
41
+ import { platform } from '../platform.js'
42
+ import {
43
+ BOUND_ATTR,
44
+ BOUND_ID_ATTR,
45
+ RC_OWNED_ATTR,
46
+ markBound,
47
+ registerBinding,
48
+ releaseBinding,
49
+ resolveRichClay,
50
+ stripOrphanEditorState,
51
+ } from '../richclay-bridge.js'
52
+ import { installSnapshotHook } from '../hooks.js'
53
+ import { createInlineLayer } from './inline-layer.js'
54
+
55
+ const HOST_TAG = 'hypercms-inline'
56
+
57
+ // The one revealed field, and the wrappers it hides behind. Both are cleared
58
+ // before every activation, so at most one path is ever showing.
59
+ const ACTIVE_CLASS = 'is-hcms-inline-active'
60
+ const ONPATH_CLASS = 'is-hcms-inline-onpath'
61
+
62
+ const FOCUSABLE =
63
+ 'input:not([disabled]):not([type="hidden"]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
64
+
65
+ // The same commands the sidebar's rich-text fields carry, so the two views
66
+ // offer the same formatting.
67
+ const TOOLBAR = ['bold', 'italic', 'link', 'undo', 'redo']
68
+
69
+ const HEADING = /^H[1-6]$/
70
+
71
+ let nextBoundId = 0
72
+
73
+ export function createInlineView({ doc, pageRoot, opts = {} }) {
74
+ const richText = opts.richText !== false
75
+ let host = null
76
+ let layer = null
77
+ let unbindPage = null
78
+ // Every richclay instance this view put on a PAGE element, keyed by that
79
+ // element. Bindings are made on the first click and live until the session
80
+ // ends, so a second click on a heading finds its editor rather than building
81
+ // another one.
82
+ const bindings = new Map()
83
+ // Which target the popover is open over, which handle opened it (so focus can
84
+ // go back there), and the placement mode of THIS open. The mode is reset on
85
+ // every activation: place()'s hysteresis is about one popover following its
86
+ // own anchor, and carrying a previous open's mode into a new one would bias
87
+ // the first placement toward wherever the last field happened to sit.
88
+ let activeTarget = null
89
+ let activeHandle = null
90
+ let popMode = null
91
+
92
+ // Build one binding: the editor, the marker, the snapshot hook, and the two
93
+ // listeners that turn typing into commits and blur into one undo primitive.
94
+ // Returns it, or null when the element cannot carry an editor. Shared by the
95
+ // first click and by the repair after a morph, so a rebound element comes back
96
+ // with everything the first bind gave it.
97
+ function bindText(ctx, el, path, prop, carry) {
98
+ const RichClay = resolveRichClay(doc.defaultView)
99
+ if (typeof RichClay !== 'function') return null
100
+
101
+ // A rebind inherits what the element looked like before richclay first
102
+ // touched it. Re-deriving these from the DOM reads the editor's own
103
+ // normalisation back as the author's markup, and the peer's morph as
104
+ // proof there was never an author's editor here.
105
+ const inherited = carry || {}
106
+ // Before richclay touches it. Squire normalises the markup it is handed:
107
+ // measured in Chrome, binding a heading rewrote
108
+ // <h1>A page that edits <em>itself</em></h1>
109
+ // into
110
+ // <h1><div>A page that edits <i>itself</i></div></h1>
111
+ // and that reaches the saved file. Someone who clicks a heading, reads it
112
+ // and presses Escape has changed nothing and must not have changed their
113
+ // document either, so an untouched session puts this back verbatim.
114
+ const originalHTML = inherited.originalHTML ?? el.innerHTML
115
+ const richClayIsOurs = inherited.richClayIsOurs ?? !el.hasAttribute('data-richclay')
116
+ // richclay's constructor returns the existing instance for an element that
117
+ // already has one (richclay.js:85-86), so on a page where the author mounted
118
+ // their own editor this is theirs, not ours. Adopt it for the session and
119
+ // never tear it down: destroying it would take away an editor hypercms did
120
+ // not create and cannot put back. data-richclay-active is richclay's own
121
+ // "there is a live editor here" flag, written by setupEditorAttributes.
122
+ const adopted = inherited.adopted ?? el.getAttribute('data-richclay-active') === 'true'
123
+ // The undo baseline, read before richclay touches the element for the same
124
+ // reason as originalHTML above: read after construct it holds Squire's
125
+ // normalisation, and undoing then writes <div> wrappers and <i> tags into
126
+ // the author's file. richclay stamps no-undo on what it activates, so from
127
+ // the bind until unbind this is the only record the page's undo stack gets.
128
+ // Never inherited: a rebind's baseline is the value on the page now.
129
+ const oldValue = readPageValue(el, prop)
130
+ const editor = construct(ctx, RichClay, el, prop, richClayIsOurs)
131
+ if (!editor) return null
132
+
133
+ // The rule's projection, not this element's children. A marker is only ever
134
+ // read back to answer "should the rule be rich", and for an array the rule
135
+ // is shared by every row: a plain row under a rich rule would otherwise
136
+ // report plain and downgrade the whole rule the moment the row that proved
137
+ // it rich is deleted. Recording the projection also states the frozen
138
+ // projection rule directly instead of approximating it.
139
+ markBound(el, prop === 'innerHTML', richClayIsOurs)
140
+ const boundId = String(++nextBoundId)
141
+ el.setAttribute(BOUND_ID_ATTR, boundId)
142
+ // The second trigger for the snapshot hook, and the reliable one: by the
143
+ // time anyone clicks a heading the host client is certainly loaded, even
144
+ // on a page that missed the readiness event at open(). Without the hook
145
+ // the editor's contenteditable reaches the saved file.
146
+ installSnapshotHook()
147
+
148
+ const binding = {
149
+ el,
150
+ editor,
151
+ path,
152
+ prop,
153
+ boundId,
154
+ originalHTML,
155
+ oldValue,
156
+ richClayIsOurs,
157
+ adopted,
158
+ // Set by squire's input event, which is the only signal that someone
159
+ // actually edited. Comparing the projected value cannot answer this:
160
+ // the normalisation above changes innerHTML at bind time, before anyone
161
+ // has typed anything.
162
+ dirty: false,
163
+ written: false,
164
+ // Whether the pre-bind markup may go back. Adopted means the editor is the
165
+ // author's and its normalisation is theirs to keep; dirty means they
166
+ // typed; written means a commit changed this path.
167
+ restorable() {
168
+ return !this.adopted && !this.dirty && !this.written
169
+ },
170
+ lastEdited: undefined,
171
+ }
172
+ bindings.set(el, binding)
173
+ registerBinding(boundId, binding)
174
+
175
+ // Toolbar commands mutate the DOM through squire without always firing a
176
+ // native input event, so squire's own signal is the one to commit on.
177
+ const squire = editor.squire
178
+ if (squire && typeof squire.addEventListener === 'function') {
179
+ const onInput = () => {
180
+ binding.dirty = true
181
+ // What this person produced, captured while the DOM still holds it.
182
+ // recordUndo reads this rather than the element, because at the one
183
+ // boundary that matters the element no longer holds their work.
184
+ binding.lastEdited = readPageValue(el, binding.prop)
185
+ acceptInlineTextChange(ctx, binding)
186
+ }
187
+ squire.addEventListener('input', onInput)
188
+ binding.detachInput = () => squire.removeEventListener?.('input', onInput)
189
+ }
190
+
191
+ // Blur closes an edit session: one undo primitive covering everything
192
+ // typed since the bind (or since the last blur).
193
+ const onBlur = () => recordUndo(binding)
194
+ el.addEventListener('blur', onBlur)
195
+ binding.detachBlur = () => el.removeEventListener('blur', onBlur)
196
+
197
+ return binding
198
+ }
199
+
200
+ return {
201
+ name: 'inline',
202
+ richText,
203
+ ctx: null,
204
+ root: null,
205
+ formRoot: null,
206
+ errorEl: null,
207
+ noticeEl: null,
208
+ countEl: null,
209
+ handoffEl: null,
210
+ handoffCountEl: null,
211
+
212
+ popEl: null,
213
+
214
+ // The form's own rich-text fields are never focused in this view — a rich
215
+ // target is edited on the page element itself — so mounting richclay on
216
+ // them buys nothing and costs five document-level capture listeners each.
217
+ enhanceFormRichText: false,
218
+
219
+ // The wider upgrade, not the sidebar's. Every text projection this view
220
+ // binds rich text to has to round-trip through @innerHTML, list rows
221
+ // included, and only this one looks inside an array.
222
+ prepareRules(sourceRules) {
223
+ return richText ? upgradeInlineTextRules(sourceRules, pageRoot) : sourceRules
224
+ },
225
+
226
+ bindText(el, path, prop, carry) { return bindText(this.ctx, el, path, prop, carry) },
227
+
228
+ mount(initialData) {
229
+ const ctx = this.ctx
230
+ host = mountInlineHost(doc, opts.theme)
231
+ this.root = host.root
232
+ this.formRoot = host.formRoot
233
+ this.errorEl = host.errorEl
234
+ this.noticeEl = host.noticeEl
235
+ this.countEl = host.countEl
236
+ this.handoffEl = host.handoffEl
237
+ this.handoffCountEl = host.handoffCountEl
238
+ this.popEl = host.popEl
239
+ // A body class is a page edit unless it is suppressed, the same reason
240
+ // the sidebar wraps its own mount.
241
+ suppressUndo(() => markSessionOpen(doc))
242
+
243
+ const fragment = buildForm({
244
+ pageRules: ctx.pageRules,
245
+ formRules: ctx.formRules,
246
+ data: initialData,
247
+ doc,
248
+ })
249
+ host.formRoot.appendChild(fragment)
250
+ ctx.seeder.seed(host.formRoot)
251
+ enhanceFields(host.formRoot, doc, this.enhanceFormRichText)
252
+ applyUnresolvedState(ctx)
253
+
254
+ bindEvents(ctx)
255
+
256
+ layer = createInlineLayer({
257
+ doc,
258
+ layerEl: host.layerEl,
259
+ // The handle and the page click are two doors to one room.
260
+ onActivate: (target, handle) => this.activate(target, handle),
261
+ onListAction: (op) => this.listAction(op),
262
+ })
263
+ layer.setFollower(() => this.placePopover())
264
+ host.toggleEl.addEventListener('click', (event) => {
265
+ event.preventDefault()
266
+ event.stopPropagation()
267
+ this.toggleControls()
268
+ })
269
+ // There is no drawer. What a field with no visual representation gets is
270
+ // the count above and this one tap out to the view that edits everything.
271
+ // The switch carries the session's options across (hypercms.js open()), so
272
+ // the sidebar comes up on the same page root with the same callbacks.
273
+ host.handoffEl.querySelector('[data-hcms-open-view]').addEventListener('click', (event) => {
274
+ event.preventDefault()
275
+ event.stopPropagation()
276
+ ctx.onViewRequested?.('sidebar')
277
+ })
278
+ this.bindPage()
279
+ this.syncTargets()
280
+ },
281
+
282
+ // Add, remove and reorder, every one of them acting on the FORM row and
283
+ // letting the commit move the page.
284
+ //
285
+ // Moving the page row here instead would corrupt the rollback:
286
+ // captureChildren runs INSIDE applyWithRollback (apply-loop.js:39), after
287
+ // the caller has already mutated, so a failed apply would restore the
288
+ // post-move DOM and the reorder would never come undone. Moving the form row
289
+ // first means the commit snapshots the page container still in its pre-move
290
+ // state. Routing through requestRemove rather than onRemove is the same kind
291
+ // of inheritance: the consent modal an object array raises is that path's,
292
+ // and reimplementing it here would be a second policy to keep in step.
293
+ listAction({ action, list, index, row }) {
294
+ const ctx = this.ctx
295
+ const arrayPath = list.path.join('.')
296
+ const arrayEl = ctx.formRoot.querySelector(`[data-hcms-path="${cssEscape(arrayPath)}"]`)
297
+ if (!arrayEl) return
298
+ if (action === 'add') {
299
+ onAdd(arrayPath, ctx)
300
+ return
301
+ }
302
+ // The strip was stamped with the index its row held when the layer was
303
+ // last built, and a refresh trails a structural change by an observer
304
+ // batch. Re-resolve from the row element so a second click inside that
305
+ // window acts on the row under the pointer rather than on whatever has
306
+ // since taken its number.
307
+ const rowIndex = row ? liveRowIndex(ctx, arrayPath, row) : index
308
+ // A row that is no longer in the list was removed by the click before this
309
+ // one, and its strip is still on screen because the refresh trails the
310
+ // change. There is nothing to act on, and the number it was stamped with
311
+ // now belongs to the row that moved up into its place, so doing nothing is
312
+ // the only correct outcome.
313
+ if (rowIndex === -1) return
314
+ const formRow = formRowAt(arrayEl, rowIndex)
315
+ if (!formRow) return
316
+ if (action === 'remove') {
317
+ requestRemove(formRow, ctx)
318
+ // The commit has already moved the page and the form. Reconcile now
319
+ // rather than on the debounced refresh: until this runs, every binding
320
+ // below the change still holds the numeric path it had before it, and a
321
+ // keystroke inside that window commits into the wrong row.
322
+ this.syncTargets()
323
+ return
324
+ }
325
+ onMove(formRow, action === 'move-up' ? -1 : 1, ctx)
326
+ this.syncTargets()
327
+ },
328
+
329
+ toggleControls() {
330
+ if (!layer || !host) return
331
+ const hidden = !layer.controlsHidden
332
+ layer.setControlsHidden(hidden)
333
+ host.toggleEl.setAttribute('aria-pressed', String(hidden))
334
+ const label = host.toggleEl.querySelector('.mirk-button__label')
335
+ if (label) label.textContent = hidden ? 'Show controls' : 'Hide controls'
336
+ },
337
+
338
+ // Discovery and activation, delegated on the page root rather than bound
339
+ // per target: the target set is rebuilt on every refresh, and per-element
340
+ // listeners would have to be torn down and reattached with it.
341
+ bindPage() {
342
+ const root = this.ctx.pageRoot
343
+ const hostRoot = host.root
344
+
345
+ const onPointerOver = (event) => {
346
+ const target = layer && layer.elementToTarget(event.target)
347
+ if (target) layer.showHighlight(target.el)
348
+ else layer?.hideHighlight()
349
+ // One pointer path, two questions. The second: a list row too small to
350
+ // hold its own strip shows it only while the pointer is on that row,
351
+ // and the raw element is what answers that — the strip is drawn over
352
+ // its row and is not a target, so a resolved target cannot say so.
353
+ layer?.setHoveredRow(event.target)
354
+ }
355
+ const onPointerLeave = () => {
356
+ layer?.hideHighlight()
357
+ layer?.setHoveredRow(null)
358
+ }
359
+
360
+ // Bind on the press, not on the click. The browser places the caret as the
361
+ // default action of mousedown, and it can only do that if the element is
362
+ // already editable by then. Binding on click is one step too late, which is
363
+ // why the caret used to land at the start of the text however far into it
364
+ // the person pressed. Nothing here focuses: the browser's own default
365
+ // action does that, and it is the only thing that knows the press
366
+ // coordinates.
367
+ const onPointerDown = (event) => {
368
+ if (hostRoot.contains(event.target)) return
369
+ const target = layer && layer.elementToTarget(event.target)
370
+ if (!target || target.kind !== 'text') return
371
+ if (bindings.has(target.el)) return
372
+ bindText(this.ctx, target.el, target.path.join('.'), projectionOf(target))
373
+ }
374
+
375
+ const onClick = (event) => {
376
+ // The editor's own chrome. A handle already activates through its own
377
+ // listener, and a click in the popover must not close it.
378
+ if (hostRoot.contains(event.target)) return
379
+ const target = layer && layer.elementToTarget(event.target)
380
+ if (!target) {
381
+ this.deactivate()
382
+ return
383
+ }
384
+ // A text target is already bound and focused by the pointerdown above,
385
+ // so this only has to leave its default alone. A link inside one is the
386
+ // exception: while the session is open, following it would navigate away
387
+ // mid-edit.
388
+ const inLink = typeof event.target.closest === 'function' && event.target.closest('a[href]')
389
+ if (inLink || target.kind !== 'text') event.preventDefault()
390
+ this.activate(target)
391
+ }
392
+
393
+ const onKeyDown = (event) => {
394
+ if (event.key === 'Escape') this.deactivate()
395
+ }
396
+
397
+ // A commit naming this element's path means something wrote content to it,
398
+ // and the pre-bind markup must never go back over that. richclay's
399
+ // rewriting commits nothing, which is precisely what makes this the right
400
+ // signal and the text comparison it replaces the wrong one: that could not
401
+ // tell an API write that kept the same words from the editor's own work.
402
+ //
403
+ // On the document because the event is dispatched on the host, which does
404
+ // not sit inside the page root. detail.pageRoot is what the payload
405
+ // carries it for.
406
+ const onChanged = (event) => {
407
+ const detail = event.detail
408
+ if (!detail || detail.pageRoot !== this.ctx.pageRoot || !detail.path) return
409
+ for (const binding of bindings.values()) {
410
+ if (binding.path === detail.path) binding.written = true
411
+ }
412
+ }
413
+
414
+ root.addEventListener('pointerover', onPointerOver)
415
+ root.addEventListener('pointerleave', onPointerLeave)
416
+ root.addEventListener('pointerdown', onPointerDown)
417
+ root.addEventListener('click', onClick)
418
+ hostRoot.addEventListener('keydown', onKeyDown)
419
+ doc.addEventListener('hcms:change', onChanged)
420
+
421
+ unbindPage = () => {
422
+ root.removeEventListener('pointerover', onPointerOver)
423
+ root.removeEventListener('pointerleave', onPointerLeave)
424
+ root.removeEventListener('pointerdown', onPointerDown)
425
+ root.removeEventListener('click', onClick)
426
+ hostRoot.removeEventListener('keydown', onKeyDown)
427
+ doc.removeEventListener('hcms:change', onChanged)
428
+ }
429
+ },
430
+
431
+ // Reveal the form leaf for this target, over the target. Nothing moves in
432
+ // or out of the form tree — the leaf is shown where it already lives — so
433
+ // engine.extract(formRoot) can never be missing a path.
434
+ activate(target, handle) {
435
+ if (!target || !host) return
436
+ // A text target is edited where it sits, and only falls through to the
437
+ // popover when it cannot be: no richclay on the page, or a root richclay
438
+ // refuses. Without the fall-through, clicking such a target would do
439
+ // nothing at all.
440
+ if (target.kind === 'text' && this.activateText(target)) return
441
+
442
+ const leaf = revealPath(this, host, target.path.join('.'))
443
+ if (!leaf) {
444
+ this.deactivate()
445
+ return
446
+ }
447
+
448
+ activeTarget = target
449
+ activeHandle = handle || null
450
+ popMode = null
451
+
452
+ // Unhide BEFORE measuring. A hidden popover has a zero rect, which places
453
+ // it at the clamp instead of beside its anchor — the same trap as the 0px
454
+ // textarea below, which was sized while the popover had no height.
455
+ host.popEl.hidden = false
456
+ autosizeIn(leaf)
457
+ this.placePopover()
458
+ focusFirst(leaf)
459
+ },
460
+
461
+ // Bind richclay to the page element and edit the text where it sits.
462
+ // Returns false when the target cannot be bound, which is the caller's
463
+ // signal to open the popover over it instead.
464
+ //
465
+ // On demand, never at mount: an instance per text target on a page with
466
+ // twenty of them is twenty Squire editors, twenty toolbars and a hundred
467
+ // document-level listeners for one heading someone might click.
468
+ activateText(target) {
469
+ const el = target.el
470
+ const existing = bindings.get(el)
471
+ if (existing) {
472
+ focusEditor(existing)
473
+ return true
474
+ }
475
+
476
+ const binding = bindText(this.ctx, el, target.path.join('.'), projectionOf(target))
477
+ if (!binding) return false
478
+
479
+ focusEditor(binding)
480
+ return true
481
+ },
482
+
483
+ placePopover() {
484
+ if (!host || !activeTarget || host.popEl.hidden) return
485
+ const win = doc.defaultView
486
+ if (!win) return
487
+ const viewport = { width: win.innerWidth, height: win.innerHeight }
488
+ const anchor = activeTarget.el.getBoundingClientRect()
489
+ const bar = host.popEl.getBoundingClientRect()
490
+ const { mode, x, y } = place({ anchor, bar, viewport, current: popMode })
491
+ popMode = mode
492
+ host.popEl.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`
493
+ },
494
+
495
+ deactivate() {
496
+ if (!host || (!activeTarget && host.popEl.hidden)) return
497
+ host.popEl.hidden = true
498
+ clearPathClasses(host.root)
499
+ const handle = activeHandle
500
+ activeTarget = null
501
+ activeHandle = null
502
+ popMode = null
503
+ if (handle && doc.contains(handle) && typeof handle.focus === 'function') {
504
+ try { handle.focus({ preventScroll: true }) } catch (_) { handle.focus() }
505
+ }
506
+ },
507
+
508
+ // Re-resolve every editable thing on the page and hand the current set to
509
+ // the layer. Runs on mount and on every refresh, because a refresh can add
510
+ // or remove rows and can rebind the rules (refresh.js re-runs prepareRules).
511
+ syncTargets() {
512
+ if (!layer) return
513
+ const ctx = this.ctx
514
+ const { targets, lists } = resolveTargets(ctx.pageRoot, ctx.pageRules)
515
+ layer.setTargets(targets, lists)
516
+ const byEl = reconcileBindings(this, bindings, targets, doc)
517
+ // An open popover holds its path the same way a binding does. Re-point it
518
+ // at the target that now stands for the element it is anchored over, or
519
+ // the popover keeps editing the row that inherited its old index.
520
+ if (activeTarget) {
521
+ const fresh = byEl.get(activeTarget.el)
522
+ // Gone from the rules, so the popover would keep writing its old path
523
+ // from an element nothing reads. Close it rather than leave it editing a
524
+ // field that is not there any more.
525
+ if (fresh) activeTarget = fresh
526
+ else this.deactivate()
527
+ }
528
+ sweepOrphanEditors(this.ctx, bindings, doc)
529
+ const n = layer.count
530
+ if (this.countEl) {
531
+ this.countEl.textContent = `${n} editable ${n === 1 ? 'area' : 'areas'}`
532
+ this.countEl.hidden = n === 0
533
+ }
534
+ // Every ruled field with no anchorable element, whatever its kind: a
535
+ // permanently hidden metadata span and a target inside a closed tab are
536
+ // byte-identical to the browser, so this is a live count and not a
537
+ // classification. Read off the same floor the handles clear rather than a
538
+ // second rule that could disagree with it.
539
+ const away = targets.reduce((total, target) => total + (isAnchorable(target.el) ? 0 : 1), 0)
540
+ if (this.handoffEl) {
541
+ this.handoffCountEl.textContent = away === 0
542
+ ? ''
543
+ : `${away} ${away === 1 ? "field isn't" : "fields aren't"} visible right now.`
544
+ // At zero the bar says nothing at all, rather than "0 fields".
545
+ this.handoffEl.hidden = away === 0
546
+ }
547
+ },
548
+
549
+ // `changes` is the mutation batch behind an 'observer' refresh. The inline
550
+ // view will need it to recognise its own edits; nothing reads it yet, and a
551
+ // view must treat an absent batch as "refresh everything".
552
+ refresh(reason, changes) {
553
+ if (reason === 'livesync') {
554
+ // The stylesheet lives in <head> and the session class on <body>, both
555
+ // outside this host, so a full-document morph can strip either. The page
556
+ // shift is still the sidebar's alone: this view adds no geometry class.
557
+ reensureStyles(doc)
558
+ markSessionOpen(doc)
559
+ refreshForm(this.ctx, { ignoreActiveValue: true })
560
+ } else if (reason === 'undo') {
561
+ refreshForm(this.ctx, { ignoreActiveValue: false })
562
+ } else {
563
+ refreshForm(this.ctx)
564
+ }
565
+ this.syncTargets()
566
+ // Both reasons put a morph over the page, and a morph takes the editor's
567
+ // attributes off every element this view has bound.
568
+ if (reason === 'livesync' || reason === 'undo') this.rebindText(reason)
569
+ this.restoreActive()
570
+ },
571
+
572
+ // A live-sync morph re-syncs the page against the incoming copy, and this
573
+ // view's own snapshot hook is what makes that copy clean: contenteditable,
574
+ // data-richclay, no-undo and the bound marker all come back off an element
575
+ // a live editor is still pointing at. An undo replaying those same
576
+ // attribute writes in reverse does the same thing. Repair the pairing after
577
+ // the morph, for the reason restoreActive repairs the popover after it.
578
+ rebindText(reason) {
579
+ // On the undo path the value did not change because anyone typed: the undo
580
+ // itself reverted it. Recording that would push a primitive describing the
581
+ // undo's own effect, and a fresh record clears the redo stack, so the
582
+ // person who just pressed undo could not press redo. Measured against the
583
+ // recorder, which fires its handlers outside its own pause.
584
+ const record = reason !== 'undo'
585
+ for (const [el, binding] of [...bindings]) {
586
+ // Replaced outright: no key matched, so the incoming element took this
587
+ // one's place. What they typed before the sync is already committed and
588
+ // still belongs on the undo stack, but the node it was typed into is off
589
+ // the page — nothing is put back onto it, and the incoming markup stands.
590
+ if (!doc.contains(el)) {
591
+ unbind(this.ctx, binding, { restore: false, record })
592
+ bindings.delete(el)
593
+ continue
594
+ }
595
+ if (el.hasAttribute(BOUND_ATTR)) {
596
+ const current = readPageValue(el, binding.prop)
597
+ // An undo or a sync that did not reach this element leaves its value
598
+ // exactly as the person typed it, and their edit is still pending.
599
+ // Only an element the change actually moved needs a fresh baseline,
600
+ // and only then is what they typed gone with it.
601
+ if (current !== binding.lastEdited) {
602
+ // Survived with the marker intact, so the editor is still live and
603
+ // still correct; only the value moved under it. Its baseline is now
604
+ // the value the morph reverted away from, and left stale the next
605
+ // blur records that revert as a fresh edit — which clears the redo
606
+ // stack, so the person who pressed undo cannot press redo.
607
+ binding.oldValue = current
608
+ // The value they had typed is gone with the undo, so it must not
609
+ // survive as a pending edit: left set, the next blur would record it
610
+ // against the new baseline and clear the redo stack, which is the
611
+ // defect the line above exists to prevent, arriving one step later.
612
+ binding.lastEdited = undefined
613
+ }
614
+ continue
615
+ }
616
+ // Survived, but the page no longer treats it as ours. Close the session
617
+ // that was open, one primitive for what they typed, and open a fresh one
618
+ // on the markup as it now stands. Deliberately no restore: the element
619
+ // holds the incoming copy's markup, and putting a pre-morph snapshot
620
+ // back over it would undo somebody else's edit.
621
+ const hadFocus = doc.activeElement === el
622
+ unbind(this.ctx, binding, { restore: false, record })
623
+ bindings.delete(el)
624
+ const rebound = bindText(this.ctx, el, binding.path, binding.prop, {
625
+ richClayIsOurs: binding.richClayIsOurs,
626
+ adopted: binding.adopted,
627
+ })
628
+ // Only the element they were actually in: a sync that arrives while
629
+ // someone is typing must not pull the caret into a different one.
630
+ if (rebound && hadFocus) focusEditor(rebound)
631
+ }
632
+ },
633
+
634
+ // morphForm re-syncs every field from a freshly built form, and that form
635
+ // carries no reveal classes, so an open popover goes blank while still
636
+ // sitting there open. Measured in a browser: the leaf keeps its identity
637
+ // and loses only its class, which is why nothing here rebuilds anything.
638
+ // The same reason enhanceFields and applyErrorState re-run after the morph
639
+ // in refresh.js.
640
+ restoreActive() {
641
+ if (!host || !activeTarget || host.popEl.hidden) return
642
+ // The refresh can take the page element out from under an open popover:
643
+ // a live-sync that deletes the row being edited, say. Its rect would then
644
+ // measure zero and the popover would place against the viewport clamp.
645
+ if (!doc.contains(activeTarget.el)) {
646
+ this.deactivate()
647
+ return
648
+ }
649
+ const leaf = revealPath(this, host, activeTarget.path.join('.'))
650
+ if (!leaf) {
651
+ this.deactivate()
652
+ return
653
+ }
654
+ // After the classes, never before: a textarea measured while its wrapper
655
+ // is still display:none reports a zero scrollHeight and sizes to nothing.
656
+ autosizeIn(leaf)
657
+ this.placePopover()
658
+ // Deliberately no focusFirst: a refresh can land mid-typing, and moving
659
+ // the caret back to the top of the field on every page mutation would
660
+ // make the field unusable.
661
+ },
662
+
663
+ // Deliberately NOT a page field. Focusing one would scroll the page and
664
+ // begin an edit nobody asked for. Focusing the host itself puts the next Tab
665
+ // into the session bar without moving the page or starting anything.
666
+ focusOnOpen() {
667
+ if (this.root && typeof this.root.focus === 'function') {
668
+ try { this.root.focus({ preventScroll: true }) } catch (_) { this.root.focus() }
669
+ }
670
+ },
671
+
672
+ destroy() {
673
+ // Dropped before the close, not restored to: the handle is about to be
674
+ // removed with the layer, and focusing it on the way out would fight the
675
+ // session's own restore of whatever had focus before it opened.
676
+ activeHandle = null
677
+ this.deactivate()
678
+ for (const binding of bindings.values()) unbind(this.ctx, binding)
679
+ bindings.clear()
680
+ unbindPage?.()
681
+ unbindPage = null
682
+ layer?.destroy()
683
+ layer = null
684
+ host?.destroy()
685
+ host = null
686
+ clearSessionOpen(doc)
687
+ this.popEl = null
688
+ this.handoffEl = null
689
+ this.handoffCountEl = null
690
+ },
691
+ }
692
+ }
693
+
694
+ // Build the editor, or answer null when this element cannot carry one.
695
+ //
696
+ // The pause is around the construction and nothing else, and it is not
697
+ // belt-and-braces: richclay writes contenteditable, data-richclay and a class
698
+ // onto an authored page element, and those three DO reach the mutation hub
699
+ // (measured, plan §6). Un-paused they read as a page edit and drive a full form
700
+ // refresh; unsuppressed they land on the page's undo stack as three attribute
701
+ // writes nobody made.
702
+ function construct(ctx, RichClay, el, prop, richClayIsOurs) {
703
+ const handle = ctx && ctx.observerHandle
704
+ handle?.pause()
705
+ try {
706
+ return suppressUndo(() => {
707
+ let editor = null
708
+ try {
709
+ editor = new RichClay(el, {
710
+ inline: true,
711
+ hyperclay: false,
712
+ // Formatting is offered only where the rule can keep it. A textContent
713
+ // projection commits the element's text, so a bold applied through this
714
+ // toolbar would be flattened by the very next commit — the button would
715
+ // be promising something the rule cannot hold. That is every scalar
716
+ // array row (enhance.js never upgrades a "[]" rule) and everything at
717
+ // all under richText: false. richclay reads false as "no toolbar"
718
+ // (resolveToolbarControls, richclay.js:1007).
719
+ toolbar: prop === 'innerHTML' ? TOOLBAR : false,
720
+ // A heading is one line by definition; without this, Enter in it
721
+ // creates a second block inside the <h1>.
722
+ ...(HEADING.test(el.tagName) ? { singleLine: true } : null),
723
+ })
724
+ } catch (err) {
725
+ console.warn('[hypercms] richclay activation failed; the field falls back to the popover', err)
726
+ return null
727
+ }
728
+ // richclay's own verdict, which it delivers by returning an instance that
729
+ // never activates: TABLE and its row parts, SCRIPT/STYLE/TEXTAREA/TITLE/
730
+ // IFRAME/NOSCRIPT/XMP, TEMPLATE, and anything outside the HTML namespace.
731
+ // Without this check the person clicks one of those and nothing happens.
732
+ if (editor.unsupported || !editor.active) {
733
+ // Only tear down an instance this call is responsible for. On an
734
+ // element the author opted in on, richclay owns the mount and the
735
+ // instance is theirs; destroying it takes away an editor hypercms
736
+ // did not create and cannot put back.
737
+ if (richClayIsOurs) { try { editor.destroy() } catch (_) {} }
738
+ return null
739
+ }
740
+ // An editor can be active while its element is inert. A live-sync morph
741
+ // strips contenteditable and the runtime attributes off the element and
742
+ // leaves the instance believing it never stopped, so on the rebind that
743
+ // follows, this call is the only thing that makes the element editable
744
+ // again. reattach answers for itself whether anything needs doing, so a
745
+ // healthy editor costs one comparison. Absent on an older richclay, where
746
+ // the element stays inert and there is nothing hypercms can do about it.
747
+ if (typeof editor.reattach === 'function') editor.reattach()
748
+ return editor
749
+ })
750
+ } finally {
751
+ handle?.resume()
752
+ }
753
+ }
754
+
755
+ // Mirror the page element into the form leaf, then commit like any other edit.
756
+ //
757
+ // Per change, not on blur. commit IS the notification — it dispatches
758
+ // hcms:change and calls onChange — so a blur-only commit would lose the edit's
759
+ // own event to whatever unrelated commit folded the text in first, and then be
760
+ // skipped on its fingerprint. The form leaf has to be written first or the
761
+ // commit extracts the stale one and puts the old text back on the page.
762
+ function acceptInlineTextChange(ctx, { path, el, prop }) {
763
+ const field = findLeafField(ctx.formRoot, path)
764
+ if (!field) return
765
+ writeFieldValue(field, readPageValue(el, prop), ctx.formRoot, path)
766
+ commit(extractFormData(ctx), { path, structural: false }, ctx)
767
+ }
768
+
769
+ // What the engine will read back out of this element. The DOM adapter trims a
770
+ // text projection (hyper-html-api dom.js text()), so a raw textContent read puts
771
+ // a value in the form that the page can never extract, the mirror disagrees on
772
+ // every commit, and the engine rewrites the text node the caret is sitting in.
773
+ function readPageValue(el, prop) {
774
+ return prop === 'innerHTML' ? el.innerHTML : (el.textContent || '').trim()
775
+ }
776
+
777
+ // One primitive per edit session, at the boundary that closes it. Everything in
778
+ // between is invisible to the page's undo stack: richclay stamps no-undo on
779
+ // what it activates and hyper-undo honours it.
780
+ //
781
+ // Unlike resolveUnobservedProjection this can record an array row, because the
782
+ // element is in hand rather than resolved from a document-level selector.
783
+ function recordUndo(binding) {
784
+ const { el, prop, oldValue, lastEdited } = binding
785
+ // Deliberately not the element. A live-sync morph replaces the content before
786
+ // this boundary is reached, so reading the DOM here records a peer's edit
787
+ // under this person's name and undo reverts THEIR change. No local edit means
788
+ // no primitive at all, which is the same fix from the other side: an untouched
789
+ // binding that a morph wrote through must not put anything on the stack.
790
+ const newValue = lastEdited
791
+ if (newValue === undefined || newValue === oldValue) return
792
+ binding.oldValue = newValue
793
+ const u = platform('undo')
794
+ if (!u || typeof u.recordValue !== 'function') return
795
+ // hyper-undo drops recordValue while its recorder is paused (scope.js:190),
796
+ // and both boundaries that close a binding sit inside a pause somebody else
797
+ // opened: teardownSession suppresses around destroy (session.js:225), and
798
+ // clayjs dispatches clay:sync-applied inside its mutation pause, which pauses
799
+ // undo too (live-sync.js:1357, mutation.js:132). The values are read here and
800
+ // the record is posted after those finally-blocks have run, so the edit keeps
801
+ // its place on the stack instead of vanishing at the moment it ends.
802
+ //
803
+ // A record against an element the morph took off the page would be an entry
804
+ // that replays into nothing and still costs an undo press, so it is skipped.
805
+ if (u.isPaused) queueMicrotask(() => { if (el.isConnected) u.recordValue(el, { prop, oldValue, newValue }) })
806
+ else u.recordValue(el, { prop, oldValue, newValue })
807
+ }
808
+
809
+ // Close one binding. The undo record comes first: recordValue is a no-op while
810
+ // the recorder is paused, and destroy has to run inside a pause.
811
+ //
812
+ // `restore` is what separates ending a session from repairing one. Ending it
813
+ // puts the author's markup back when nobody typed; repairing after a morph must
814
+ // not, because the markup on the element is the incoming copy's and a pre-morph
815
+ // snapshot written back over it would undo somebody else's edit.
816
+ function unbind(ctx, binding, { restore = true, record = true } = {}) {
817
+ if (record) recordUndo(binding)
818
+ binding.detachInput?.()
819
+ binding.detachBlur?.()
820
+ const handle = ctx && ctx.observerHandle
821
+ handle?.pause()
822
+ try {
823
+ suppressUndo(() => {
824
+ if (!binding.adopted) {
825
+ try { binding.editor.destroy() } catch (err) {
826
+ console.warn('[hypercms] richclay teardown failed; editor state may reach the save', err)
827
+ }
828
+ // destroy() takes data-richclay off only on richclay 0.5.0, which tracks
829
+ // who wrote it. Neither vendored 0.4.0 copy removes that attribute
830
+ // anywhere, so without this the mount selector stays on the live element
831
+ // and the next save carries it into the author's file, exactly what the
832
+ // clone-side removal exists to prevent. Never on an element the author
833
+ // opted in on: there the attribute is theirs.
834
+ if (binding.richClayIsOurs) binding.el.removeAttribute('data-richclay')
835
+ }
836
+ // The bridge only unmarks the snapshot clone. Left on the live element,
837
+ // the marker would tell the next snapshot to strip an element hypercms no
838
+ // longer owns, and the provenance flag beside it would reach the file.
839
+ binding.el.removeAttribute(BOUND_ATTR)
840
+ binding.el.removeAttribute(RC_OWNED_ATTR)
841
+ binding.el.removeAttribute(BOUND_ID_ATTR)
842
+ releaseBinding(binding.boundId)
843
+ // Nobody typed, so the only difference between this element and the one
844
+ // the author wrote is the editor's own normalisation. Put their markup
845
+ // back. Inside the pause and the undo suppression with the teardown, so
846
+ // the restore is not itself an edit.
847
+ if (restore && binding.restorable()) {
848
+ binding.el.innerHTML = binding.originalHTML
849
+ }
850
+ })
851
+ } finally {
852
+ handle?.resume()
853
+ }
854
+ }
855
+
856
+ // The click that opens a text target lands before the element is editable, so
857
+ // the browser has put no caret in it. richclay's focus() goes through squire,
858
+ // which puts the caret in the text rather than selecting the whole region.
859
+ function focusEditor(binding) {
860
+ const { editor, el } = binding
861
+ if (el.ownerDocument.activeElement === el) return
862
+ try {
863
+ if (typeof editor.focus === 'function') editor.focus()
864
+ else el.focus()
865
+ } catch (_) {}
866
+ }
867
+
868
+ // resolveTargets re-walks the page on every refresh, so it is the only authority
869
+ // on which path reaches which element right now. Both a binding and an open
870
+ // popover cache a path from the moment they were made: a move or a remove
871
+ // renumbers every row after it, and without this, typing into the row you just
872
+ // moved up writes into the row that took its old index.
873
+ //
874
+ // The projection is re-read for the same reason. refreshForm re-runs
875
+ // prepareRules every refresh, so a rule upgrades to @innerHTML the moment its
876
+ // element gains markup. A binding frozen on textContent would flatten every tag
877
+ // in that element on the next keystroke, including the node under the caret.
878
+ function reconcileBindings(view, bindings, targets, doc) {
879
+ const byEl = new Map()
880
+ for (const target of targets) byEl.set(target.el, target)
881
+
882
+ for (const [el, binding] of [...bindings]) {
883
+ const target = byEl.get(el)
884
+ if (!target) {
885
+ // The rules no longer name this element. Left bound it stays editable with
886
+ // nowhere of its own to write, and every keystroke commits the old path,
887
+ // so the text lands in whatever element the rule now points at. End the
888
+ // session on it instead. restore is on because nobody has taken the
889
+ // element away: if it was never typed into, the author's markup goes back.
890
+ unbind(view.ctx, binding, { restore: true })
891
+ bindings.delete(el)
892
+ continue
893
+ }
894
+ binding.path = target.path.join('.')
895
+ const prop = projectionOf(target)
896
+ if (prop === binding.prop) continue
897
+ // The projection changed under a live editor. Rebuilding it is the only way
898
+ // the toolbar, the commit and the undo baseline end up describing the same
899
+ // half of the element.
900
+ const hadFocus = doc.activeElement === el
901
+ unbind(view.ctx, binding, { restore: false })
902
+ bindings.delete(el)
903
+ const rebound = view.bindText(el, binding.path, prop, {
904
+ richClayIsOurs: binding.richClayIsOurs,
905
+ adopted: binding.adopted,
906
+ originalHTML: binding.originalHTML,
907
+ })
908
+ if (rebound && hadFocus) focusEditor(rebound)
909
+ }
910
+
911
+ return byEl
912
+ }
913
+
914
+ // A text target is either a bare rule (textContent) or one prepareRules rebound
915
+ // to @innerHTML. Nothing else is ever text.
916
+ function projectionOf(target) {
917
+ return target.attr === 'innerHTML' ? 'innerHTML' : 'textContent'
918
+ }
919
+
920
+ // Runs on every sync, because a structural apply is exactly when the engine
921
+ // clones, and a refresh is what follows one.
922
+ function sweepOrphanEditors(ctx, bindings, doc) {
923
+ for (const el of ctx.pageRoot.querySelectorAll(`[${BOUND_ATTR}]`)) {
924
+ if (bindings.has(el)) continue
925
+ const handle = ctx.observerHandle
926
+ handle?.pause()
927
+ try {
928
+ suppressUndo(() => stripOrphanEditorState(el, doc.defaultView))
929
+ } finally {
930
+ handle?.resume()
931
+ }
932
+ }
933
+ }
934
+
935
+ // Show exactly one leaf: mark it active, and mark every wrapper between it and
936
+ // the form root, each of which the one-field-at-a-time rule would otherwise
937
+ // leave hidden around it. Returns the leaf, or null when the path is not in the
938
+ // form. The walk stops AT the form root and never above it: <body> and <html>
939
+ // are the author's elements, they reach the saved file, and clearPathClasses
940
+ // only queries downward from the host so a class left up there never comes off.
941
+ function revealPath(view, host, path) {
942
+ const leaf = view.formRoot &&
943
+ view.formRoot.querySelector(`[data-hcms-path="${cssEscape(path)}"]`)
944
+ clearPathClasses(host.root)
945
+ if (!leaf) return null
946
+ leaf.classList.add(ACTIVE_CLASS)
947
+ for (let el = leaf.parentElement; el; el = el.parentElement) {
948
+ el.classList.add(ONPATH_CLASS)
949
+ if (el === view.formRoot) break
950
+ }
951
+ return leaf
952
+ }
953
+
954
+ function clearPathClasses(root) {
955
+ if (!root) return
956
+ for (const el of root.querySelectorAll(`.${ACTIVE_CLASS}, .${ONPATH_CLASS}`)) {
957
+ el.classList.remove(ACTIVE_CLASS, ONPATH_CLASS)
958
+ }
959
+ }
960
+
961
+ // Every textarea in the revealed leaf was measured while the popover was
962
+ // hidden, so it is sitting at height: 0 until it is sized again.
963
+ function autosizeIn(leaf) {
964
+ if (leaf.tagName === 'TEXTAREA') autosizeTextarea(leaf)
965
+ leaf.querySelectorAll?.('textarea').forEach(autosizeTextarea)
966
+ }
967
+
968
+ function focusFirst(leaf) {
969
+ const el = leaf.matches?.(FOCUSABLE) ? leaf : leaf.querySelector?.(FOCUSABLE)
970
+ if (!el || typeof el.focus !== 'function') return
971
+ try { el.focus({ preventScroll: true }) } catch (_) { el.focus() }
972
+ }
973
+
974
+ function mountInlineHost(doc, theme) {
975
+ // reensureStyles is idempotent: it returns immediately when the stylesheet is
976
+ // already in the document and installs it when it is not, so it doubles as the
977
+ // "ensure" the sidebar's private ensureStyles provides.
978
+ reensureStyles(doc)
979
+
980
+ const root = doc.createElement(HOST_TAG)
981
+
982
+ // Same theme contract as the sidebar: pixel-quiet is the baked-in look and an
983
+ // optional theme pins light/dark. hcms-shell is what the tokens and every
984
+ // mirk widget rule are scoped to, so without it the popover's fields render
985
+ // as browser defaults; hcms-inline is what keeps the docked-panel geometry
986
+ // off, since that is now scoped to .hcms-panel.
987
+ const themeClass = theme === 'dark' ? ' dark' : theme === 'light' ? ' light' : ''
988
+ root.className = 'hcms-shell pixel-quiet hcms-inline' + themeClass
989
+
990
+ // The engine skip selector. Every read and write in hypercms passes
991
+ // { skip: '[data-hcms-shell]' }, so this one attribute is what keeps the
992
+ // engine from walking into the form and treating the CMS's own fields as page
993
+ // content. Reused rather than renamed: it is already honored in seven places
994
+ // across six files, and each of them covers this host for free.
995
+ root.setAttribute('data-hcms-shell', '')
996
+
997
+ // Both spellings of "never persist this", because the two clients each have a
998
+ // path that knows only one of them. clayjs strips [no-save] and [save-remove]
999
+ // alike (region-policy.js:82), but hyperclayjs's AI-edit clone path queries
1000
+ // [save-remove] exclusively (ai-edit.js:151, :249), so the modern spelling on
1001
+ // its own would leave the whole editor sitting in that clone.
1002
+ root.setAttribute('no-save', '')
1003
+ root.setAttribute('save-remove', '')
1004
+ // Out of every snapshot, not just the save: without this, live sync ships the
1005
+ // editor to everyone else's browser.
1006
+ root.setAttribute('snapshot-remove', '')
1007
+ // The CMS's own chrome must never wake autosave, undo or dirty tracking.
1008
+ // no-watch implies all three (region-policy.js:184-185), which is why
1009
+ // save-ignore and no-trigger-autosave are NOT here: they would be redundant,
1010
+ // and a redundant attribute is one a later reader mistakes for load-bearing.
1011
+ root.setAttribute('no-watch', '')
1012
+
1013
+ // Focusable for focusOnOpen, but NOT role="dialog" / aria-modal. The sidebar
1014
+ // is modal — it traps focus and locks the page. This one is the opposite: the
1015
+ // page stays fully usable while it is open, which is the entire point.
1016
+ root.setAttribute('tabindex', '-1')
1017
+
1018
+ root.innerHTML = `
1019
+ <div class="hcms-inline-bar">
1020
+ <div class="hcms-inline-count" hidden></div>
1021
+ <div class="hcms-inline-handoff" hidden>
1022
+ <span class="hcms-inline-handoff-count"></span>
1023
+ <button type="button" class="hcms-inline-handoff-open mirk-button mirk-button--small" data-hcms-open-view="sidebar">
1024
+ <span class="mirk-button__label">Edit in the sidebar</span>
1025
+ </button>
1026
+ </div>
1027
+ <button type="button" class="hcms-inline-toggle mirk-button mirk-button--small" data-hcms-controls-toggle aria-pressed="false">
1028
+ <span class="mirk-button__label">Hide controls</span>
1029
+ </button>
1030
+ <div class="hcms-inline-notice" role="status" hidden></div>
1031
+ <div class="hcms-inline-error" role="alert" hidden></div>
1032
+ </div>
1033
+ <div class="hcms-inline-layer"></div>
1034
+ <div class="hcms-inline-pop" hidden><div data-hcms-form-root class="hcms-form"></div></div>
1035
+ `
1036
+
1037
+ doc.body.appendChild(root)
1038
+
1039
+ return {
1040
+ root,
1041
+ formRoot: root.querySelector('[data-hcms-form-root]'),
1042
+ noticeEl: root.querySelector('.hcms-inline-notice'),
1043
+ errorEl: root.querySelector('.hcms-inline-error'),
1044
+ countEl: root.querySelector('.hcms-inline-count'),
1045
+ handoffEl: root.querySelector('.hcms-inline-handoff'),
1046
+ handoffCountEl: root.querySelector('.hcms-inline-handoff-count'),
1047
+ layerEl: root.querySelector('.hcms-inline-layer'),
1048
+ popEl: root.querySelector('.hcms-inline-pop'),
1049
+ toggleEl: root.querySelector('[data-hcms-controls-toggle]'),
1050
+ destroy() { root.remove() },
1051
+ }
1052
+ }
1053
+
1054
+ // The form row a page row stands for. onMove and requestRemove both take a form
1055
+ // item element and find their array back from it, so this resolves exactly what
1056
+ // they expect to be handed: the array's own items slot, indexed. Deliberately
1057
+ // not a path query — the row's data-hcms-path is restamped by every structural
1058
+ // operation, and the slot's child order is the thing those two read.
1059
+ function formRowAt(arrayEl, index) {
1060
+ const slot = arrayEl.querySelector('.hcms-array-items')
1061
+ if (!slot) return null
1062
+ const rows = slot.querySelectorAll(':scope > [data-hcms-card], :scope > [data-hcms-array-item]')
1063
+ return rows[index] || null
1064
+ }
1065
+
1066
+ // Where this page row sits in its list right now, or -1 when the row is no
1067
+ // longer in it (a remove that already landed).
1068
+ function liveRowIndex(ctx, arrayPath, row) {
1069
+ const { lists } = resolveTargets(ctx.pageRoot, ctx.pageRules)
1070
+ const list = lists.find((candidate) => candidate.path.join('.') === arrayPath)
1071
+ return list ? list.items.indexOf(row) : -1
1072
+ }