@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,1356 @@
1
+ import {
2
+ createDefaultRegistry,
3
+ isApplePlatform,
4
+ presets
5
+ } from "./buttons.js";
6
+ import {
7
+ captureRange,
8
+ caretEdge,
9
+ editorRootNeedsNormalization,
10
+ ejectsBlocks,
11
+ flattenBlocks,
12
+ hasBlockDescendant,
13
+ isCaretHost,
14
+ isInlineTag,
15
+ isUnsupportedRootTag,
16
+ keepsTextShape,
17
+ nodeLength,
18
+ normalizeEditorRoot,
19
+ restoreRange,
20
+ unsupportedRootReason
21
+ } from "./normalize.js";
22
+ import { Toolbar } from "./toolbar.js";
23
+ import { FloatingToolbar } from "./toolbar-float.js";
24
+ import {
25
+ createSanitizer,
26
+ flattenFragmentToSingleLine,
27
+ inlineSanitizeConfig,
28
+ normalizeUrl,
29
+ sanitizeHTML as sanitizeHTMLString,
30
+ sanitizeElement as sanitizeElementInPlace
31
+ } from "./sanitize.js";
32
+ import {
33
+ RICHCLAY_SELECTOR,
34
+ isRichClayHost,
35
+ consumeInertContenteditable,
36
+ installHyperclayBridge,
37
+ isHyperclayEditMode,
38
+ markChrome,
39
+ parseEditableOptions,
40
+ removeRuntimeState,
41
+ shouldActivateEditor,
42
+ shouldUseHyperclay,
43
+ stripRichClayFromClone
44
+ } from "./hyperclay.js";
45
+ import { ensureStyles } from "./styles.js";
46
+ import {
47
+ announce,
48
+ cloneRange,
49
+ connectDescription,
50
+ createLiveRegion,
51
+ getSquireSelection,
52
+ restoreSquireSelection,
53
+ setRuntimeAttribute
54
+ } from "./a11y.js";
55
+
56
+ const instances = new WeakMap();
57
+ const autoInitWindows = new WeakSet();
58
+ const watchedWindows = new WeakSet();
59
+ const globalRegistry = createDefaultRegistry();
60
+ const KEEP_FOCUS = Symbol("richclay-keep-focus");
61
+ const ROOT_TRANSFER_EVENTS = ["cut", "paste", "drop"];
62
+ const MODIFIER_ORDER = ["Alt", "Ctrl", "Meta", "Shift"];
63
+ let dialogSeq = 0;
64
+
65
+ const defaultOptions = {
66
+ toolbar: "standard",
67
+ toolbarContainer: null,
68
+ sanitize: {},
69
+ placeholder: "",
70
+ readOnly: false,
71
+ hyperclay: "auto",
72
+ onChange: null,
73
+ Squire: null,
74
+ inline: false,
75
+ singleLine: false,
76
+ toolbarOnSelect: false
77
+ };
78
+
79
+ export default class RichClay {
80
+ static presets = presets;
81
+ static selector = RICHCLAY_SELECTOR;
82
+
83
+ constructor(element, options = {}) {
84
+ if (!element) throw new Error("RichClay requires an element.");
85
+ const existing = instances.get(element);
86
+ if (existing) return existing;
87
+
88
+ this.element = element;
89
+ this.unsupported = conflictsWithExistingEditor(element) || refuseUnsupportedRoot(element);
90
+ const derived = { ...parseEditableOptions(element), ...options };
91
+ this.options = { ...defaultOptions, ...derived };
92
+ if (this.options.singleLine && !("toolbar" in derived)) {
93
+ this.options.toolbar = "inline";
94
+ }
95
+ // Squire's _ensureBottomLine() appends a fresh default block whenever the
96
+ // root's last element child differs from blockTag, and it runs from every
97
+ // Backspace and Delete. With "P", any inline region ending in an <h2>, a
98
+ // <ul>, or the repair's own wrapper gained a permanent empty margined
99
+ // paragraph on the first delete. DIV also matches what Squire's fixContainer
100
+ // produces and carries no margins, which is what inline mode needs.
101
+ this._blockTag = this.options.inline ? "DIV" : "P";
102
+ this.registry = new Map(globalRegistry);
103
+ this.toolbar = null;
104
+ this.float = null;
105
+ this._onToolbarKey = null;
106
+ this._onFloatFocusOut = null;
107
+ this._onDocPointerDown = null;
108
+ this.liveRegion = null;
109
+ this.description = null;
110
+ this.dialog = null;
111
+ this.savedSelection = null;
112
+ this.path = "";
113
+ this.active = false;
114
+ this._squire = null;
115
+ this._squireListeners = [];
116
+ this._onBeforeInput = null;
117
+ this._onRootBeforeInput = null;
118
+ this._onRootKeydown = null;
119
+ this._onRootTransfer = null;
120
+ this._shortcutKeys = [];
121
+ this._appleDeleteKeys = new Set();
122
+ this._authoredPres = new WeakSet();
123
+ this._warnedInlineBlock = false;
124
+ this._onFocus = () => {
125
+ this.element.classList.add("richclay-focused");
126
+ if (this.options.inline) this.ensureFloatingToolbar();
127
+ };
128
+ this._onBlur = event => {
129
+ this.element.classList.remove("richclay-focused");
130
+ if (this.options.inline) this.scheduleFloatTeardown(event);
131
+ };
132
+
133
+ this.ensureMarker();
134
+ this.hyperclay = shouldUseHyperclay(this.options, this.window);
135
+ if (this.hyperclay) installHyperclayBridge(this.window);
136
+
137
+ instances.set(element, this);
138
+
139
+ this.sanitizeConfig = this.options.inline
140
+ ? inlineSanitizeConfig(this.options.sanitize)
141
+ : this.options.sanitize;
142
+ this.sanitizer = createSanitizer(this.sanitizeConfig, this.element.ownerDocument);
143
+ if (!this.options.inline) {
144
+ // Card editors sanitize their region up front. Inline editors must not:
145
+ // the content is the page's own live DOM, and rewriting it on activation
146
+ // would destroy author markup (classes, images, data attributes).
147
+ sanitizeElementInPlace(this.element, this.sanitizeConfig);
148
+ }
149
+
150
+ if (shouldActivateEditor(this.options, this.window)) {
151
+ this.activate();
152
+ }
153
+ }
154
+
155
+ get squire() {
156
+ return this._squire;
157
+ }
158
+
159
+ get window() {
160
+ return this.element.ownerDocument.defaultView || window;
161
+ }
162
+
163
+ static init(selector = RICHCLAY_SELECTOR, options = {}) {
164
+ // A caller who names its own hosts means them, custom elements included; the
165
+ // custom-element guard only applies to the default catch-all selector.
166
+ const guard = selector === RICHCLAY_SELECTOR ? isRichClayHost : () => true;
167
+ const elements = resolveElements(selector)
168
+ .filter(guard)
169
+ .filter(element => !conflictsWithExistingEditor(element));
170
+ return elements.map(element => new RichClay(element, options));
171
+ }
172
+
173
+ static autoInit(win = typeof window !== "undefined" ? window : undefined) {
174
+ if (!win || !win.document || autoInitWindows.has(win)) return;
175
+ autoInitWindows.add(win);
176
+ const run = () => {
177
+ if (shouldUseHyperclay({}, win) && isHyperclayEditMode(win)) {
178
+ RichClay.init();
179
+ RichClay.watch(win);
180
+ }
181
+ };
182
+ if (win.document.readyState === "loading") {
183
+ win.document.addEventListener("DOMContentLoaded", run, { once: true });
184
+ } else {
185
+ run();
186
+ }
187
+ }
188
+
189
+ static watch(win = typeof window !== "undefined" ? window : undefined, options = {}) {
190
+ if (!win || !win.document || watchedWindows.has(win)) return;
191
+ watchedWindows.add(win);
192
+
193
+ const mount = element => {
194
+ if (!isMountable(element)) return;
195
+ if (!instances.has(element) && !conflictsWithExistingEditor(element)) new RichClay(element, options);
196
+ };
197
+ const unmount = element => instances.get(element)?.destroy();
198
+
199
+ const observer = new win.MutationObserver(records => {
200
+ records.forEach(record => {
201
+ if (record.type === "attributes") {
202
+ const target = record.target;
203
+ // Matching the selector is not enough to keep an editor alive. A custom
204
+ // element carrying a bare `editable` matches it and is still refused by
205
+ // the host guard, so deciding by the selector alone left such an element
206
+ // mounted forever with its runtime chrome saved into the file.
207
+ if (isMountable(target)) mount(target);
208
+ else unmount(target);
209
+ return;
210
+ }
211
+ record.addedNodes.forEach(node => {
212
+ if (node.nodeType !== 1) return;
213
+ if (node.matches?.(RICHCLAY_SELECTOR)) mount(node);
214
+ node.querySelectorAll?.(RICHCLAY_SELECTOR).forEach(mount);
215
+ });
216
+ record.removedNodes.forEach(node => {
217
+ if (node.nodeType !== 1) return;
218
+ // A node that is still connected was moved, not removed; keep its
219
+ // editor alive.
220
+ const teardown = el => {
221
+ if (!el.isConnected) instances.get(el)?.destroy();
222
+ };
223
+ if (node.matches?.(RICHCLAY_SELECTOR)) teardown(node);
224
+ node.querySelectorAll?.(RICHCLAY_SELECTOR).forEach(teardown);
225
+ });
226
+ });
227
+ });
228
+
229
+ observer.observe(win.document.documentElement || win.document, {
230
+ childList: true,
231
+ subtree: true,
232
+ attributes: true,
233
+ attributeFilter: ["editable", "clay-editable", "data-richclay", "richclay"]
234
+ });
235
+ }
236
+
237
+ static registerButton(def) {
238
+ validateButton(def);
239
+ globalRegistry.set(def.id, def);
240
+ return RichClay;
241
+ }
242
+
243
+ static unregisterButton(id) {
244
+ globalRegistry.delete(id);
245
+ return RichClay;
246
+ }
247
+
248
+ static stripFromClone(docElem) {
249
+ stripRichClayFromClone(docElem);
250
+ }
251
+
252
+ // Remove richclay's runtime state from ONE element, addressed by element
253
+ // rather than by instance.
254
+ //
255
+ // A host that grows a list by cloning a row clones an active region with it,
256
+ // and the copy carries contenteditable, the marker and the runtime classes.
257
+ // Usually there is no editor behind them and destroy() cannot reach it,
258
+ // because destroy belongs to an instance and that clone has none. But a clone
259
+ // carrying the author's own data-richclay is mountable, so the watcher can
260
+ // have given it an instance before the host gets here: this cannot assume the
261
+ // element is instanceless.
262
+ static stripElement(element) {
263
+ if (!element || typeof element.getAttribute !== "function") return;
264
+ // A watcher can have mounted an instance on the clone before the host got
265
+ // here, and then this is not the instanceless orphan described above:
266
+ // stripping the attributes while the instance stays in the cache leaves an
267
+ // editor reporting active on an element with no contenteditable, and every
268
+ // later constructor call hands that same dead instance back. destroy() is
269
+ // the same cleanup and it clears the cache too.
270
+ const existing = instances.get(element);
271
+ if (existing) {
272
+ existing.destroy();
273
+ return;
274
+ }
275
+ removeRuntimeState(element, "destroy");
276
+ }
277
+
278
+ registerButton(def) {
279
+ validateButton(def);
280
+ this.registry.set(def.id, def);
281
+ if (this.active) this.renderToolbar();
282
+ return this;
283
+ }
284
+
285
+ unregisterButton(id) {
286
+ this.registry.delete(id);
287
+ if (this.active) this.renderToolbar();
288
+ return this;
289
+ }
290
+
291
+ activate() {
292
+ if (this.unsupported) return;
293
+ if (this.active) return;
294
+
295
+ this.active = true;
296
+ this.applyInlineBox();
297
+ ensureStyles(this.element.ownerDocument);
298
+ consumeInertContenteditable(this.element);
299
+
300
+ // Captured before Squire's constructor wipes the root. Inline mode keeps the
301
+ // live nodes rather than a serialized string, so activation cannot lose
302
+ // anything an innerHTML round trip would drop (form state, node identity).
303
+ const initialNodes = this.options.inline ? Array.from(this.element.childNodes) : null;
304
+ const initialHTML = this.options.inline ? null : this.element.innerHTML;
305
+ const Squire = this.options.Squire || this.window.Squire || globalThis.Squire;
306
+ if (!Squire) {
307
+ throw new Error("RichClay requires Squire. Load vendor/squire.js before richclay.js.");
308
+ }
309
+
310
+ this.setupEditorAttributes();
311
+ this.liveRegion = createLiveRegion(this.element.ownerDocument);
312
+
313
+ this._squire = new Squire(this.element, {
314
+ blockTag: this._blockTag,
315
+ sanitizeToDOMFragment: (html, editor) => {
316
+ const fragment = this.sanitizer.sanitizeToDOMFragment(html, editor);
317
+ return this.options.singleLine ? flattenFragmentToSingleLine(fragment) : fragment;
318
+ },
319
+ didError: error => {
320
+ console.error("RichClay/Squire error", error);
321
+ }
322
+ });
323
+ if (this.options.inline) {
324
+ // Fidelity-first: Squire's constructor wiped the root; put the captured
325
+ // nodes back verbatim. Running them through setHTML would sanitize and
326
+ // re-wrap the page's own content into blocks on mere activation.
327
+ //
328
+ // The structural repair Squire needs is deliberately NOT done here. Merely
329
+ // opening a page in edit mode must leave it byte-identical, or a Hyperclay
330
+ // page with autosave on writes itself to disk for being looked at. It runs
331
+ // on the first real edit instead — see ensureRootIsEditable.
332
+ this.element.replaceChildren(...initialNodes);
333
+ this.resetSquireUndoBaseline();
334
+ } else {
335
+ this._squire.setHTML(initialHTML);
336
+ }
337
+
338
+ this.bindSquire();
339
+ if (this.options.singleLine) this.installSingleLineGuards();
340
+ if (this.options.inline && !this.options.singleLine) this.installRootGuards();
341
+ // Before installShortcuts, so an explicitly declared Ctrl+ shortcut wins.
342
+ this.installAppleDeleteKeys();
343
+ // Squire appends a default block whenever the root's last element child is not
344
+ // its blockTag, from Backspace over a selection, cut, and paste. Where blocks
345
+ // stay out that is a block nobody asked for, and the save hook is not enough on
346
+ // its own: flattening the block leaves its <br> behind, the next session adds
347
+ // another, and the file grows a blank line per edit. Prevention, because the
348
+ // cleanup has a residue.
349
+ if (this.blocksStayOut()) this._squire._ensureBottomLine = () => {};
350
+ this.maskSquireCodeShortcut();
351
+ this.maskSquireBlockShortcuts();
352
+ this.installShortcuts();
353
+ if (this.options.inline) {
354
+ this._onToolbarKey = event => {
355
+ if (event.altKey && event.key === "F10") {
356
+ event.preventDefault();
357
+ this.ensureFloatingToolbar();
358
+ this.float?.toolbar.focusFirst();
359
+ }
360
+ };
361
+ this.element.addEventListener("keydown", this._onToolbarKey);
362
+ }
363
+ // Squire's paste path runs fixContainer over the pasted fragment, so pasting
364
+ // even two plain lines into a <h2 editable> lands a <div> the author never
365
+ // wrote. Flatten it where it arrives: this is the same promise the rest of the
366
+ // editor keeps, that richclay never manufactures a block in a region the
367
+ // author wrote as a line of text.
368
+ if (this.blocksStayOut()) {
369
+ const willPaste = event => {
370
+ const fragment = event.detail?.fragment;
371
+ if (!fragment) return;
372
+ const doc = this.element.ownerDocument;
373
+ flattenBlocks(fragment, () =>
374
+ this.options.singleLine ? doc.createTextNode(" ") : doc.createElement("br")
375
+ );
376
+ };
377
+ this._squire.addEventListener("willPaste", willPaste);
378
+ this._squireListeners.push(["willPaste", willPaste]);
379
+ }
380
+ this.renderToolbar();
381
+ this.updatePlaceholder();
382
+ this.warnOnBlockInInlineRegion();
383
+ // Whatever <pre> elements exist before the first command are the author's.
384
+ this.element.querySelectorAll("pre").forEach(pre => this._authoredPres.add(pre));
385
+ }
386
+
387
+ // Live sync morphs the page against an incoming copy, and that copy is clean:
388
+ // contenteditable, the marker and the runtime attributes all come back off an
389
+ // element this editor is still pointing at. The instance is fine and Squire is
390
+ // still bound; only the element's own state is gone, and activate() cannot put
391
+ // it back because it returns early on an instance that never stopped being
392
+ // active. Rebuilding is not an option for a host that adopted an editor the
393
+ // author mounted, so this re-applies the state instead.
394
+ //
395
+ // The marker is in here because it is the one whose absence is fatal rather
396
+ // than cosmetic: without it the watcher stops recognising the element, which
397
+ // is the failure ensureMarker's own comment describes.
398
+ //
399
+ // Returns whether it had to do anything, so a caller can tell a repair from a
400
+ // no-op without asking the same question twice.
401
+ reattach() {
402
+ if (this.unsupported || !this.active) return false;
403
+ // isMountable is the question ensureMarker asks, so this is "would the
404
+ // watcher still adopt this element", not a guess at which attributes matter.
405
+ const intact =
406
+ this.element.getAttribute("contenteditable") === "true" &&
407
+ this.element.getAttribute("data-richclay-active") === "true" &&
408
+ isMountable(this.element);
409
+ if (intact) return false;
410
+ this.ensureMarker();
411
+ this.applyInlineBox();
412
+ consumeInertContenteditable(this.element);
413
+ this.setupEditorAttributes();
414
+ return true;
415
+ }
416
+
417
+ // A block inside an inline element renders as a run-on. This changes the box
418
+ // and nothing else: whether a region survives a reload is decided by the
419
+ // parser from tag names, before any CSS exists. Runtime only, so the author's
420
+ // file keeps no style richclay put there.
421
+ applyInlineBox() {
422
+ if (isInlineTag(this.element) && !this.element.style.display) {
423
+ this.element.style.display = "inline-block";
424
+ this.element.setAttribute("data-richclay-runtime-display", "true");
425
+ }
426
+ }
427
+
428
+ getHTML() {
429
+ return this._squire ? this._squire.getHTML() : this.element.innerHTML;
430
+ }
431
+
432
+ setHTML(html) {
433
+ if (this._squire) {
434
+ this._squire.setHTML(String(html || ""));
435
+ } else {
436
+ this.element.innerHTML = sanitizeHTMLString(
437
+ html,
438
+ this.sanitizeConfig,
439
+ this.element.ownerDocument
440
+ );
441
+ }
442
+ this.updatePlaceholder();
443
+ this.toolbar?.update();
444
+ this.options.onChange?.(this.getHTML());
445
+ return this;
446
+ }
447
+
448
+ focus() {
449
+ if (this._squire) this._squire.focus();
450
+ else this.element.focus();
451
+ return this;
452
+ }
453
+
454
+ destroy() {
455
+ this.closeLinkDialog();
456
+ this.teardownFloatingToolbar();
457
+ this.toolbar?.destroy();
458
+ this.toolbar = null;
459
+ if (this._onToolbarKey) {
460
+ this.element.removeEventListener("keydown", this._onToolbarKey);
461
+ this._onToolbarKey = null;
462
+ }
463
+ this.liveRegion?.remove();
464
+ this.liveRegion = null;
465
+ this.description?.remove();
466
+ this.description = null;
467
+
468
+ this._shortcutKeys.forEach(key => this._squire?.setKeyHandler?.(key, null));
469
+ this._squireListeners.forEach(([type, listener]) => {
470
+ this._squire?.removeEventListener?.(type, listener);
471
+ });
472
+ this._squireListeners = [];
473
+ this._shortcutKeys = [];
474
+
475
+ if (this._onBeforeInput) {
476
+ this.element.ownerDocument.removeEventListener("beforeinput", this._onBeforeInput, true);
477
+ this._onBeforeInput = null;
478
+ }
479
+
480
+ const doc = this.element.ownerDocument;
481
+ if (this._onRootBeforeInput) {
482
+ doc.removeEventListener("beforeinput", this._onRootBeforeInput, true);
483
+ this._onRootBeforeInput = null;
484
+ }
485
+ if (this._onRootKeydown) {
486
+ doc.removeEventListener("keydown", this._onRootKeydown, true);
487
+ this._onRootKeydown = null;
488
+ }
489
+ if (this._onRootTransfer) {
490
+ ROOT_TRANSFER_EVENTS.forEach(type => doc.removeEventListener(type, this._onRootTransfer, true));
491
+ this._onRootTransfer = null;
492
+ }
493
+
494
+ this._squire?.destroy?.();
495
+ this._squire = null;
496
+ this.cleanupEditorAttributes();
497
+
498
+ instances.delete(this.element);
499
+ }
500
+
501
+ saveSelection() {
502
+ this.savedSelection = getSquireSelection(this._squire) || this.savedSelection;
503
+ return this.savedSelection;
504
+ }
505
+
506
+ restoreSelection() {
507
+ return restoreSquireSelection(this._squire, this.savedSelection);
508
+ }
509
+
510
+ // Squire's invariant is established on the first real edit rather than at
511
+ // activation, so that opening a page in edit mode never rewrites it. These
512
+ // listeners capture on the document, so the root is valid before Squire's own
513
+ // handlers see the event. History inputs are skipped: repairing mid-undo would
514
+ // fight the stack it is replaying.
515
+ installRootGuards() {
516
+ const doc = this.element.ownerDocument;
517
+ const owns = event => this.element.contains(event.target);
518
+
519
+ this._onRootBeforeInput = event => {
520
+ // Repairing mid-composition mutates the DOM under the IME and moves the
521
+ // selection out from under it; repairing mid-undo fights the stack being
522
+ // replayed.
523
+ if (!owns(event) || event.isComposing || /^history/.test(event.inputType || "")) return;
524
+ this.ensureRootIsEditable();
525
+ };
526
+ this._onRootKeydown = event => {
527
+ if (!owns(event) || event.defaultPrevented || event.isComposing) return;
528
+ if (this.isEditingKey(event)) this.ensureRootIsEditable();
529
+ };
530
+ this._onRootTransfer = event => {
531
+ if (owns(event)) this.ensureRootIsEditable();
532
+ };
533
+
534
+ doc.addEventListener("beforeinput", this._onRootBeforeInput, true);
535
+ doc.addEventListener("keydown", this._onRootKeydown, true);
536
+ ROOT_TRANSFER_EVENTS.forEach(type => {
537
+ doc.addEventListener(type, this._onRootTransfer, true);
538
+ });
539
+ }
540
+
541
+ // Re-checked rather than latched, because Hyperclay's live sync morphs new DOM
542
+ // into the region long after activation and can reintroduce a loose text node.
543
+ ensureRootIsEditable() {
544
+ if (!this._squire || this.options.singleLine) return;
545
+ const wrapBareRoot = this.options.inline;
546
+ const blocksAllowed = !this.blocksStayOut();
547
+ if (!editorRootNeedsNormalization(this.element, { wrapBareRoot, blocksAllowed })) return;
548
+
549
+ const range = getSquireSelection(this._squire);
550
+ const saved = range ? captureRange(this.element, range) : null;
551
+ const repair = () => {
552
+ normalizeEditorRoot(this.element, {
553
+ blockTag: this._blockTag,
554
+ wrapBareRoot,
555
+ blocksAllowed,
556
+ onBareRootWrapped: (root, wrapper) => {
557
+ console.warn(
558
+ "richclay: wrapped this region's loose text in a <div> so multi-line editing " +
559
+ "works. Wrap the content in your own block element to keep the markup you wrote, " +
560
+ 'or use editable="single-line" if it is meant to be one line.',
561
+ root,
562
+ wrapper
563
+ );
564
+ }
565
+ });
566
+ };
567
+
568
+ // Through modifyDocument so the repair is not recorded as a change of its
569
+ // own: it folds into the first edit's diff, which is what makes undo restore
570
+ // the byte-identical pre-repair source instead of stepping back into a
571
+ // half-repaired state.
572
+ if (typeof this._squire.modifyDocument === "function") this._squire.modifyDocument(repair);
573
+ else repair();
574
+
575
+ if (saved) {
576
+ restoreRange(this.element, range, saved);
577
+ restoreSquireSelection(this._squire, range);
578
+ this.savedSelection = cloneRange(range);
579
+ }
580
+ }
581
+
582
+ // Only keys that will actually edit. Repairing is not free on a root that has
583
+ // not been repaired yet, which is every freshly opened page, so a blanket
584
+ // "any single-character key" made Cmd+C and Cmd+S rewrite an untouched region.
585
+ // The only modified keys that do edit are the ones this editor rebound itself,
586
+ // which is why the set is asked rather than Squire's dispatch mirrored.
587
+ isEditingKey(event) {
588
+ if (["Backspace", "Delete", "Enter"].includes(event.key)) return true;
589
+ // Squire acts on Tab only inside a list. Anywhere else it edits nothing, and
590
+ // treating it as an editing key made merely tabbing out of a fresh region run
591
+ // the repair and strip the author's source indentation.
592
+ if (event.key === "Tab") return this.caretIsInList();
593
+ if (event.key.length !== 1) return false;
594
+ if (!event.ctrlKey && !event.metaKey && !event.altKey) return true;
595
+ return (
596
+ event.ctrlKey && !event.metaKey && !event.altKey &&
597
+ this._appleDeleteKeys.has(event.key.toLowerCase())
598
+ );
599
+ }
600
+
601
+ caretIsInList() {
602
+ const range = this._squire?.getSelection();
603
+ const container = range?.startContainer;
604
+ if (!container) return false;
605
+ const element = container.nodeType === 1 ? container : container.parentElement;
606
+ return Boolean(element && this.element.contains(element) && element.closest("ul, ol"));
607
+ }
608
+
609
+ runControl(def) {
610
+ if (!this._squire || typeof def.run !== "function") return;
611
+ // Enforced here, not only where the toolbar renders: shortcuts and menu items
612
+ // reach this same path, and the block commands they carry are exactly the
613
+ // ones that put a <blockquote> inside a <p editable>.
614
+ if (def.isDisabled?.(this)) return;
615
+ this.restoreSelection();
616
+ // Before the command, not only after. A toolbar click can be the first
617
+ // interaction with a page and no root guard fires for it, so a block command
618
+ // ran against the raw root: over two source-indented paragraphs, one list
619
+ // command produced two separate <ul>s. Both halves are block-valid, so the
620
+ // heal below cannot undo it.
621
+ if (def.mutates !== false) this.ensureRootIsEditable();
622
+ const result = def.run(this);
623
+ // Stamp any <pre> this command just created, so the save hook can contain it
624
+ // without touching one the author wrote themselves.
625
+ this.element.querySelectorAll("pre:not([data-richclay-pre])").forEach(pre => {
626
+ if (!this._authoredPres.has(pre)) pre.setAttribute("data-richclay-pre", "");
627
+ });
628
+ // After it too. Squire's removeCode() splices an emptied <pre> out onto the
629
+ // root, and a caret parked on the orphan makes every later block command a
630
+ // silent no-op.
631
+ if (def.mutates !== false) {
632
+ this.ensureRootIsEditable();
633
+ this.anchorSelectionInBlock();
634
+ }
635
+ this.saveSelection();
636
+ this.toolbar?.update();
637
+ this.updatePlaceholder();
638
+ this.announceControl(def);
639
+
640
+ if (result !== KEEP_FOCUS) {
641
+ this.focus();
642
+ }
643
+ this.warnOnBlockInInlineRegion();
644
+ }
645
+
646
+ // Squire's modifyBlocks leaves the caret anchored on the root itself, between
647
+ // blocks. From there getStartBlockOfRange finds no block, so the next block
648
+ // command is a silent no-op — the "it stopped working" the user hit. Push a
649
+ // collapsed caret back inside the block it is sitting in front of.
650
+ anchorSelectionInBlock() {
651
+ const range = getSquireSelection(this._squire);
652
+ if (!range?.collapsed || range.startContainer !== this.element) return;
653
+
654
+ const children = Array.from(this.element.childNodes);
655
+ const atEnd = range.startOffset >= children.length;
656
+ // Comments are skipped: a caret set inside a comment node is not a text
657
+ // position, and the region's markers are comments.
658
+ const child = atEnd
659
+ ? children.filter(isCaretHost).pop()
660
+ : children.slice(range.startOffset).find(isCaretHost) ||
661
+ children.slice(0, range.startOffset).filter(isCaretHost).pop();
662
+ if (!child) return;
663
+
664
+ // At the end of the region the caret belongs at the end of the last block,
665
+ // not thrown back to the start of the line it was sitting after.
666
+ const edge = caretEdge(child, !atEnd);
667
+ range.setStart(edge, atEnd ? nodeLength(edge) : 0);
668
+ range.collapse(true);
669
+ restoreSquireSelection(this._squire, range);
670
+ }
671
+
672
+ selectionHasFormat(tag) {
673
+ if (!tag || !this._squire?.hasFormat) return false;
674
+ try {
675
+ return this._squire.hasFormat(tag);
676
+ } catch {
677
+ return false;
678
+ }
679
+ }
680
+
681
+ // Squire sets its path to "(selection)" whenever the selection spans more than
682
+ // one node, so asking the path answered "no" for every multi-block selection.
683
+ // Both boundaries must land in the same matching element, or a selection that
684
+ // merely starts in a list would report as being in one.
685
+ pathHas(tag) {
686
+ const range = getSquireSelection(this._squire);
687
+ if (!range) return false;
688
+ const selector = tag.toLowerCase();
689
+ const start = matchAtBoundary(this.element, range.startContainer, range.startOffset, true, selector);
690
+ return Boolean(start) &&
691
+ start === matchAtBoundary(this.element, range.endContainer, range.endOffset, false, selector);
692
+ }
693
+
694
+ // Blocks stay out of this region for either of two reasons, and both are fixed
695
+ // for the editor's life, which is why the toolbar can leave the controls out
696
+ // rather than grey them. Either the parser would eject a block on the next page
697
+ // load, or the author wrote the region as a line of text and richclay does not
698
+ // rewrite what they wrote.
699
+ blocksStayOut() {
700
+ return (
701
+ this.options.singleLine ||
702
+ ejectsBlocks(this.element) ||
703
+ keepsTextShape(this.element)
704
+ );
705
+ }
706
+
707
+ // Advisory only: the markup is stable and saves correctly, it just will not
708
+ // validate. Checked when the editor mounts and after any command, which covers
709
+ // the ways an author actually produces one. Paste is deliberately not covered:
710
+ // nothing hooks paste any more, and a console note is not worth reinstating a
711
+ // hook that took three rounds to get wrong.
712
+ warnOnBlockInInlineRegion() {
713
+ if (this._warnedInlineBlock || !isInlineTag(this.element)) return;
714
+ if (!hasBlockDescendant(this.element)) return;
715
+ this._warnedInlineBlock = true;
716
+ const tag = this.element.nodeName.toLowerCase();
717
+ console.warn(
718
+ `richclay: a block element inside <${tag} editable> is not valid HTML. It stays where it is ` +
719
+ "and saves correctly, but a validator will flag it. Put the editable attribute on a block " +
720
+ "element if you want blocks here.",
721
+ this.element
722
+ );
723
+ }
724
+
725
+ toggleFormat(tag, addMethod, removeMethod) {
726
+ if (this.selectionHasFormat(tag)) return this._squire[removeMethod]();
727
+ return this._squire[addMethod]();
728
+ }
729
+
730
+ toggleList(tag) {
731
+ if (this.pathHas(tag)) return this._squire.removeList();
732
+ return tag === "UL" ? this._squire.makeUnorderedList() : this._squire.makeOrderedList();
733
+ }
734
+
735
+ indent() {
736
+ if (this.pathHas("UL") || this.pathHas("OL")) return this._squire.increaseListLevel();
737
+ return this._squire.increaseQuoteLevel();
738
+ }
739
+
740
+ outdent() {
741
+ if (this.pathHas("UL") || this.pathHas("OL")) return this._squire.decreaseListLevel();
742
+ return this._squire.decreaseQuoteLevel();
743
+ }
744
+
745
+ setBlockType(tag) {
746
+ const target = tag.toUpperCase();
747
+ if (target === "BLOCKQUOTE") return this._squire.increaseQuoteLevel();
748
+
749
+ return this._squire.modifyBlocks(fragment => {
750
+ const doc = this.element.ownerDocument;
751
+ const output = doc.createDocumentFragment();
752
+
753
+ Array.from(fragment.childNodes).forEach(node => {
754
+ if (node.nodeType !== 1) {
755
+ output.appendChild(node);
756
+ return;
757
+ }
758
+
759
+ if (node.nodeName === target) {
760
+ output.appendChild(node);
761
+ return;
762
+ }
763
+
764
+ const replacement = doc.createElement(target);
765
+ while (node.firstChild) replacement.appendChild(node.firstChild);
766
+ output.appendChild(replacement);
767
+ });
768
+
769
+ return output;
770
+ });
771
+ }
772
+
773
+ openLinkDialog() {
774
+ this.saveSelection();
775
+ this.closeLinkDialog();
776
+
777
+ const doc = this.element.ownerDocument;
778
+ const existing = this.currentLinkHref();
779
+ const dialog = doc.createElement("form");
780
+ const labelId = `richclay-link-label-${++dialogSeq}`;
781
+ dialog.className = "richclay-dialog";
782
+ dialog.setAttribute("role", "dialog");
783
+ dialog.setAttribute("aria-modal", "true");
784
+ dialog.setAttribute("aria-labelledby", labelId);
785
+ dialog.setAttribute("data-richclay-dialog", "");
786
+ markChrome(dialog);
787
+ dialog.innerHTML = `
788
+ <div class="richclay-dialog-title" id="${labelId}">${existing ? "Edit link" : "Insert link"}</div>
789
+ <label class="richclay-field">
790
+ <span>URL</span>
791
+ <input class="richclay-input" name="url" type="text" inputmode="url" autocomplete="url" placeholder="https://example.com">
792
+ </label>
793
+ <div class="richclay-dialog-actions">
794
+ <button type="button" class="richclay-secondary" data-richclay-cancel>Cancel</button>
795
+ <button type="submit" class="richclay-primary">Apply</button>
796
+ </div>
797
+ `;
798
+
799
+ const close = () => {
800
+ this.closeLinkDialog();
801
+ this.toolbar?.update();
802
+ this.focus();
803
+ };
804
+
805
+ dialog.addEventListener("submit", event => {
806
+ event.preventDefault();
807
+ const data = new FormData(dialog);
808
+ const href = normalizeUrl(data.get("url"));
809
+ if (!href) {
810
+ announce(this.liveRegion, "Enter a valid URL");
811
+ return;
812
+ }
813
+ this.restoreSelection();
814
+ const anchor = this.currentLinkElement();
815
+ if (anchor) this.selectElement(anchor);
816
+ this._squire.makeLink(href);
817
+ announce(this.liveRegion, "Link applied");
818
+ close();
819
+ });
820
+ dialog.querySelector("[data-richclay-cancel]").addEventListener("click", close);
821
+ dialog.addEventListener("keydown", event => {
822
+ if (event.key === "Escape") {
823
+ event.preventDefault();
824
+ close();
825
+ } else if (event.key === "Tab") {
826
+ trapTab(event, dialog);
827
+ }
828
+ });
829
+
830
+ const mount = this.toolbar?.root || this.element;
831
+ mount.insertAdjacentElement("afterend", dialog);
832
+ this.dialog = dialog;
833
+
834
+ const input = dialog.querySelector("input");
835
+ input.value = existing;
836
+ input.focus();
837
+ input.select();
838
+ return KEEP_FOCUS;
839
+ }
840
+
841
+ currentLinkElement() {
842
+ const range = getSquireSelection(this._squire);
843
+ let node = range?.commonAncestorContainer || null;
844
+ if (node && node.nodeType !== 1) node = node.parentElement;
845
+ return node?.closest?.("a[href]") || null;
846
+ }
847
+
848
+ currentLinkHref() {
849
+ const anchor = this.currentLinkElement();
850
+ return anchor ? anchor.getAttribute("href") || "" : "";
851
+ }
852
+
853
+ selectElement(node) {
854
+ const range = node.ownerDocument.createRange();
855
+ range.selectNode(node);
856
+ this._squire.setSelection(range);
857
+ this.saveSelection();
858
+ return range;
859
+ }
860
+
861
+ closeLinkDialog() {
862
+ this.dialog?.remove();
863
+ this.dialog = null;
864
+ }
865
+
866
+ ensureMarker() {
867
+ // The marker exists so an element mounted through an explicit selector is
868
+ // still recognized on the next pass. What decides that is not whether the
869
+ // element matches the selector but whether the watcher would adopt it again: a
870
+ // custom element carrying a bare `editable` matches and is still refused by the
871
+ // host guard, so with no marker its editor died on the first node replacement
872
+ // and never came back. Stamping makes the guard pass, so the two agree.
873
+ if (!isMountable(this.element)) {
874
+ // Record that WE invented this marker. Without it, cleanup cannot tell an
875
+ // element the author marked from one richclay stamped, so it either leaves
876
+ // ours behind forever (the element stays adoptable on every later load,
877
+ // and the attribute reaches the saved file) or strips theirs (silently
878
+ // breaking a region they asked for). Same provenance idea as
879
+ // data-richclay-runtime-contenteditable just below.
880
+ this.element.setAttribute("data-richclay-runtime-marker", "true");
881
+ this.element.setAttribute("data-richclay", "");
882
+ }
883
+ }
884
+
885
+ setupEditorAttributes() {
886
+ this.element.classList.add(
887
+ this.options.inline ? "richclay-inline" : "richclay-editor",
888
+ "richclay-active"
889
+ );
890
+ this.element.setAttribute("data-richclay-active", "true");
891
+
892
+ if (!this.element.hasAttribute("data-richclay-runtime-contenteditable")) {
893
+ const original = this.element.getAttribute("contenteditable");
894
+ if (original === null) {
895
+ this.element.setAttribute("data-richclay-runtime-contenteditable", "true");
896
+ } else if (original !== "true" && original !== "") {
897
+ // A bare contenteditable attribute is the "true" state; only meaningful
898
+ // non-default values are recorded for restore.
899
+ this.element.setAttribute("data-richclay-runtime-contenteditable", original);
900
+ }
901
+ }
902
+ this.element.setAttribute("contenteditable", "true");
903
+
904
+ if (!this.element.hasAttribute("role")) {
905
+ setRuntimeAttribute(this.element, "role", "textbox", "data-richclay-runtime-role");
906
+ }
907
+ if (!this.element.hasAttribute("aria-multiline")) {
908
+ setRuntimeAttribute(
909
+ this.element,
910
+ "aria-multiline",
911
+ this.options.singleLine ? "false" : "true",
912
+ "data-richclay-runtime-aria-multiline"
913
+ );
914
+ }
915
+
916
+ // Squire owns this region's undo stack. Mark the region so Hyperclay's
917
+ // optional page-level undo (hyper-undo) defers to Squire instead of fighting
918
+ // it. Runtime-only: stripped on save, re-applied on the next edit-mode load.
919
+ if (!this.element.hasAttribute("no-undo")) {
920
+ setRuntimeAttribute(this.element, "no-undo", "", "data-richclay-runtime-no-undo");
921
+ }
922
+
923
+ if (this.options.placeholder) {
924
+ this.element.setAttribute("data-richclay-placeholder", this.options.placeholder);
925
+ // One description for the life of the instance. A second call that built
926
+ // another would orphan the first: destroy() removes only the node this
927
+ // points at, and removeRuntimeDescribedBy clears only the id named by the
928
+ // runtime attribute, both of which the second call has overwritten. The
929
+ // original node and a dangling aria-describedby would reach the saved page.
930
+ // A morph can take the node itself away, so a detached one is put back
931
+ // rather than replaced, which keeps its id and the wiring that names it.
932
+ if (!this.description) {
933
+ this.description = this.element.ownerDocument.createElement("div");
934
+ this.description.id = `richclay-placeholder-${++dialogSeq}`;
935
+ this.description.className = "richclay-sr-only";
936
+ this.description.textContent = this.options.placeholder;
937
+ this.description.setAttribute("data-richclay-live", "");
938
+ markChrome(this.description);
939
+ }
940
+ if (!this.description.isConnected) {
941
+ this.element.insertAdjacentElement("afterend", this.description);
942
+ }
943
+ // Outside the branch above, because a morph can strip the element's own
944
+ // aria-describedby while leaving this node connected, and then the hint is
945
+ // gone with nothing to notice it. connectDescription builds its id list
946
+ // from a Set, so naming the same description twice costs nothing.
947
+ connectDescription(this.element, this.description);
948
+ }
949
+
950
+ this.element.addEventListener("focus", this._onFocus);
951
+ this.element.addEventListener("blur", this._onBlur);
952
+ }
953
+
954
+ cleanupEditorAttributes() {
955
+ removeRuntimeState(this.element, "destroy");
956
+ this.element.removeEventListener("focus", this._onFocus);
957
+ this.element.removeEventListener("blur", this._onBlur);
958
+ }
959
+
960
+ bindSquire() {
961
+ const input = () => {
962
+ this.saveSelection();
963
+ this.updatePlaceholder();
964
+ this.options.onChange?.(this.getHTML());
965
+ this.toolbar?.update();
966
+ };
967
+ const pathChange = event => {
968
+ this.path = event.detail?.path || this._squire.getPath?.() || "";
969
+ this.saveSelection();
970
+ this.toolbar?.update();
971
+ };
972
+ const selection = () => {
973
+ this.saveSelection();
974
+ this.toolbar?.update();
975
+ if (this.options.toolbarOnSelect) this.updateFloatVisibility();
976
+ };
977
+ const undoState = () => this.toolbar?.update();
978
+
979
+ [
980
+ ["input", input],
981
+ ["pathChange", pathChange],
982
+ ["select", selection],
983
+ ["cursor", selection],
984
+ ["undoStateChange", undoState]
985
+ ].forEach(([type, listener]) => {
986
+ this._squire.addEventListener(type, listener);
987
+ this._squireListeners.push([type, listener]);
988
+ });
989
+ }
990
+
991
+ renderToolbar() {
992
+ if (!this.active || this.options.readOnly) return;
993
+ if (this.options.inline) {
994
+ // Inline toolbars are floating and focus-scoped; if one is open, rebuild
995
+ // it so registry changes show up immediately.
996
+ if (this.float) {
997
+ this.teardownFloatingToolbar();
998
+ this.ensureFloatingToolbar();
999
+ }
1000
+ return;
1001
+ }
1002
+ this.toolbar?.destroy();
1003
+ this.toolbar = null;
1004
+ const controls = this.resolveToolbarControls(this.options.toolbar);
1005
+ if (!controls.length) return;
1006
+ this.toolbar = new Toolbar(this, controls, {
1007
+ toolbarContainer: this.options.toolbarContainer
1008
+ });
1009
+ }
1010
+
1011
+ ensureFloatingToolbar() {
1012
+ if (this.float || !this.active || this.options.readOnly) return;
1013
+ const controls = this.resolveToolbarControls(this.options.toolbar);
1014
+ if (!controls.length) return;
1015
+ this.float = new FloatingToolbar(this, controls);
1016
+ // Blur on the editor doesn't fire when focus later leaves the toolbar or
1017
+ // link dialog directly (Alt+F10 then Tab away), so the shell watches its
1018
+ // own focusout too.
1019
+ this._onFloatFocusOut = event => this.scheduleFloatTeardown(event);
1020
+ this.float.root.addEventListener("focusout", this._onFloatFocusOut);
1021
+ // A press on non-focusable page chrome never blurs a contenteditable, so
1022
+ // blur/focusout alone can't dismiss the float. Watch the document
1023
+ // (capture, so stopped events still count) and dismiss on any pointer
1024
+ // press the editor doesn't own, releasing focus with it.
1025
+ this._onDocPointerDown = event => {
1026
+ if (this.ownsFocusTarget(event.target)) return;
1027
+ const doc = this.element.ownerDocument;
1028
+ if (doc.activeElement && this.ownsFocusTarget(doc.activeElement)) {
1029
+ doc.activeElement.blur?.();
1030
+ }
1031
+ this.teardownFloatingToolbar();
1032
+ };
1033
+ this.element.ownerDocument.addEventListener("pointerdown", this._onDocPointerDown, true);
1034
+ this.toolbar = this.float.toolbar;
1035
+ this.toolbar.update();
1036
+ if (this.options.toolbarOnSelect) this.updateFloatVisibility();
1037
+ }
1038
+
1039
+ teardownFloatingToolbar() {
1040
+ if (!this.float) return;
1041
+ if (this._onFloatFocusOut) {
1042
+ this.float.root.removeEventListener("focusout", this._onFloatFocusOut);
1043
+ this._onFloatFocusOut = null;
1044
+ }
1045
+ if (this._onDocPointerDown) {
1046
+ this.element.ownerDocument.removeEventListener("pointerdown", this._onDocPointerDown, true);
1047
+ this._onDocPointerDown = null;
1048
+ }
1049
+ this.float.destroy();
1050
+ this.float = null;
1051
+ this.toolbar = null;
1052
+ }
1053
+
1054
+ scheduleFloatTeardown(event) {
1055
+ const next = event?.relatedTarget;
1056
+ if (next && this.ownsFocusTarget(next)) return;
1057
+ const doc = this.element.ownerDocument;
1058
+ const win = doc.defaultView || globalThis;
1059
+ win.setTimeout(() => {
1060
+ const active = doc.activeElement;
1061
+ if (active && this.ownsFocusTarget(active)) return;
1062
+ this.teardownFloatingToolbar();
1063
+ }, 0);
1064
+ }
1065
+
1066
+ ownsFocusTarget(node) {
1067
+ return (
1068
+ node === this.element ||
1069
+ this.element.contains(node) ||
1070
+ Boolean(this.float?.root.contains(node)) ||
1071
+ Boolean(this.dialog?.contains(node))
1072
+ );
1073
+ }
1074
+
1075
+ updateFloatVisibility() {
1076
+ if (!this.float) return;
1077
+ const range = this.savedSelection;
1078
+ this.float.setVisible(Boolean(range && !range.collapsed));
1079
+ }
1080
+
1081
+ resolveToolbarControls(toolbar) {
1082
+ if (toolbar === false || toolbar === null || toolbar === "none") return [];
1083
+ const requested = Array.isArray(toolbar) ? toolbar : presets[toolbar] || presets.standard;
1084
+
1085
+ return requested.map(item => {
1086
+ if (typeof item === "string") {
1087
+ const def = this.registry.get(item);
1088
+ if (!def) throw new Error(`Unknown RichClay toolbar control: ${item}`);
1089
+ return def;
1090
+ }
1091
+ if (item?.type === "separator") return item;
1092
+ validateButton(item);
1093
+ return item;
1094
+ });
1095
+ }
1096
+
1097
+ installShortcuts() {
1098
+ // Shortcuts are a capability of the registry plus this instance's toolbar
1099
+ // definitions, not of the visible toolbar: toolbar:false keeps shortcuts,
1100
+ // and inline toolbar defs win over registry defs with the same id.
1101
+ const seen = new Set();
1102
+ // Single-line editors must never gain block commands: registry defaults are
1103
+ // filtered to the inline preset (explicit toolbar defs still always bind).
1104
+ const registryDefs = [...this.registry.values()].filter(
1105
+ def => !this.options.singleLine || presets.inline.includes(def.id)
1106
+ );
1107
+ const defs = [...this.resolveToolbarControls(this.options.toolbar), ...registryDefs];
1108
+ defs.forEach(def => {
1109
+ if (def.type === "menu" || def.type === "separator") return;
1110
+ if (!def.shortcut || !def.id || seen.has(def.id)) return;
1111
+ // isDisabled is a property of the root, not of the selection, so a control
1112
+ // disabled here is disabled for this editor's whole life. Masked rather than
1113
+ // skipped, for the same reason as maskSquireBlockShortcuts: not installing
1114
+ // ours is what leaves Squire's own handler in charge.
1115
+ if (def.isDisabled?.(this)) {
1116
+ const masked = shortcutKey(def.shortcut, this.window);
1117
+ this._squire.setKeyHandler(masked, null);
1118
+ this._shortcutKeys.push(masked);
1119
+ return;
1120
+ }
1121
+ seen.add(def.id);
1122
+ const key = shortcutKey(def.shortcut, this.window);
1123
+ this._squire.setKeyHandler(key, (squire, event) => {
1124
+ event.preventDefault();
1125
+ this.runControl(def);
1126
+ });
1127
+ this._shortcutKeys.push(key);
1128
+ });
1129
+ }
1130
+
1131
+ // macOS sends Ctrl+D and Ctrl+H to the browser's own forward/backward delete,
1132
+ // which merges two blocks by wrapping the moved text in a computed-style
1133
+ // <span> — permanent junk in an editor whose DOM is the saved document. Point
1134
+ // them at Squire's own handlers so they behave exactly like Delete and
1135
+ // Backspace. Reads Squire's key handler table; no-ops for other engines.
1136
+ // Squire binds Code to the platform modifier itself, on _keyHandlers' prototype,
1137
+ // so dropping richclay's own shortcut is not enough to retire it. An own
1138
+ // property of null shadows the inherited handler and the key falls through.
1139
+ maskSquireCodeShortcut() {
1140
+ const key = `${isApplePlatform(this.window) ? "Meta" : "Ctrl"}-d`;
1141
+ this._squire.setKeyHandler(key, null);
1142
+ this._shortcutKeys.push(key);
1143
+ }
1144
+
1145
+ // Squire binds these on _keyHandlers' prototype, and richclay has no definition
1146
+ // carrying some of them, so filtering richclay's own shortcuts cannot reach
1147
+ // them: Mod+] kept building a <blockquote> in a <p editable> through every
1148
+ // round. An own property of null shadows the inherited handler and the key
1149
+ // falls through. The prefix must match the one Squire computed, which is why
1150
+ // the test harness has to agree about the platform.
1151
+ maskSquireBlockShortcuts() {
1152
+ if (!this.blocksStayOut()) return;
1153
+ const mod = isApplePlatform(this.window) ? "Meta" : "Ctrl";
1154
+ [`${mod}-]`, `${mod}-[`, `${mod}-Shift-8`, `${mod}-Shift-9`].forEach(key => {
1155
+ this._squire.setKeyHandler(key, null);
1156
+ this._shortcutKeys.push(key);
1157
+ });
1158
+ }
1159
+
1160
+ // Squire's toggleCode() makes a block <pre> when the selection is collapsed,
1161
+ // which inside a heading or a paragraph root means a code block where no block
1162
+ // can legally go. Those regions get inline <code> instead.
1163
+ toggleCode() {
1164
+ if (!this.blocksStayOut()) return this._squire.toggleCode();
1165
+ if (this.selectionHasFormat("CODE")) return this._squire.changeFormat(null, { tag: "CODE" });
1166
+ return this._squire.changeFormat({ tag: "CODE" }, null);
1167
+ }
1168
+
1169
+ installAppleDeleteKeys() {
1170
+ if (!isApplePlatform(this.window)) return;
1171
+ const handlers = this._squire._keyHandlers;
1172
+ if (!handlers) return;
1173
+
1174
+ [["Ctrl-d", "Delete"], ["Ctrl-h", "Backspace"]].forEach(([key, native]) => {
1175
+ if (typeof handlers[native] !== "function") return;
1176
+ this._squire.setKeyHandler(key, handlers[native]);
1177
+ this._shortcutKeys.push(key);
1178
+ this._appleDeleteKeys.add(key.slice("Ctrl-".length));
1179
+ });
1180
+ }
1181
+
1182
+ installSingleLineGuards() {
1183
+ ["Enter", "Shift-Enter"].forEach(key => {
1184
+ this._squire.setKeyHandler(key, (squire, event) => event.preventDefault());
1185
+ this._shortcutKeys.push(key);
1186
+ });
1187
+ // Belt for input paths that bypass keydown (IME confirm, mobile keyboards):
1188
+ // cancel paragraph/line-break insertions before Squire's own handler runs.
1189
+ this._onBeforeInput = event => {
1190
+ if (!this.element.contains(event.target)) return;
1191
+ if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") {
1192
+ event.preventDefault();
1193
+ event.stopPropagation();
1194
+ }
1195
+ };
1196
+ this.element.ownerDocument.addEventListener("beforeinput", this._onBeforeInput, true);
1197
+ }
1198
+
1199
+ // The fidelity restore bypasses setHTML, so Squire's undo baseline is still
1200
+ // the empty document its constructor recorded; the first undo could wipe the
1201
+ // region. Rebase the stack on the restored content. This touches Squire
1202
+ // internals (property names survive its esbuild minify) and no-ops for other
1203
+ // engines, like the tests' FakeSquire.
1204
+ resetSquireUndoBaseline() {
1205
+ const squire = this._squire;
1206
+ if (!squire || !Array.isArray(squire._undoStack) || typeof squire.saveUndoState !== "function") {
1207
+ return;
1208
+ }
1209
+ squire._undoStack.length = 0;
1210
+ squire._undoStackLength = 0;
1211
+ squire._undoIndex = -1;
1212
+ squire._isInUndoState = false;
1213
+ squire.saveUndoState();
1214
+ }
1215
+
1216
+ updatePlaceholder() {
1217
+ const text = (this.element.textContent || "").replace(/\u200B/g, "").trim();
1218
+ const hasMeaningfulElement = this.element.querySelector("img, video, audio, iframe, table");
1219
+ const isEmpty = !text && !hasMeaningfulElement;
1220
+ this.element.classList.toggle("richclay-empty", isEmpty);
1221
+ }
1222
+
1223
+ announceControl(def) {
1224
+ if (!this.liveRegion) return;
1225
+ const label = def.ariaLabel || def.label;
1226
+ if (!label) return;
1227
+
1228
+ if (typeof def.isActive === "function") {
1229
+ announce(this.liveRegion, `${label} ${def.isActive(this) ? "on" : "off"}`);
1230
+ } else {
1231
+ announce(this.liveRegion, `${label} applied`);
1232
+ }
1233
+ }
1234
+ }
1235
+
1236
+ export { presets };
1237
+
1238
+ function resolveElements(selector) {
1239
+ if (typeof selector === "string") {
1240
+ return Array.from(document.querySelectorAll(selector));
1241
+ }
1242
+ if (selector?.nodeType === 1) return [selector];
1243
+ return Array.from(selector || []);
1244
+ }
1245
+
1246
+ // A selection anchored on the root itself, which is what Select All produces, has
1247
+ // no ancestor chain to walk, so the boundary is first resolved down to the node it
1248
+ // actually points at. Type selectors are ASCII case-insensitive in HTML documents
1249
+ // but jsdom's closest() is not, hence the lowercased selector.
1250
+ function matchAtBoundary(root, container, offset, first, selector) {
1251
+ let node = container;
1252
+ if (node === root) {
1253
+ const children = Array.from(root.childNodes).filter(isCaretHost);
1254
+ if (!children.length) return null;
1255
+ const index = first
1256
+ ? Math.min(offset, children.length - 1)
1257
+ : Math.min(Math.max(offset - 1, 0), children.length - 1);
1258
+ node = caretEdge(children[index], first);
1259
+ }
1260
+ const element = node?.nodeType === 1 ? node : node?.parentElement;
1261
+ const match = element?.closest?.(selector);
1262
+ return match && root.contains(match) && match !== root ? match : null;
1263
+ }
1264
+
1265
+ // An ancestor conflicts when it holds a live editor, or when it would get one.
1266
+ // closest() cannot answer the second half on its own: it stops at the nearest
1267
+ // element matching the selector, and a custom element the host guard refuses
1268
+ // matches it while never becoming an editor, which made <my-grid editable> block a
1269
+ // genuine <h2 editable> inside it and left the page with no editors at all.
1270
+ function closestConflictingAncestor(element) {
1271
+ let node = element.parentElement;
1272
+ while (node) {
1273
+ if (instances.has(node) || isMountable(node)) return node;
1274
+ node = node.parentElement;
1275
+ }
1276
+ return null;
1277
+ }
1278
+
1279
+ // Two Squire instances mutating one subtree is undefined: the outer editor's
1280
+ // repair restructures the inner one's content behind its back, inside the outer's
1281
+ // modifyDocument, so the inner instance never sees it. Checked in both directions,
1282
+ // because the editors can be constructed in either order.
1283
+ // The single question every mount path asks: would the default watcher adopt this?
1284
+ function isMountable(element) {
1285
+ return Boolean(element.matches?.(RICHCLAY_SELECTOR)) && isRichClayHost(element);
1286
+ }
1287
+
1288
+ function conflictsWithExistingEditor(element) {
1289
+ const host = closestConflictingAncestor(element);
1290
+ const nested = Array.from(element.querySelectorAll?.(RICHCLAY_SELECTOR) || []).find(node =>
1291
+ instances.has(node)
1292
+ );
1293
+ const other = host || nested;
1294
+ if (!other) return false;
1295
+ console.warn(
1296
+ "richclay: nested editable regions are not supported, so this one was skipped. " +
1297
+ "Remove the editable attribute from either it or the other region.",
1298
+ element,
1299
+ other
1300
+ );
1301
+ return true;
1302
+ }
1303
+
1304
+ function refuseUnsupportedRoot(element) {
1305
+ if (!isUnsupportedRootTag(element)) return false;
1306
+ console.warn(`richclay: ${unsupportedRootReason(element)} This region was skipped.`, element);
1307
+ return true;
1308
+ }
1309
+
1310
+ function validateButton(def) {
1311
+ if (!def || !def.id) throw new Error("RichClay button definitions require an id.");
1312
+ if (def.type !== "menu" && typeof def.run !== "function") {
1313
+ throw new Error(`RichClay button "${def.id}" requires a run(editor) function.`);
1314
+ }
1315
+ }
1316
+
1317
+ // Squire looks a handler up by "Alt-Ctrl-Meta-Shift-<key>", in that order, and
1318
+ // binds Mod to Meta on Apple platforms and to Ctrl elsewhere. richclay used to
1319
+ // bind both, which on macOS swallowed the Emacs bindings every text field has:
1320
+ // Ctrl+D (delete forward), Ctrl+K (kill line), Ctrl+B, Ctrl+U, Ctrl+I.
1321
+ function shortcutKey(shortcut, win) {
1322
+ const parts = shortcut.split("+");
1323
+ const keyPart = parts[parts.length - 1];
1324
+ const modifiers = new Set(parts.slice(0, -1).map(normalizeModifier));
1325
+ if (modifiers.delete("Mod")) modifiers.add(isApplePlatform(win) ? "Meta" : "Ctrl");
1326
+
1327
+ const key =
1328
+ keyPart.length === 1
1329
+ ? modifiers.has("Shift")
1330
+ ? keyPart.toUpperCase()
1331
+ : keyPart.toLowerCase()
1332
+ : keyPart;
1333
+ const prefix = MODIFIER_ORDER.filter(modifier => modifiers.has(modifier)).join("-");
1334
+ return `${prefix ? `${prefix}-` : ""}${key}`;
1335
+ }
1336
+
1337
+ function normalizeModifier(modifier) {
1338
+ return modifier === "Cmd" ? "Meta" : modifier;
1339
+ }
1340
+
1341
+ function trapTab(event, container) {
1342
+ const focusables = container.querySelectorAll(
1343
+ 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
1344
+ );
1345
+ if (!focusables.length) return;
1346
+ const first = focusables[0];
1347
+ const last = focusables[focusables.length - 1];
1348
+ const active = container.ownerDocument.activeElement;
1349
+ if (event.shiftKey && active === first) {
1350
+ event.preventDefault();
1351
+ last.focus();
1352
+ } else if (!event.shiftKey && active === last) {
1353
+ event.preventDefault();
1354
+ first.focus();
1355
+ }
1356
+ }