@yewdiel21/motion-kit 0.10.0 → 0.14.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 (365) hide show
  1. package/README.md +155 -93
  2. package/dist/auto/index.d.ts +53 -3
  3. package/dist/auto/index.js +101 -3
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-IKQ5FODN.js → chunk-6P4P7NU6.js} +2 -218
  6. package/dist/chunk-6P4P7NU6.js.map +1 -0
  7. package/dist/chunk-CU6SXW3E.js +20334 -0
  8. package/dist/chunk-CU6SXW3E.js.map +1 -0
  9. package/dist/{chunk-XAKC2QY7.js → chunk-EY4BJKC4.js} +3 -28
  10. package/dist/chunk-EY4BJKC4.js.map +1 -0
  11. package/dist/chunk-QP4QJOEN.js +72 -0
  12. package/dist/chunk-QP4QJOEN.js.map +1 -0
  13. package/dist/css/components/activity-feed.css +240 -0
  14. package/dist/css/components/attachment.css +266 -0
  15. package/dist/css/components/breadcrumb.css +110 -0
  16. package/dist/css/components/chart-tooltip.css +168 -0
  17. package/dist/css/components/chart.css +296 -0
  18. package/dist/css/components/code-block.css +264 -0
  19. package/dist/css/components/color-area.css +82 -0
  20. package/dist/css/components/color-field.css +67 -0
  21. package/dist/css/components/color-picker.css +131 -0
  22. package/dist/css/components/color-slider.css +113 -0
  23. package/dist/css/components/command-search.css +252 -0
  24. package/dist/css/components/context-menu.css +23 -0
  25. package/dist/css/components/credit-card.css +288 -0
  26. package/dist/css/components/data-table.css +591 -0
  27. package/dist/css/components/date-range-picker.css +168 -0
  28. package/dist/css/components/date-time-picker.css +178 -0
  29. package/dist/css/components/drawer.css +58 -0
  30. package/dist/css/components/editable-table.css +201 -0
  31. package/dist/css/components/empty-state.css +61 -0
  32. package/dist/css/components/file-explorer.css +633 -0
  33. package/dist/css/components/filter-builder.css +232 -0
  34. package/dist/css/components/hold-confirm.css +87 -0
  35. package/dist/css/components/hover-card.css +13 -7
  36. package/dist/css/components/image-preview.css +195 -0
  37. package/dist/css/components/json-viewer.css +272 -0
  38. package/dist/css/components/like-button.css +78 -0
  39. package/dist/css/components/link-preview.css +275 -0
  40. package/dist/css/components/loading-button.css +77 -0
  41. package/dist/css/components/multi-select.css +267 -0
  42. package/dist/css/components/notification-bell.css +127 -0
  43. package/dist/css/components/password-strength.css +183 -0
  44. package/dist/css/components/popover.css +25 -0
  45. package/dist/css/components/progress.css +140 -0
  46. package/dist/css/components/questionnaire.css +217 -0
  47. package/dist/css/components/range-calendar.css +186 -0
  48. package/dist/css/components/range-slider.css +192 -0
  49. package/dist/css/components/resizable-panel.css +243 -0
  50. package/dist/css/components/rich-text-editor.css +284 -0
  51. package/dist/css/components/scratch-card.css +75 -0
  52. package/dist/css/components/skeleton.css +108 -0
  53. package/dist/css/components/split-view.css +19 -0
  54. package/dist/css/components/star-rating.css +80 -0
  55. package/dist/css/components/stat-card.css +164 -0
  56. package/dist/css/components/swipe-actions.css +105 -0
  57. package/dist/css/components/tag-input.css +244 -0
  58. package/dist/css/components/time-field.css +88 -0
  59. package/dist/css/components/timeline.css +219 -0
  60. package/dist/css/components/tooltip.css +79 -0
  61. package/dist/css/components/tree-view.css +210 -0
  62. package/dist/css/components/virtual-list.css +195 -0
  63. package/dist/css/tokens.css +185 -181
  64. package/dist/elements/index.d.ts +674 -5
  65. package/dist/elements/index.js +1361 -8
  66. package/dist/elements/index.js.map +1 -1
  67. package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
  68. package/dist/engine-QECQnGz1.d.ts +33 -0
  69. package/dist/index.css +9104 -189
  70. package/dist/index.d.ts +89 -5
  71. package/dist/index.js +243 -9
  72. package/dist/index.js.map +1 -1
  73. package/dist/react/index.d.ts +579 -23
  74. package/dist/react/index.js +2042 -7
  75. package/dist/react/index.js.map +1 -1
  76. package/dist/{reduced-BmBzKH94.d.ts → reduced-BBUusld6.d.ts} +9 -2
  77. package/dist/reveal-DEvp6U6T.d.ts +2381 -0
  78. package/dist/{store-DYb3tTsZ.d.ts → store-DjkTBX7f.d.ts} +2 -2
  79. package/package.json +1 -1
  80. package/registry.json +406 -1
  81. package/src/auto/auto-init.ts +98 -0
  82. package/src/components/activity-feed/README.md +56 -0
  83. package/src/components/activity-feed/activity-feed.css +239 -0
  84. package/src/components/activity-feed/element.ts +36 -0
  85. package/src/components/activity-feed/engine.ts +351 -0
  86. package/src/components/activity-feed/react.tsx +32 -0
  87. package/src/components/attachment/README.md +27 -0
  88. package/src/components/attachment/attachment.css +265 -0
  89. package/src/components/attachment/element.ts +50 -0
  90. package/src/components/attachment/engine.ts +469 -0
  91. package/src/components/attachment/react.tsx +65 -0
  92. package/src/components/breadcrumb/README.md +38 -0
  93. package/src/components/breadcrumb/breadcrumb.css +109 -0
  94. package/src/components/breadcrumb/element.ts +27 -0
  95. package/src/components/breadcrumb/engine.ts +249 -0
  96. package/src/components/breadcrumb/react.tsx +30 -0
  97. package/src/components/chart/README.md +24 -0
  98. package/src/components/chart/chart.css +295 -0
  99. package/src/components/chart/element.ts +41 -0
  100. package/src/components/chart/engine.ts +811 -0
  101. package/src/components/chart/react.tsx +50 -0
  102. package/src/components/chart-tooltip/README.md +53 -0
  103. package/src/components/chart-tooltip/chart-tooltip.css +167 -0
  104. package/src/components/chart-tooltip/element.ts +37 -0
  105. package/src/components/chart-tooltip/engine.ts +442 -0
  106. package/src/components/chart-tooltip/react.tsx +47 -0
  107. package/src/components/code-block/README.md +19 -0
  108. package/src/components/code-block/code-block.css +263 -0
  109. package/src/components/code-block/element.ts +34 -0
  110. package/src/components/code-block/engine.ts +254 -0
  111. package/src/components/code-block/highlight.ts +244 -0
  112. package/src/components/code-block/react.tsx +94 -0
  113. package/src/components/color-area/README.md +50 -0
  114. package/src/components/color-area/color-area.css +81 -0
  115. package/src/components/color-area/element.ts +26 -0
  116. package/src/components/color-area/engine.ts +246 -0
  117. package/src/components/color-area/react.tsx +42 -0
  118. package/src/components/color-field/README.md +43 -0
  119. package/src/components/color-field/color-field.css +66 -0
  120. package/src/components/color-field/element.ts +26 -0
  121. package/src/components/color-field/engine.ts +174 -0
  122. package/src/components/color-field/react.tsx +42 -0
  123. package/src/components/color-picker/README.md +45 -0
  124. package/src/components/color-picker/color-picker.css +130 -0
  125. package/src/components/color-picker/element.ts +45 -0
  126. package/src/components/color-picker/engine.ts +314 -0
  127. package/src/components/color-picker/react.tsx +47 -0
  128. package/src/components/color-slider/README.md +55 -0
  129. package/src/components/color-slider/color-slider.css +112 -0
  130. package/src/components/color-slider/element.ts +26 -0
  131. package/src/components/color-slider/engine.ts +234 -0
  132. package/src/components/color-slider/react.tsx +45 -0
  133. package/src/components/command-search/README.md +15 -0
  134. package/src/components/command-search/command-search.css +251 -0
  135. package/src/components/command-search/element.ts +36 -0
  136. package/src/components/command-search/engine.ts +356 -0
  137. package/src/components/command-search/match.ts +53 -0
  138. package/src/components/command-search/react.tsx +66 -0
  139. package/src/components/context-menu/README.md +31 -0
  140. package/src/components/context-menu/context-menu.css +22 -0
  141. package/src/components/context-menu/element.ts +40 -0
  142. package/src/components/context-menu/engine.ts +153 -0
  143. package/src/components/context-menu/react.tsx +57 -0
  144. package/src/components/credit-card/README.md +20 -0
  145. package/src/components/credit-card/card.ts +77 -0
  146. package/src/components/credit-card/credit-card.css +287 -0
  147. package/src/components/credit-card/element.ts +33 -0
  148. package/src/components/credit-card/engine.ts +410 -0
  149. package/src/components/credit-card/react.tsx +36 -0
  150. package/src/components/data-table/README.md +83 -0
  151. package/src/components/data-table/data-table.css +590 -0
  152. package/src/components/data-table/element.ts +54 -0
  153. package/src/components/data-table/engine.ts +1059 -0
  154. package/src/components/data-table/react.tsx +94 -0
  155. package/src/components/date-range-picker/README.md +51 -0
  156. package/src/components/date-range-picker/date-range-picker.css +167 -0
  157. package/src/components/date-range-picker/element.ts +52 -0
  158. package/src/components/date-range-picker/engine.ts +352 -0
  159. package/src/components/date-range-picker/react.tsx +57 -0
  160. package/src/components/date-time-picker/README.md +48 -0
  161. package/src/components/date-time-picker/date-time-picker.css +177 -0
  162. package/src/components/date-time-picker/element.ts +47 -0
  163. package/src/components/date-time-picker/engine.ts +350 -0
  164. package/src/components/date-time-picker/react.tsx +54 -0
  165. package/src/components/dialog/README.md +103 -102
  166. package/src/components/dialog/element.ts +123 -118
  167. package/src/components/dialog/engine.ts +449 -432
  168. package/src/components/drawer/README.md +28 -0
  169. package/src/components/drawer/drawer.css +57 -0
  170. package/src/components/drawer/element.ts +21 -0
  171. package/src/components/drawer/engine.ts +56 -0
  172. package/src/components/drawer/react.tsx +85 -0
  173. package/src/components/editable-table/README.md +67 -0
  174. package/src/components/editable-table/editable-table.css +200 -0
  175. package/src/components/editable-table/element.ts +45 -0
  176. package/src/components/editable-table/engine.ts +775 -0
  177. package/src/components/editable-table/react.tsx +79 -0
  178. package/src/components/empty-state/README.md +41 -0
  179. package/src/components/empty-state/element.ts +23 -0
  180. package/src/components/empty-state/empty-state.css +60 -0
  181. package/src/components/empty-state/engine.ts +232 -0
  182. package/src/components/empty-state/react.tsx +34 -0
  183. package/src/components/file-explorer/README.md +67 -0
  184. package/src/components/file-explorer/element.ts +39 -0
  185. package/src/components/file-explorer/engine.ts +1307 -0
  186. package/src/components/file-explorer/file-explorer.css +632 -0
  187. package/src/components/file-explorer/react.tsx +77 -0
  188. package/src/components/filter-builder/README.md +47 -0
  189. package/src/components/filter-builder/element.ts +34 -0
  190. package/src/components/filter-builder/engine.ts +365 -0
  191. package/src/components/filter-builder/filter-builder.css +231 -0
  192. package/src/components/filter-builder/react.tsx +36 -0
  193. package/src/components/hold-confirm/README.md +13 -0
  194. package/src/components/hold-confirm/element.ts +24 -0
  195. package/src/components/hold-confirm/engine.ts +216 -0
  196. package/src/components/hold-confirm/hold-confirm.css +86 -0
  197. package/src/components/hold-confirm/react.tsx +28 -0
  198. package/src/components/hover-card/README.md +6 -2
  199. package/src/components/hover-card/engine.ts +68 -6
  200. package/src/components/hover-card/hover-card.css +13 -7
  201. package/src/components/image-preview/README.md +16 -0
  202. package/src/components/image-preview/element.ts +26 -0
  203. package/src/components/image-preview/engine.ts +325 -0
  204. package/src/components/image-preview/image-preview.css +194 -0
  205. package/src/components/image-preview/react.tsx +43 -0
  206. package/src/components/json-viewer/README.md +56 -0
  207. package/src/components/json-viewer/element.ts +37 -0
  208. package/src/components/json-viewer/engine.ts +440 -0
  209. package/src/components/json-viewer/json-viewer.css +271 -0
  210. package/src/components/json-viewer/react.tsx +56 -0
  211. package/src/components/like-button/README.md +13 -0
  212. package/src/components/like-button/element.ts +47 -0
  213. package/src/components/like-button/engine.ts +145 -0
  214. package/src/components/like-button/like-button.css +77 -0
  215. package/src/components/like-button/react.tsx +39 -0
  216. package/src/components/link-preview/README.md +19 -0
  217. package/src/components/link-preview/element.ts +29 -0
  218. package/src/components/link-preview/engine.ts +192 -0
  219. package/src/components/link-preview/link-preview.css +274 -0
  220. package/src/components/link-preview/react.tsx +101 -0
  221. package/src/components/loading-button/README.md +45 -0
  222. package/src/components/loading-button/element.ts +34 -0
  223. package/src/components/loading-button/engine.ts +201 -0
  224. package/src/components/loading-button/loading-button.css +76 -0
  225. package/src/components/loading-button/react.tsx +43 -0
  226. package/src/components/multi-select/README.md +48 -0
  227. package/src/components/multi-select/element.ts +28 -0
  228. package/src/components/multi-select/engine.ts +294 -0
  229. package/src/components/multi-select/multi-select.css +266 -0
  230. package/src/components/multi-select/react.tsx +39 -0
  231. package/src/components/notification-bell/README.md +14 -0
  232. package/src/components/notification-bell/element.ts +48 -0
  233. package/src/components/notification-bell/engine.ts +202 -0
  234. package/src/components/notification-bell/notification-bell.css +126 -0
  235. package/src/components/notification-bell/react.tsx +57 -0
  236. package/src/components/password-strength/README.md +17 -0
  237. package/src/components/password-strength/element.ts +22 -0
  238. package/src/components/password-strength/engine.ts +212 -0
  239. package/src/components/password-strength/password-strength.css +182 -0
  240. package/src/components/password-strength/react.tsx +36 -0
  241. package/src/components/password-strength/score.ts +62 -0
  242. package/src/components/popover/README.md +44 -0
  243. package/src/components/popover/element.ts +31 -0
  244. package/src/components/popover/engine.ts +114 -0
  245. package/src/components/popover/popover.css +24 -0
  246. package/src/components/popover/react.tsx +50 -0
  247. package/src/components/progress/README.md +17 -0
  248. package/src/components/progress/element.ts +30 -0
  249. package/src/components/progress/engine.ts +173 -0
  250. package/src/components/progress/progress.css +139 -0
  251. package/src/components/progress/react.tsx +35 -0
  252. package/src/components/questionnaire/README.md +32 -0
  253. package/src/components/questionnaire/element.ts +118 -0
  254. package/src/components/questionnaire/engine.ts +255 -0
  255. package/src/components/questionnaire/questionnaire.css +216 -0
  256. package/src/components/questionnaire/react.tsx +140 -0
  257. package/src/components/range-calendar/README.md +58 -0
  258. package/src/components/range-calendar/element.ts +42 -0
  259. package/src/components/range-calendar/engine.ts +390 -0
  260. package/src/components/range-calendar/range-calendar.css +185 -0
  261. package/src/components/range-calendar/react.tsx +56 -0
  262. package/src/components/range-slider/README.md +23 -0
  263. package/src/components/range-slider/element.ts +53 -0
  264. package/src/components/range-slider/engine.ts +316 -0
  265. package/src/components/range-slider/range-slider.css +191 -0
  266. package/src/components/range-slider/react.tsx +66 -0
  267. package/src/components/resizable-panel/README.md +61 -0
  268. package/src/components/resizable-panel/element.ts +36 -0
  269. package/src/components/resizable-panel/engine.ts +551 -0
  270. package/src/components/resizable-panel/react.tsx +74 -0
  271. package/src/components/resizable-panel/resizable-panel.css +242 -0
  272. package/src/components/rich-text-editor/README.md +57 -0
  273. package/src/components/rich-text-editor/element.ts +34 -0
  274. package/src/components/rich-text-editor/engine.ts +673 -0
  275. package/src/components/rich-text-editor/react.tsx +60 -0
  276. package/src/components/rich-text-editor/rich-text-editor.css +283 -0
  277. package/src/components/scratch-card/README.md +15 -0
  278. package/src/components/scratch-card/element.ts +30 -0
  279. package/src/components/scratch-card/engine.ts +257 -0
  280. package/src/components/scratch-card/react.tsx +40 -0
  281. package/src/components/scratch-card/scratch-card.css +74 -0
  282. package/src/components/skeleton/README.md +15 -0
  283. package/src/components/skeleton/element.ts +32 -0
  284. package/src/components/skeleton/engine.ts +104 -0
  285. package/src/components/skeleton/react.tsx +41 -0
  286. package/src/components/skeleton/skeleton.css +107 -0
  287. package/src/components/split-view/README.md +50 -0
  288. package/src/components/split-view/element.ts +34 -0
  289. package/src/components/split-view/engine.ts +114 -0
  290. package/src/components/split-view/react.tsx +63 -0
  291. package/src/components/split-view/split-view.css +18 -0
  292. package/src/components/star-rating/README.md +14 -0
  293. package/src/components/star-rating/element.ts +26 -0
  294. package/src/components/star-rating/engine.ts +217 -0
  295. package/src/components/star-rating/react.tsx +33 -0
  296. package/src/components/star-rating/star-rating.css +79 -0
  297. package/src/components/stat-card/README.md +42 -0
  298. package/src/components/stat-card/element.ts +40 -0
  299. package/src/components/stat-card/engine.ts +283 -0
  300. package/src/components/stat-card/react.tsx +36 -0
  301. package/src/components/stat-card/stat-card.css +163 -0
  302. package/src/components/swipe-actions/README.md +19 -0
  303. package/src/components/swipe-actions/element.ts +26 -0
  304. package/src/components/swipe-actions/engine.ts +260 -0
  305. package/src/components/swipe-actions/react.tsx +80 -0
  306. package/src/components/swipe-actions/swipe-actions.css +104 -0
  307. package/src/components/tag-input/README.md +42 -0
  308. package/src/components/tag-input/element.ts +39 -0
  309. package/src/components/tag-input/engine.ts +313 -0
  310. package/src/components/tag-input/react.tsx +54 -0
  311. package/src/components/tag-input/tag-input.css +243 -0
  312. package/src/components/time-field/README.md +54 -0
  313. package/src/components/time-field/element.ts +38 -0
  314. package/src/components/time-field/engine.ts +110 -0
  315. package/src/components/time-field/react.tsx +45 -0
  316. package/src/components/time-field/time-field.css +87 -0
  317. package/src/components/timeline/README.md +57 -0
  318. package/src/components/timeline/element.ts +31 -0
  319. package/src/components/timeline/engine.ts +300 -0
  320. package/src/components/timeline/react.tsx +59 -0
  321. package/src/components/timeline/timeline.css +218 -0
  322. package/src/components/tooltip/README.md +46 -0
  323. package/src/components/tooltip/element.ts +22 -0
  324. package/src/components/tooltip/engine.ts +288 -0
  325. package/src/components/tooltip/react.tsx +30 -0
  326. package/src/components/tooltip/tooltip.css +78 -0
  327. package/src/components/tree-view/README.md +61 -0
  328. package/src/components/tree-view/element.ts +32 -0
  329. package/src/components/tree-view/engine.ts +569 -0
  330. package/src/components/tree-view/react.tsx +78 -0
  331. package/src/components/tree-view/tree-view.css +209 -0
  332. package/src/components/virtual-list/README.md +81 -0
  333. package/src/components/virtual-list/element.ts +42 -0
  334. package/src/components/virtual-list/engine.ts +678 -0
  335. package/src/components/virtual-list/react.tsx +101 -0
  336. package/src/components/virtual-list/virtual-list.css +194 -0
  337. package/src/core/chips.ts +196 -0
  338. package/src/core/color.ts +304 -0
  339. package/src/core/date-range.ts +67 -0
  340. package/src/core/dates.ts +11 -1
  341. package/src/core/files.ts +107 -0
  342. package/src/core/filter.ts +229 -0
  343. package/src/core/gsap.ts +80 -65
  344. package/src/core/listbox.ts +332 -0
  345. package/src/core/mark.ts +29 -0
  346. package/src/core/position.ts +89 -0
  347. package/src/core/relative-time.ts +77 -0
  348. package/src/core/segment-field.ts +373 -0
  349. package/src/core/segments.ts +153 -0
  350. package/src/core/tree.ts +116 -0
  351. package/src/core/virtual.ts +79 -0
  352. package/src/css/tokens.css +186 -182
  353. package/src/elements/index.ts +147 -0
  354. package/src/index.ts +72 -0
  355. package/src/motion/micro.ts +248 -0
  356. package/src/motion/sheet.ts +88 -65
  357. package/src/motion/surface.ts +9 -0
  358. package/src/react/index.ts +57 -0
  359. package/dist/chunk-IKQ5FODN.js.map +0 -1
  360. package/dist/chunk-J6OCFNGU.js +0 -177
  361. package/dist/chunk-J6OCFNGU.js.map +0 -1
  362. package/dist/chunk-TFNRIZHU.js +0 -3977
  363. package/dist/chunk-TFNRIZHU.js.map +0 -1
  364. package/dist/chunk-XAKC2QY7.js.map +0 -1
  365. package/dist/reveal-jsX-uwUz.d.ts +0 -625
@@ -0,0 +1,673 @@
1
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import { press, shake } from '../../motion/micro';
6
+
7
+ /*
8
+ * Rich text editor: a writing area with a toolbar for bold, italic,
9
+ * underline, strikethrough, headings, lists, quotes, code and links. Buttons
10
+ * press with a bounce, and the formats under the caret light up as it moves
11
+ * (their pill grows in). Shortcuts: Ctrl/⌘+B, I, U, K, Shift+X (strikethrough),
12
+ * Ctrl/⌘+Z and Y. Markdown as you type: "# " and "## " make headings, "- " or
13
+ * "* " a list, "1. " a numbered one, "> " a quote, **bold** and `code` inline.
14
+ * Pasted text keeps only safe formatting (no styles, classes or scripts). The
15
+ * link editor grows out of its button; the counter turns red and the area
16
+ * shakes once when it passes maxLength. A hidden field carries the HTML for
17
+ * forms; the placeholder fades.
18
+ */
19
+
20
+ export type RichCommand = 'bold' | 'italic' | 'underline' | 'strike' | 'h2' | 'h3' | 'ul' | 'ol' | 'quote' | 'code' | 'link' | 'clear' | 'undo' | 'redo';
21
+
22
+ export interface RichTextEditorOptions {
23
+ /** The first content, as HTML (cleaned like a paste). */
24
+ value?: string;
25
+ placeholder?: string;
26
+ /** The buttons, in order; '|' separates groups (default: all). */
27
+ toolbar?: Array<RichCommand | '|'>;
28
+ /** Characters before the counter turns red (the text can go on). */
29
+ maxLength?: number;
30
+ /** 'words' (default), 'characters', or false. */
31
+ counter?: 'words' | 'characters' | false;
32
+ /** A hidden field with this name carries the HTML. */
33
+ name?: string;
34
+ /** Accessible name of the writing area (default 'Editor de texto'). */
35
+ label?: string;
36
+ readOnly?: boolean;
37
+ /** Markdown shortcuts while typing (default true). */
38
+ markdown?: boolean;
39
+ onChange?: (html: string, text: string) => void;
40
+ }
41
+
42
+ export interface RichTextEditorInstance extends Instance {
43
+ setValue(html: string): void;
44
+ focus(): void;
45
+ exec(command: RichCommand, value?: string): void;
46
+ readonly value: string;
47
+ readonly text: string;
48
+ readonly element: HTMLElement;
49
+ }
50
+
51
+ const SVG = (paths: string) =>
52
+ `<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${paths}</svg>`;
53
+ const BUTTONS: Record<RichCommand, { label: string; keys?: string; icon: string; toggle?: boolean }> = {
54
+ bold: { label: 'Negrita', keys: 'Ctrl+B', toggle: true, icon: SVG('<path d="M7 5h6a3.5 3.5 0 0 1 0 7H7zM7 12h7a3.5 3.5 0 0 1 0 7H7z"/>') },
55
+ italic: { label: 'Cursiva', keys: 'Ctrl+I', toggle: true, icon: SVG('<path d="M14 5h-4M14 19h-4M14 5l-4 14"/>') },
56
+ underline: { label: 'Subrayado', keys: 'Ctrl+U', toggle: true, icon: SVG('<path d="M7 4v7a5 5 0 0 0 10 0V4M5 20h14"/>') },
57
+ strike: { label: 'Tachado', keys: 'Ctrl+Mayús+X', toggle: true, icon: SVG('<path d="M5 12h14M16 6.5A4 4 0 0 0 12.5 5h-1A3.5 3.5 0 0 0 8 8.5c0 1.5 1 2.6 2.6 3.1M8 17.5A4 4 0 0 0 11.5 19h1a3.5 3.5 0 0 0 3.5-3.5"/>') },
58
+ h2: { label: 'Título', toggle: true, icon: SVG('<path d="M4 6v12M12 6v12M4 12h8M16 10l3-2v10"/>') },
59
+ h3: { label: 'Subtítulo', toggle: true, icon: SVG('<path d="M4 6v12M12 6v12M4 12h8M16 8.5a2.5 2.5 0 1 1 2.5 3.5 2.5 2.5 0 1 1-2.5 3.5"/>') },
60
+ ul: { label: 'Lista', toggle: true, icon: SVG('<path d="M9 6h11M9 12h11M9 18h11M4.5 6h.01M4.5 12h.01M4.5 18h.01"/>') },
61
+ ol: { label: 'Lista numerada', toggle: true, icon: SVG('<path d="M10 6h10M10 12h10M10 18h10M4 5l1.5-1v5M3.5 13.5a1.5 1.5 0 0 1 3 .5c0 1-3 2-3 3h3"/>') },
62
+ quote: { label: 'Cita', toggle: true, icon: SVG('<path d="M7 7h4v6a4 4 0 0 1-4 4M15 7h4v6a4 4 0 0 1-4 4"/>') },
63
+ code: { label: 'Código', toggle: true, icon: SVG('<path d="M9 8l-4 4 4 4M15 8l4 4-4 4"/>') },
64
+ link: { label: 'Enlace', keys: 'Ctrl+K', toggle: true, icon: SVG('<path d="M10 14a4 4 0 0 0 5.66 0l3-3a4 4 0 0 0-5.66-5.66l-1 1M14 10a4 4 0 0 0-5.66 0l-3 3a4 4 0 0 0 5.66 5.66l1-1"/>') },
65
+ clear: { label: 'Quitar formato', icon: SVG('<path d="M6 5h12M12 5l-3 14M15.5 15.5l4 4M19.5 15.5l-4 4"/>') },
66
+ undo: { label: 'Deshacer', keys: 'Ctrl+Z', icon: SVG('<path d="M9 7L4 12l5 5M4 12h11a5 5 0 0 1 0 10h-2"/>') },
67
+ redo: { label: 'Rehacer', keys: 'Ctrl+Y', icon: SVG('<path d="M15 7l5 5-5 5M20 12H9a5 5 0 0 0 0 10h2"/>') },
68
+ };
69
+ const DEFAULT_TOOLBAR: Array<RichCommand | '|'> = ['bold', 'italic', 'underline', 'strike', '|', 'h2', 'h3', '|', 'ul', 'ol', 'quote', 'code', '|', 'link', 'clear', '|', 'undo', 'redo'];
70
+ const ZWSP = '​';
71
+
72
+ // ── Cleaning HTML ──────────────────────────────────────────────────────────
73
+ const KEEP: Record<string, string> = {
74
+ p: 'p', div: 'p', br: 'br', strong: 'strong', b: 'strong', em: 'em', i: 'em', u: 'u', s: 's', strike: 's', del: 's',
75
+ h1: 'h2', h2: 'h2', h3: 'h3', h4: 'h3', h5: 'h3', h6: 'h3', ul: 'ul', ol: 'ol', li: 'li', blockquote: 'blockquote', code: 'code', pre: 'pre', a: 'a',
76
+ };
77
+ const DROP = new Set(['script', 'style', 'meta', 'link', 'iframe', 'object', 'embed', 'template', 'noscript', 'svg', 'math', 'head', 'title', 'form', 'input', 'button', 'select', 'textarea']);
78
+ const SAFE_URL = /^(https?:|mailto:|tel:|#|\/|\.{0,2}\/)/i;
79
+
80
+ /** HTML with only safe formatting: known tags (renamed to their kind), links with safe addresses, no attributes else. */
81
+ export function sanitizeHtml(html: string): string {
82
+ if (typeof DOMParser === 'undefined') return '';
83
+ const source = new DOMParser().parseFromString(`<body>${html}</body>`, 'text/html').body;
84
+ const out = document.createElement('div');
85
+ const copy = (from: Node, into: Node) => {
86
+ for (const node of from.childNodes) {
87
+ if (node.nodeType === Node.TEXT_NODE) {
88
+ into.appendChild(document.createTextNode((node.textContent ?? '').replace(new RegExp(ZWSP, 'g'), '')));
89
+ continue;
90
+ }
91
+ if (node.nodeType !== Node.ELEMENT_NODE) continue;
92
+ const element = node as Element;
93
+ const tag = element.tagName.toLowerCase();
94
+ if (DROP.has(tag)) continue;
95
+ const kind = KEEP[tag];
96
+ // Unknown wrappers (span, font, section…) give their content.
97
+ if (!kind) {
98
+ copy(element, into);
99
+ continue;
100
+ }
101
+ const made = document.createElement(kind);
102
+ if (kind === 'a') {
103
+ const href = element.getAttribute('href')?.trim() ?? '';
104
+ if (!SAFE_URL.test(href)) {
105
+ copy(element, into);
106
+ continue;
107
+ }
108
+ made.setAttribute('href', href);
109
+ if (/^https?:/i.test(href)) {
110
+ made.setAttribute('target', '_blank');
111
+ made.setAttribute('rel', 'noopener noreferrer');
112
+ }
113
+ }
114
+ copy(element, made);
115
+ into.appendChild(made);
116
+ }
117
+ };
118
+ copy(source, out);
119
+ return out.innerHTML;
120
+ }
121
+
122
+ const escapeText = (text: string) => {
123
+ const holder = document.createElement('div');
124
+ holder.textContent = text;
125
+ return holder.innerHTML;
126
+ };
127
+ /** Plain text as paragraphs (blank lines) and line breaks. */
128
+ const textToHtml = (text: string) =>
129
+ text
130
+ .replace(/\r\n?/g, '\n')
131
+ .split(/\n{2,}/)
132
+ .map((paragraph) => `<p>${escapeText(paragraph).replace(/\n/g, '<br>')}</p>`)
133
+ .join('');
134
+
135
+ export function createRichTextEditor(root: HTMLElement | null, options: RichTextEditorOptions = {}): RichTextEditorInstance {
136
+ if (!root) return inert({ setValue() {}, focus() {}, exec() {}, value: '', text: '', element: null as unknown as HTMLElement });
137
+ registerMotion();
138
+ const scope = createScope();
139
+ const markdown = options.markdown !== false;
140
+ const counterMode = options.counter === false ? null : (options.counter ?? (options.maxLength ? 'characters' : 'words'));
141
+ const effects = new Set<gsap.core.Animation>();
142
+ const keep = (animation: gsap.core.Animation | null) => {
143
+ if (!animation) return;
144
+ effects.add(animation);
145
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
146
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
147
+ };
148
+ // What was written inside the root is the first content (unless `value` is given).
149
+ const authored = root.innerHTML;
150
+ const holder = document.createElement('div');
151
+ holder.hidden = true;
152
+ holder.append(...root.childNodes);
153
+
154
+ // ── The parts ────────────────────────────────────────────────────────────
155
+ const toolbar = document.createElement('div');
156
+ toolbar.dataset.mkRteToolbar = '';
157
+ toolbar.setAttribute('role', 'toolbar');
158
+ toolbar.setAttribute('aria-label', 'Formato');
159
+ const buttons = new Map<RichCommand, HTMLButtonElement>();
160
+ let group = document.createElement('div');
161
+ group.dataset.mkRteGroup = '';
162
+ toolbar.append(group);
163
+ for (const entry of options.toolbar ?? DEFAULT_TOOLBAR) {
164
+ if (entry === '|') {
165
+ group = document.createElement('div');
166
+ group.dataset.mkRteGroup = '';
167
+ toolbar.append(group);
168
+ continue;
169
+ }
170
+ const spec = BUTTONS[entry];
171
+ if (!spec) continue;
172
+ const button = document.createElement('button');
173
+ button.type = 'button';
174
+ button.dataset.command = entry;
175
+ const label = spec.keys ? `${spec.label} (${spec.keys})` : spec.label;
176
+ button.setAttribute('aria-label', label);
177
+ button.title = label;
178
+ if (spec.toggle) button.setAttribute('aria-pressed', 'false');
179
+ button.innerHTML = spec.icon;
180
+ button.tabIndex = buttons.size ? -1 : 0;
181
+ group.append(button);
182
+ buttons.set(entry, button);
183
+ }
184
+ const content = document.createElement('div');
185
+ content.dataset.mkRteContent = '';
186
+ content.contentEditable = options.readOnly ? 'false' : 'true';
187
+ content.setAttribute('role', 'textbox');
188
+ content.setAttribute('aria-multiline', 'true');
189
+ content.setAttribute('aria-label', options.label ?? 'Editor de texto');
190
+ content.dataset.placeholder = options.placeholder ?? 'Escribe algo…';
191
+ content.spellcheck = true;
192
+ if (options.readOnly) content.setAttribute('aria-readonly', 'true');
193
+ const foot = document.createElement('div');
194
+ foot.dataset.mkRteFoot = '';
195
+ const counter = document.createElement('span');
196
+ counter.dataset.mkRteCounter = '';
197
+ counter.setAttribute('aria-live', 'polite');
198
+ foot.append(counter);
199
+ foot.hidden = !counterMode;
200
+ let hidden: HTMLInputElement | null = null;
201
+ if (options.name) {
202
+ hidden = document.createElement('input');
203
+ hidden.type = 'hidden';
204
+ hidden.name = options.name;
205
+ foot.append(hidden);
206
+ }
207
+ // The link editor.
208
+ const linkBox = document.createElement('div');
209
+ linkBox.dataset.mkRteLink = '';
210
+ linkBox.hidden = true;
211
+ linkBox.setAttribute('role', 'group');
212
+ linkBox.setAttribute('aria-label', 'Enlace');
213
+ const linkInput = document.createElement('input');
214
+ linkInput.type = 'url';
215
+ linkInput.placeholder = 'https://…';
216
+ linkInput.setAttribute('aria-label', 'Dirección del enlace');
217
+ const linkApply = document.createElement('button');
218
+ linkApply.type = 'button';
219
+ linkApply.dataset.mkRteLinkApply = '';
220
+ linkApply.textContent = 'Aplicar';
221
+ const linkRemove = document.createElement('button');
222
+ linkRemove.type = 'button';
223
+ linkRemove.dataset.mkRteLinkRemove = '';
224
+ linkRemove.textContent = 'Quitar';
225
+ linkBox.append(linkInput, linkApply, linkRemove);
226
+ const status = document.createElement('span');
227
+ status.className = 'mk-sr-only';
228
+ status.setAttribute('role', 'status');
229
+ if (!options.readOnly) root.append(toolbar);
230
+ root.append(content, linkBox, foot, status);
231
+ scope.attr(root, 'data-mk-ready', '');
232
+ if (options.readOnly) scope.attr(root, 'data-mk-readonly', '');
233
+ scope.add(() => {
234
+ for (const effect of effects) effect.kill();
235
+ effects.clear();
236
+ for (const part of [toolbar, content, linkBox, foot, status]) part.remove();
237
+ root.append(...holder.childNodes);
238
+ });
239
+
240
+ const run = (name: string, value?: string) => {
241
+ if (typeof document.execCommand !== 'function') return false;
242
+ try {
243
+ return document.execCommand(name, false, value);
244
+ } catch {
245
+ return false;
246
+ }
247
+ };
248
+ try {
249
+ document.execCommand?.('defaultParagraphSeparator', false, 'p');
250
+ } catch {
251
+ // Older engines: their default separator stands.
252
+ }
253
+
254
+ // ── Value ────────────────────────────────────────────────────────────────
255
+ const cleanValue = () => sanitizeHtml(content.innerHTML).replace(/<p><br><\/p>$/, '').replace(/^<p><\/p>$/, '');
256
+ const textValue = () => (content.innerText ?? content.textContent ?? '').replace(new RegExp(ZWSP, 'g'), '').replace(/\n{3,}/g, '\n\n').trim();
257
+ const BLOCKS = new Set(['P', 'H2', 'H3', 'UL', 'OL', 'BLOCKQUOTE', 'PRE']);
258
+ const setContent = (html: string) => {
259
+ const holder = document.createElement('div');
260
+ holder.innerHTML = sanitizeHtml(html);
261
+ // Blocks stay; runs of loose text and inline formatting at the top go in paragraphs; blank space between blocks goes.
262
+ const out: Node[] = [];
263
+ let paragraph: HTMLParagraphElement | null = null;
264
+ for (const node of [...holder.childNodes]) {
265
+ if (node.nodeType === Node.ELEMENT_NODE && BLOCKS.has((node as Element).tagName)) {
266
+ paragraph = null;
267
+ out.push(node);
268
+ continue;
269
+ }
270
+ if (node.nodeType === Node.TEXT_NODE && !node.textContent?.trim() && !paragraph) continue;
271
+ if (!paragraph) {
272
+ paragraph = document.createElement('p');
273
+ out.push(paragraph);
274
+ }
275
+ paragraph.append(node);
276
+ }
277
+ content.replaceChildren(...out);
278
+ if (!content.firstChild) content.innerHTML = '<p><br></p>';
279
+ };
280
+ setContent(options.value ?? authored);
281
+
282
+ let over = false;
283
+ let lastValue = '';
284
+ const paint = (notify: boolean) => {
285
+ const text = textValue();
286
+ content.toggleAttribute('data-empty', !text && !content.querySelector('li, img'));
287
+ if (counterMode) {
288
+ const characters = [...text].length;
289
+ const words = text ? text.split(/\s+/).filter(Boolean).length : 0;
290
+ if (options.maxLength) {
291
+ counter.textContent = `${characters} / ${options.maxLength}`;
292
+ const now = characters > options.maxLength;
293
+ if (now && !over) {
294
+ keep(shake(content, 3));
295
+ status.textContent = `Pasaste el límite de ${options.maxLength} caracteres`;
296
+ }
297
+ over = now;
298
+ root.toggleAttribute('data-mk-over', now);
299
+ content.setAttribute('aria-invalid', String(now));
300
+ } else counter.textContent = counterMode === 'words' ? `${words} ${words === 1 ? 'palabra' : 'palabras'}` : `${characters} ${characters === 1 ? 'carácter' : 'caracteres'}`;
301
+ }
302
+ const html = cleanValue();
303
+ if (hidden) hidden.value = html;
304
+ if (notify && html !== lastValue) {
305
+ lastValue = html;
306
+ options.onChange?.(html, text);
307
+ root.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { html, text } }));
308
+ } else lastValue = html;
309
+ };
310
+ paint(false);
311
+
312
+ // ── Where the caret is ───────────────────────────────────────────────────
313
+ const selectionInside = () => {
314
+ const selection = window.getSelection();
315
+ if (!selection || !selection.rangeCount) return null;
316
+ const range = selection.getRangeAt(0);
317
+ return content.contains(range.commonAncestorContainer) ? range : null;
318
+ };
319
+ const closest = (node: Node | null, selector: string) => {
320
+ const element = node?.nodeType === Node.ELEMENT_NODE ? (node as Element) : (node?.parentElement ?? null);
321
+ const found = element?.closest(selector);
322
+ return found && content.contains(found) ? (found as HTMLElement) : null;
323
+ };
324
+ const block = (node: Node | null) => closest(node, 'p, h2, h3, li, blockquote, pre');
325
+ const states = (): Partial<Record<RichCommand, boolean>> => {
326
+ const range = selectionInside();
327
+ if (!range) return {};
328
+ const node = range.startContainer;
329
+ const query = (name: string) => {
330
+ try {
331
+ return Boolean(document.queryCommandState?.(name));
332
+ } catch {
333
+ return false;
334
+ }
335
+ };
336
+ return {
337
+ bold: query('bold') || Boolean(closest(node, 'strong, b')),
338
+ italic: query('italic') || Boolean(closest(node, 'em, i')),
339
+ underline: query('underline') || Boolean(closest(node, 'u')),
340
+ strike: query('strikeThrough') || Boolean(closest(node, 's, strike, del')),
341
+ h2: Boolean(closest(node, 'h2')),
342
+ h3: Boolean(closest(node, 'h3')),
343
+ ul: Boolean(closest(node, 'ul')),
344
+ ol: Boolean(closest(node, 'ol')),
345
+ quote: Boolean(closest(node, 'blockquote')),
346
+ code: Boolean(closest(node, 'code')),
347
+ link: Boolean(closest(node, 'a')),
348
+ };
349
+ };
350
+ const paintToolbar = () => {
351
+ const now = states();
352
+ for (const [command, button] of buttons) {
353
+ if (!button.hasAttribute('aria-pressed')) continue;
354
+ button.setAttribute('aria-pressed', String(Boolean(now[command])));
355
+ }
356
+ };
357
+ scope.on(document, 'selectionchange', () => {
358
+ if (selectionInside()) paintToolbar();
359
+ });
360
+
361
+ // ── Commands ─────────────────────────────────────────────────────────────
362
+ const toggleBlock = (tag: 'h2' | 'h3' | 'blockquote') => {
363
+ const range = selectionInside();
364
+ const inside = range ? closest(range.startContainer, tag) : null;
365
+ run('formatBlock', inside ? '<p>' : `<${tag}>`);
366
+ };
367
+ const toggleCode = () => {
368
+ const range = selectionInside();
369
+ if (!range) return;
370
+ const code = closest(range.startContainer, 'code');
371
+ if (code) {
372
+ // Out of code: its text stays, plain.
373
+ code.replaceWith(...code.childNodes);
374
+ return;
375
+ }
376
+ if (range.collapsed) return;
377
+ const element = document.createElement('code');
378
+ element.textContent = range.toString();
379
+ range.deleteContents();
380
+ range.insertNode(element);
381
+ const after = document.createTextNode(ZWSP);
382
+ element.after(after);
383
+ placeCaret(after, 1);
384
+ };
385
+ const placeCaret = (node: Node, offset: number) => {
386
+ const selection = window.getSelection();
387
+ if (!selection) return;
388
+ const range = document.createRange();
389
+ range.setStart(node, offset);
390
+ range.collapse(true);
391
+ selection.removeAllRanges();
392
+ selection.addRange(range);
393
+ };
394
+
395
+ let savedRange: Range | null = null;
396
+ const openLink = () => {
397
+ const range = selectionInside();
398
+ savedRange = range ? range.cloneRange() : null;
399
+ const current = range ? closest(range.startContainer, 'a') : null;
400
+ linkInput.value = current?.getAttribute('href') ?? '';
401
+ linkRemove.hidden = !current;
402
+ linkBox.hidden = false;
403
+ // Under its button, inside the editor.
404
+ const button = buttons.get('link');
405
+ const rootBox = root.getBoundingClientRect();
406
+ const anchor = button?.getBoundingClientRect();
407
+ if (anchor) {
408
+ linkBox.style.top = `${anchor.bottom - rootBox.top + 6}px`;
409
+ linkBox.style.left = `${Math.max(0, Math.min(anchor.left - rootBox.left, rootBox.width - linkBox.offsetWidth))}px`;
410
+ }
411
+ if (!reduced()) keep(gsap.fromTo(linkBox, { opacity: 0, scale: 0.92, y: -6, filter: 'blur(6px)' }, { opacity: 1, scale: 1, y: 0, filter: 'blur(0px)', duration: 0.28, ease: EASE.pop, transformOrigin: '0% 0%', clearProps: 'opacity,transform,filter' }));
412
+ linkInput.focus();
413
+ linkInput.select();
414
+ };
415
+ const closeLink = (focusBack = true) => {
416
+ if (linkBox.hidden) return;
417
+ const finish = () => {
418
+ linkBox.hidden = true;
419
+ gsap.set(linkBox, { clearProps: 'opacity,transform,filter' });
420
+ };
421
+ if (reduced()) finish();
422
+ else keep(gsap.to(linkBox, { opacity: 0, scale: 0.95, duration: 0.16, ease: 'power1.in', onComplete: finish }));
423
+ if (focusBack) {
424
+ content.focus({ preventScroll: true });
425
+ if (savedRange) {
426
+ const selection = window.getSelection();
427
+ selection?.removeAllRanges();
428
+ selection?.addRange(savedRange);
429
+ }
430
+ }
431
+ };
432
+ const applyLink = () => {
433
+ const href = linkInput.value.trim();
434
+ if (href && !SAFE_URL.test(href) && !/^[\w.-]+\.[a-z]{2,}/i.test(href)) {
435
+ keep(shake(linkInput));
436
+ status.textContent = 'La dirección no es válida';
437
+ return;
438
+ }
439
+ const url = href && !SAFE_URL.test(href) ? `https://${href}` : href;
440
+ closeLink();
441
+ if (!url) return;
442
+ run('createLink', url);
443
+ // External links open apart and safely.
444
+ if (/^https?:/i.test(url)) {
445
+ for (const anchor of content.querySelectorAll<HTMLAnchorElement>('a[href]')) {
446
+ if (anchor.getAttribute('href') !== url) continue;
447
+ anchor.target = '_blank';
448
+ anchor.rel = 'noopener noreferrer';
449
+ }
450
+ }
451
+ status.textContent = 'Enlace aplicado';
452
+ paint(true);
453
+ };
454
+ const removeLink = () => {
455
+ closeLink();
456
+ run('unlink');
457
+ status.textContent = 'Enlace quitado';
458
+ paint(true);
459
+ };
460
+
461
+ const exec = (command: RichCommand, value?: string) => {
462
+ if (options.readOnly) return;
463
+ if (command !== 'link' && !selectionInside()) content.focus({ preventScroll: true });
464
+ switch (command) {
465
+ case 'bold':
466
+ case 'italic':
467
+ case 'underline':
468
+ run(command);
469
+ break;
470
+ case 'strike':
471
+ run('strikeThrough');
472
+ break;
473
+ case 'h2':
474
+ case 'h3':
475
+ toggleBlock(command);
476
+ break;
477
+ case 'quote':
478
+ toggleBlock('blockquote');
479
+ break;
480
+ case 'ul':
481
+ run('insertUnorderedList');
482
+ break;
483
+ case 'ol':
484
+ run('insertOrderedList');
485
+ break;
486
+ case 'code':
487
+ toggleCode();
488
+ break;
489
+ case 'link':
490
+ if (value !== undefined) {
491
+ linkInput.value = value;
492
+ savedRange = selectionInside()?.cloneRange() ?? null;
493
+ applyLink();
494
+ } else openLink();
495
+ return;
496
+ case 'clear':
497
+ run('removeFormat');
498
+ run('formatBlock', '<p>');
499
+ break;
500
+ case 'undo':
501
+ case 'redo':
502
+ run(command);
503
+ break;
504
+ }
505
+ paintToolbar();
506
+ paint(true);
507
+ };
508
+
509
+ // ── Toolbar: pointer and keyboard ────────────────────────────────────────
510
+ // The editor keeps its selection while a button is pressed.
511
+ scope.on(toolbar, 'mousedown', (event) => {
512
+ if ((event.target as Element).closest('button')) event.preventDefault();
513
+ });
514
+ scope.on(toolbar, 'click', (event) => {
515
+ const button = (event.target as Element | null)?.closest<HTMLButtonElement>('button[data-command]');
516
+ if (!button) return;
517
+ keep(press(button));
518
+ exec(button.dataset.command as RichCommand);
519
+ });
520
+ scope.on(toolbar, 'keydown', (event) => {
521
+ const key = (event as KeyboardEvent).key;
522
+ const list = [...buttons.values()];
523
+ const at = list.indexOf(document.activeElement as HTMLButtonElement);
524
+ const moves: Record<string, number> = { ArrowRight: at + 1, ArrowLeft: at - 1, Home: 0, End: list.length - 1 };
525
+ if (!(key in moves) || at < 0) return;
526
+ event.preventDefault();
527
+ const next = list[(moves[key]! + list.length) % list.length]!;
528
+ for (const button of list) button.tabIndex = button === next ? 0 : -1;
529
+ next.focus();
530
+ });
531
+
532
+ // ── Writing ──────────────────────────────────────────────────────────────
533
+ scope.on(content, 'keydown', (event) => {
534
+ const key = event as KeyboardEvent;
535
+ if (!(key.ctrlKey || key.metaKey) || key.altKey) return;
536
+ const letter = key.key.toLowerCase();
537
+ const shortcuts: Record<string, RichCommand> = { b: 'bold', i: 'italic', u: 'underline', k: 'link' };
538
+ let command: RichCommand | undefined = shortcuts[letter];
539
+ if (key.shiftKey && letter === 'x') command = 'strike';
540
+ if (!command || key.shiftKey !== (command === 'strike')) return;
541
+ key.preventDefault();
542
+ const button = buttons.get(command);
543
+ if (button) keep(press(button));
544
+ exec(command);
545
+ });
546
+
547
+ /** Markdown at the start of a block ("# ", "- ", "1. ", "> ") and inline (**bold**, `code`). */
548
+ const autoformat = (typed: string) => {
549
+ const range = selectionInside();
550
+ if (!range || !range.collapsed) return;
551
+ const node = range.startContainer;
552
+ if (typed === ' ') {
553
+ const current = block(node);
554
+ if (!current || current.tagName === 'LI' || current.tagName === 'PRE') return;
555
+ // The text from the start of the block to the caret.
556
+ const before = document.createRange();
557
+ before.selectNodeContents(current);
558
+ before.setEnd(range.startContainer, range.startOffset);
559
+ const text = before.toString().replace(new RegExp(ZWSP, 'g'), '');
560
+ const rules: Array<[RegExp, () => void]> = [
561
+ [/^#\s$/, () => run('formatBlock', '<h2>')],
562
+ [/^##\s$/, () => run('formatBlock', '<h3>')],
563
+ [/^[-*]\s$/, () => run('insertUnorderedList')],
564
+ [/^1[.)]\s$/, () => run('insertOrderedList')],
565
+ [/^>\s$/, () => run('formatBlock', '<blockquote>')],
566
+ ];
567
+ for (const [pattern, apply] of rules) {
568
+ if (!pattern.test(text)) continue;
569
+ // The marker goes; the format stays.
570
+ before.deleteContents();
571
+ apply();
572
+ current.normalize();
573
+ return true;
574
+ }
575
+ return;
576
+ }
577
+ if ((typed === '*' || typed === '`') && node.nodeType === Node.TEXT_NODE) {
578
+ const text = (node.textContent ?? '').slice(0, range.startOffset);
579
+ const match = typed === '*' ? /\*\*([^*]+)\*\*$/.exec(text) : /`([^`]+)`$/.exec(text);
580
+ if (!match) return;
581
+ const start = range.startOffset - match[0].length;
582
+ const span = document.createRange();
583
+ span.setStart(node, start);
584
+ span.setEnd(node, range.startOffset);
585
+ span.deleteContents();
586
+ const element = document.createElement(typed === '*' ? 'strong' : 'code');
587
+ element.textContent = match[1]!;
588
+ span.insertNode(element);
589
+ // The caret goes after it, outside of it.
590
+ const after = document.createTextNode(ZWSP);
591
+ element.after(after);
592
+ placeCaret(after, 1);
593
+ // A soft highlight says what just changed.
594
+ if (!reduced()) {
595
+ element.setAttribute('data-mk-rte-fresh', '');
596
+ scope.timeout(() => element.removeAttribute('data-mk-rte-fresh'), 1000);
597
+ }
598
+ return true;
599
+ }
600
+ return;
601
+ };
602
+
603
+ scope.on(content, 'input', (event) => {
604
+ const input = event as InputEvent;
605
+ if (markdown && input.inputType === 'insertText' && input.data && autoformat(input.data.slice(-1))) {
606
+ const fresh = selectionInside() ? block(selectionInside()!.startContainer) : null;
607
+ if (fresh && !reduced()) keep(gsap.fromTo(fresh, { opacity: 0.4, filter: 'blur(4px)' }, { opacity: 1, filter: 'blur(0px)', duration: 0.3, ease: 'power1.out', clearProps: 'opacity,filter' }));
608
+ status.textContent = 'Formato aplicado';
609
+ }
610
+ // An emptied area keeps a paragraph to write in.
611
+ if (!content.firstElementChild) content.innerHTML = '<p><br></p>';
612
+ paint(true);
613
+ paintToolbar();
614
+ });
615
+
616
+ scope.on(content, 'paste', (event) => {
617
+ if (options.readOnly) return;
618
+ const clipboard = (event as ClipboardEvent).clipboardData;
619
+ if (!clipboard) return;
620
+ event.preventDefault();
621
+ const html = clipboard.getData('text/html');
622
+ const clean = html ? sanitizeHtml(html) : textToHtml(clipboard.getData('text/plain'));
623
+ if (!run('insertHTML', clean)) {
624
+ // No insertHTML: put it in by hand.
625
+ const range = selectionInside();
626
+ if (range) {
627
+ range.deleteContents();
628
+ const holder = document.createElement('div');
629
+ holder.innerHTML = clean;
630
+ const fragment = document.createDocumentFragment();
631
+ fragment.append(...holder.childNodes);
632
+ range.insertNode(fragment);
633
+ }
634
+ }
635
+ paint(true);
636
+ });
637
+
638
+ scope.on(linkInput, 'keydown', (event) => {
639
+ const key = (event as KeyboardEvent).key;
640
+ if (key === 'Enter') {
641
+ event.preventDefault();
642
+ applyLink();
643
+ } else if (key === 'Escape') {
644
+ event.preventDefault();
645
+ closeLink();
646
+ }
647
+ });
648
+ scope.on(linkApply, 'click', applyLink);
649
+ scope.on(linkRemove, 'click', removeLink);
650
+ scope.on(document, 'pointerdown', (event) => {
651
+ if (!linkBox.hidden && !linkBox.contains(event.target as Node) && !buttons.get('link')?.contains(event.target as Node)) closeLink(false);
652
+ });
653
+ scope.on(content, 'focus', () => root.setAttribute('data-mk-focused', ''));
654
+ scope.on(content, 'blur', () => root.removeAttribute('data-mk-focused'));
655
+
656
+ return {
657
+ setValue(html) {
658
+ if (scope.disposed) return;
659
+ setContent(html);
660
+ paint(false);
661
+ },
662
+ focus: () => content.focus(),
663
+ exec,
664
+ get value() {
665
+ return cleanValue();
666
+ },
667
+ get text() {
668
+ return textValue();
669
+ },
670
+ element: content,
671
+ destroy: () => scope.dispose(),
672
+ };
673
+ }