@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,36 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import type { FilterGroup } from '../../core/filter';
5
+ import { createFilterBuilder, type FilterBuilderInstance, type FilterBuilderOptions } from './engine';
6
+
7
+ export interface FilterBuilderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'defaultValue'>, FilterBuilderOptions {
8
+ /** Controlled filter; without it the builder keeps its own (defaultValue). */
9
+ value?: FilterGroup;
10
+ defaultValue?: FilterGroup;
11
+ }
12
+
13
+ /** <FilterBuilder fields={fields} value={filter} onChange={setFilter} />. */
14
+ export function FilterBuilder({ fields, value, defaultValue, maxDepth, label, summary, onChange, className, ...props }: FilterBuilderProps) {
15
+ const ref = useRef<HTMLDivElement>(null);
16
+ const instance = useRef<FilterBuilderInstance | null>(null);
17
+ const change = useLatest(onChange);
18
+ const start = useRef(value ?? defaultValue);
19
+ const fieldsKey = JSON.stringify(fields);
20
+ useIsomorphicLayoutEffect(() => {
21
+ const created = createFilterBuilder(ref.current, { fields, value: value ?? start.current, maxDepth, label, summary, onChange: (next) => change.current?.(next) });
22
+ instance.current = created;
23
+ return () => {
24
+ start.current = created.value;
25
+ created.destroy();
26
+ instance.current = null;
27
+ };
28
+ // eslint-disable-next-line react-hooks/exhaustive-deps
29
+ }, [fieldsKey, maxDepth, label, summary]);
30
+ const valueKey = JSON.stringify(value ?? null);
31
+ useIsomorphicLayoutEffect(() => {
32
+ if (value && instance.current && JSON.stringify(instance.current.value) !== valueKey) instance.current.setValue(value);
33
+ // eslint-disable-next-line react-hooks/exhaustive-deps
34
+ }, [valueKey]);
35
+ return <div {...props} ref={ref} className={['mk-filter-builder', className].filter(Boolean).join(' ')} />;
36
+ }
@@ -0,0 +1,13 @@
1
+ # hold-confirm
2
+
3
+ Confirmación por pulsación mantenida sobre el mismo botón: mientras se mantiene (puntero, Espacio o Enter) un relleno lo cruza durante `duration`, el botón se comprime un poco y la etiqueta cambia con blur. Soltar antes devuelve el relleno con resorte. Al completar: rebote de pulsación (curva click de Melonmind), un ✓ que se dibuja, partículas, `onConfirm` / `mk:confirm` y vuelta al inicio tras `resetAfter`.
4
+
5
+ ```html
6
+ <mk-hold-confirm class="mk-hold-confirm"><button>Eliminar cuenta</button></mk-hold-confirm>
7
+ ```
8
+
9
+ `defineElements()`, `createHoldConfirm(root, options)` (root es el botón o lo contiene) o `<HoldConfirm onConfirm={remove}>Eliminar cuenta</HoldConfirm>`.
10
+
11
+ Opciones: `duration=1200` ms, `resetAfter=1800` ms (0 lo deja confirmado), `holdingLabel='Mantén pulsado…'`, `doneLabel='Confirmado'`, `hint='Mantén pulsado para confirmar'` (descripción accesible), `disabled`, `onConfirm()`. API: `reset()`, `confirmed`, `destroy()`. Un clic simple no envía formularios ni actúa. Un estado educado anuncia la confirmación.
12
+
13
+ Variables: `--mk-hold-bg`, `--mk-hold-fg`, `--mk-hold-fill`, `--mk-hold-on-fill` (tonos de error por defecto; `.mk-hold-confirm--neutral` usa superficie y texto). Movimiento reducido: el relleno salta y no hay partículas; la confirmación sigue llegando a su tiempo.
@@ -0,0 +1,24 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createHoldConfirm, type HoldConfirmInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-hold-confirm class="mk-hold-confirm" duration="1200"><button>Eliminar cuenta</button></mk-hold-confirm>
6
+ * Hold the button (pointer, Space or Enter) to confirm. Emits mk:confirm.
7
+ */
8
+ export class MkHoldConfirmElement extends MkElement<HoldConfirmInstance> {
9
+ static options: Record<string, OptionType> = { duration: 'number', resetAfter: 'number', holdingLabel: 'string', doneLabel: 'string', hint: 'string', disabled: 'boolean' };
10
+
11
+ protected create(options: Record<string, unknown>) {
12
+ return createHoldConfirm(this, options);
13
+ }
14
+
15
+ reset() {
16
+ this.instance?.reset();
17
+ }
18
+
19
+ get confirmed() {
20
+ return this.instance?.confirmed ?? false;
21
+ }
22
+ }
23
+
24
+ export const defineHoldConfirm = () => defineElement('mk-hold-confirm', MkHoldConfirmElement);
@@ -0,0 +1,216 @@
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 { burst, press } from '../../motion/micro';
6
+
7
+ /*
8
+ * Hold to confirm: the same button is held (pointer, Space or Enter) while a
9
+ * fill runs across it for `duration`; the button presses in a little and its
10
+ * label changes with a blur. Letting go early springs the fill back. Holding
11
+ * to the end confirms: a press bounce, a check that draws itself, a ring of
12
+ * particles, onConfirm / mk:confirm, and the button returns after `resetAfter`.
13
+ * The hint ("Mantén pulsado para confirmar") is the button's description.
14
+ */
15
+
16
+ export interface HoldConfirmOptions {
17
+ /** Hold time in ms (default 1200). */
18
+ duration?: number;
19
+ /** Back to the start after confirming, in ms (default 1800; 0 keeps it confirmed). */
20
+ resetAfter?: number;
21
+ holdingLabel?: string;
22
+ doneLabel?: string;
23
+ hint?: string;
24
+ disabled?: boolean;
25
+ onConfirm?: () => void;
26
+ }
27
+
28
+ export interface HoldConfirmInstance extends Instance {
29
+ reset(): void;
30
+ readonly confirmed: boolean;
31
+ }
32
+
33
+ const CHECK = '<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"/></svg>';
34
+ let sequence = 0;
35
+
36
+ export function createHoldConfirm(root: HTMLElement | null, options: HoldConfirmOptions = {}): HoldConfirmInstance {
37
+ const button = root ? ((root.matches('button') ? root : root.querySelector('button')) as HTMLButtonElement | null) : null;
38
+ if (!root || !button) return inert({ reset() {}, confirmed: false });
39
+ registerMotion();
40
+ const scope = createScope();
41
+ const duration = Number.isFinite(options.duration) && options.duration! > 0 ? options.duration! : 1200;
42
+ const resetAfter = Number.isFinite(options.resetAfter) ? Math.max(0, options.resetAfter!) : 1800;
43
+ const made: Element[] = [];
44
+ const add = <T extends Element>(parent: Element, element: T, first = false) => {
45
+ if (first) parent.prepend(element);
46
+ else parent.append(element);
47
+ made.push(element);
48
+ return element;
49
+ };
50
+ scope.add(() => made.forEach((element) => element.remove()));
51
+
52
+ // The label: the authored text, kept in a span that swaps with a blur.
53
+ const authored = [...button.childNodes];
54
+ const label = document.createElement('span');
55
+ label.dataset.mkHoldLabel = '';
56
+ label.append(...authored);
57
+ button.append(label);
58
+ scope.add(() => {
59
+ label.remove();
60
+ button.append(...authored);
61
+ });
62
+ const fill = add(button, document.createElement('span'), true);
63
+ fill.dataset.mkHoldFill = '';
64
+ fill.setAttribute('aria-hidden', 'true');
65
+ const check = add(button, document.createElement('span'));
66
+ check.dataset.mkHoldCheck = '';
67
+ check.innerHTML = CHECK;
68
+ const hint = add(root === button ? button : root, document.createElement('span'));
69
+ hint.className = 'mk-sr-only';
70
+ hint.id = `mk-hold-${++sequence}`;
71
+ hint.textContent = options.hint ?? 'Mantén pulsado para confirmar';
72
+ const live = add(root === button ? button : root, document.createElement('span'));
73
+ live.className = 'mk-sr-only';
74
+ live.setAttribute('role', 'status');
75
+
76
+ scope.attr(button, 'type', button.getAttribute('type') ?? 'button');
77
+ scope.attr(button, 'aria-describedby', [button.getAttribute('aria-describedby'), hint.id].filter(Boolean).join(' '));
78
+ scope.attr(button, 'data-mk-hold', '');
79
+ scope.attr(button, 'style', button.getAttribute('style'));
80
+ if (options.disabled) scope.attr(button, 'disabled', '');
81
+ scope.attr(root, 'data-mk-ready', '');
82
+
83
+ let holding = false;
84
+ let confirmed = false;
85
+ let timer: ReturnType<typeof setTimeout> | undefined;
86
+ let back: ReturnType<typeof setTimeout> | undefined;
87
+ const effects = new Set<gsap.core.Animation>();
88
+ const keep = (animation: gsap.core.Animation | null) => {
89
+ if (!animation) return;
90
+ effects.add(animation);
91
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
92
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
93
+ };
94
+ scope.add(() => {
95
+ clearTimeout(timer);
96
+ clearTimeout(back);
97
+ for (const effect of effects) effect.kill();
98
+ effects.clear();
99
+ gsap.killTweensOf([fill, button, label, check]);
100
+ });
101
+
102
+ /** Show `text`, or the authored content again with null. */
103
+ let showing: string | null = null;
104
+ const write = (text: string | null) => {
105
+ showing = text;
106
+ if (text === null) label.replaceChildren(...authored);
107
+ else label.textContent = text;
108
+ };
109
+ const swap = (text: string | null) => {
110
+ if (showing === text) return;
111
+ if (reduced()) {
112
+ write(text);
113
+ return;
114
+ }
115
+ gsap.killTweensOf(label);
116
+ keep(
117
+ gsap
118
+ .timeline()
119
+ .to(label, { opacity: 0, filter: 'blur(6px)', y: -4, duration: 0.12, ease: 'power2.in' })
120
+ .call(() => write(text))
121
+ .fromTo(label, { opacity: 0, filter: 'blur(6px)', y: 4 }, { opacity: 1, filter: 'blur(0px)', y: 0, duration: 0.25, ease: EASE.pop, clearProps: 'all' }),
122
+ );
123
+ };
124
+
125
+ const setProgress = (value: number, seconds: number, ease: string) => {
126
+ gsap.killTweensOf(fill, 'scaleX');
127
+ if (reduced() || seconds === 0) gsap.set(fill, { scaleX: value });
128
+ else keep(gsap.to(fill, { scaleX: value, duration: seconds, ease }));
129
+ };
130
+
131
+ const start = () => {
132
+ if (holding || confirmed || button.disabled || scope.disposed) return;
133
+ holding = true;
134
+ scope.attr(button, 'data-mk-holding', '');
135
+ swap(options.holdingLabel ?? 'Mantén pulsado…');
136
+ setProgress(1, duration / 1000, 'power1.in');
137
+ if (!reduced()) keep(gsap.to(button, { scale: 0.96, duration: duration / 1000, ease: 'power1.out' }));
138
+ clearTimeout(timer);
139
+ timer = setTimeout(confirm, duration);
140
+ };
141
+
142
+ const cancel = () => {
143
+ if (!holding) return;
144
+ holding = false;
145
+ clearTimeout(timer);
146
+ scope.attr(button, 'data-mk-holding', null);
147
+ swap(null);
148
+ setProgress(0, 0.5, EASE.swatch);
149
+ if (!reduced()) keep(gsap.to(button, { scale: 1, duration: 0.5, ease: EASE.click, clearProps: 'scale,transform' }));
150
+ };
151
+
152
+ const reset = () => {
153
+ if (scope.disposed) return;
154
+ clearTimeout(back);
155
+ confirmed = false;
156
+ holding = false;
157
+ scope.attr(button, 'data-mk-confirmed', null);
158
+ scope.attr(button, 'data-mk-holding', null);
159
+ swap(null);
160
+ setProgress(0, 0.4, 'power2.out');
161
+ };
162
+
163
+ function confirm() {
164
+ if (!holding || scope.disposed) return;
165
+ holding = false;
166
+ confirmed = true;
167
+ scope.attr(button!, 'data-mk-holding', null);
168
+ scope.attr(button!, 'data-mk-confirmed', '');
169
+ setProgress(1, 0, 'none');
170
+ gsap.killTweensOf(button, 'scale');
171
+ gsap.set(button, { clearProps: 'scale,transform' });
172
+ keep(press(button));
173
+ swap(options.doneLabel ?? 'Confirmado');
174
+ const path = check.querySelector('path');
175
+ if (path && !reduced()) keep(gsap.fromTo(path, { strokeDasharray: 1, strokeDashoffset: 1 }, { strokeDashoffset: 0, duration: 0.45, delay: 0.1, ease: 'power2.out' }));
176
+ keep(burst(button, { count: 8, distance: 34, colors: ['currentColor'] }));
177
+ live.textContent = options.doneLabel ?? 'Confirmado';
178
+ options.onConfirm?.();
179
+ root!.dispatchEvent(new CustomEvent('mk:confirm', { bubbles: true }));
180
+ if (resetAfter) back = setTimeout(reset, resetAfter);
181
+ }
182
+
183
+ scope.on(button, 'pointerdown', (event) => {
184
+ const pointer = event as PointerEvent;
185
+ if (pointer.button !== 0) return;
186
+ pointer.preventDefault();
187
+ button.focus({ preventScroll: true });
188
+ start();
189
+ });
190
+ for (const type of ['pointerup', 'pointerleave', 'pointercancel']) scope.on(button, type, cancel);
191
+ scope.on(button, 'keydown', (event) => {
192
+ const key = event as KeyboardEvent;
193
+ if (key.key !== ' ' && key.key !== 'Enter') return;
194
+ key.preventDefault();
195
+ if (!key.repeat) start();
196
+ });
197
+ scope.on(button, 'keyup', (event) => {
198
+ const key = (event as KeyboardEvent).key;
199
+ if (key === ' ' || key === 'Enter') cancel();
200
+ });
201
+ scope.on(button, 'blur', cancel);
202
+ // A plain click (no hold) must not submit a form or act.
203
+ scope.on(button, 'click', (event) => event.preventDefault());
204
+ scope.on(button, 'contextmenu', (event) => {
205
+ if (holding) event.preventDefault();
206
+ });
207
+ gsap.set(fill, { scaleX: 0 });
208
+
209
+ return {
210
+ reset,
211
+ get confirmed() {
212
+ return confirmed;
213
+ },
214
+ destroy: () => scope.dispose(),
215
+ };
216
+ }
@@ -0,0 +1,86 @@
1
+ /* hold-confirm — Melonmind button (64 px pill, surface tones); while held a darker fill runs from the left,
2
+ the button presses in, the label changes; confirmed, a check draws itself. */
3
+ .mk-hold-confirm,
4
+ mk-hold-confirm {
5
+ display: inline-flex;
6
+ }
7
+
8
+ [data-mk-hold] {
9
+ --mk-hold-bg: var(--mk-error-soft);
10
+ --mk-hold-fg: var(--mk-on-error-soft);
11
+ --mk-hold-fill: var(--mk-error);
12
+ --mk-hold-on-fill: var(--mk-on-error);
13
+ position: relative;
14
+ isolation: isolate;
15
+ display: inline-flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ gap: 0.5rem;
19
+ min-height: 3rem;
20
+ min-width: 12rem;
21
+ padding: 0 1.75rem;
22
+ overflow: hidden;
23
+ border: 0;
24
+ border-radius: 64px;
25
+ background: var(--mk-hold-bg);
26
+ color: var(--mk-hold-fg);
27
+ font: inherit;
28
+ font-weight: 500;
29
+ cursor: pointer;
30
+ user-select: none;
31
+ -webkit-user-select: none;
32
+ touch-action: manipulation;
33
+ -webkit-touch-callout: none;
34
+ transition: color 0.2s ease;
35
+ }
36
+
37
+ [data-mk-hold]:focus-visible {
38
+ outline: 2px solid var(--mk-accent);
39
+ outline-offset: 3px;
40
+ }
41
+
42
+ [data-mk-hold]:disabled {
43
+ opacity: 0.5;
44
+ cursor: not-allowed;
45
+ }
46
+
47
+ [data-mk-hold] [data-mk-hold-fill] {
48
+ position: absolute;
49
+ inset: 0;
50
+ z-index: -1;
51
+ border-radius: inherit;
52
+ background: var(--mk-hold-fill);
53
+ transform-origin: left center;
54
+ }
55
+
56
+ [data-mk-hold]:is([data-mk-holding], [data-mk-confirmed]) {
57
+ color: var(--mk-hold-on-fill);
58
+ }
59
+
60
+ [data-mk-hold] [data-mk-hold-label] {
61
+ display: inline-block;
62
+ }
63
+
64
+ [data-mk-hold] [data-mk-hold-check] {
65
+ display: none;
66
+ }
67
+
68
+ [data-mk-hold][data-mk-confirmed] [data-mk-hold-check] {
69
+ display: inline-grid;
70
+ order: -1;
71
+ }
72
+
73
+ /* The neutral variant (Melonmind tonal). */
74
+ .mk-hold-confirm--neutral [data-mk-hold],
75
+ [data-mk-hold].mk-hold-confirm--neutral {
76
+ --mk-hold-bg: var(--mk-surface-3);
77
+ --mk-hold-fg: var(--mk-text);
78
+ --mk-hold-fill: var(--mk-text);
79
+ --mk-hold-on-fill: var(--mk-bg);
80
+ }
81
+
82
+ @media (prefers-reduced-motion: reduce) {
83
+ [data-mk-hold] {
84
+ transition: none;
85
+ }
86
+ }
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ import { useRef, type ButtonHTMLAttributes, type ReactNode } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createHoldConfirm, type HoldConfirmOptions } from './engine';
5
+
6
+ export interface HoldConfirmProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children' | 'onClick'>, HoldConfirmOptions {
7
+ children?: ReactNode;
8
+ }
9
+
10
+ /**
11
+ * <HoldConfirm onConfirm={removeAccount}>Eliminar cuenta</HoldConfirm>: hold the button to confirm.
12
+ * The engine moves the label into its own span, so pass plain text or inline content.
13
+ */
14
+ export function HoldConfirm({ duration, resetAfter, holdingLabel, doneLabel, hint, disabled, onConfirm, className, children, ...props }: HoldConfirmProps) {
15
+ const ref = useRef<HTMLSpanElement>(null);
16
+ const confirm = useLatest(onConfirm);
17
+ useIsomorphicLayoutEffect(() => {
18
+ const instance = createHoldConfirm(ref.current, { duration, resetAfter, holdingLabel, doneLabel, hint, disabled, onConfirm: () => confirm.current?.() });
19
+ return () => instance.destroy();
20
+ }, [duration, resetAfter, holdingLabel, doneLabel, hint, disabled]);
21
+ return (
22
+ <span ref={ref} className={['mk-hold-confirm', className].filter(Boolean).join(' ')}>
23
+ <button type="button" {...props}>
24
+ <span data-mk-hold-text="">{children}</span>
25
+ </button>
26
+ </span>
27
+ );
28
+ }
@@ -11,6 +11,10 @@ Vista previa no modal, accesible al señalar, enfocar o tocar. Los enlaces/boton
11
11
 
12
12
  `defineElements()`, `createHoverCard(root, options)`, o `<HoverCard trigger="Ver estudio" placement="bottom">Contenido</HoverCard>`.
13
13
 
14
- Opciones: `openDelay=120` ms, `closeDelay=140` ms, `placement='top'|'bottom'`, `onOpenChange(boolean)`. API `open()`, `close()`, `isOpen`, `destroy()`. Evento `mk:open-change` con `{open}`.
14
+ Opciones: `openDelay=120` ms, `closeDelay=140` ms, `placement='top'|'bottom'` (lado preferido), `onOpenChange(boolean)`. API `open()`, `close()`, `reposition()`, `isOpen`, `destroy()`. Evento `mk:open-change` con `{open}`.
15
15
 
16
- Escape cierra y retorna al disparador; salir con foco y puntero fuera o tocar fuera cierra. El panel se posiciona respecto al root: mantener un ancestro que permita overflow; no crea portal. No atrapa el foco. Entrada .28 s, instantánea bajo movimiento reducido. Limpia timers y restaura todos los atributos.
16
+ Escape cierra y retorna al disparador; salir con foco y puntero fuera o tocar fuera cierra. El puntero puede pasar del disparador al panel sin cerrarlo y el panel admite foco en su contenido. No atrapa el foco.
17
+
18
+ El panel se posiciona respecto al root (sin portal: mantener un ancestro que permita overflow) y no se sale del viewport: si su lado no tiene sitio pasa al otro (`data-mk-side`) y se desplaza en horizontal lejos de los bordes (`--mk-hover-shift`); vuelve a colocarse con scroll y resize mientras está abierto. El panel es cristal inverso (`--mk-glass-inverse`, radio de 24px) y entra con un pop de .3 s desde el blur al señalar o tocar; abierto desde teclado (`data-mk-input="keyboard"`) aparece sin entrada, igual que con movimiento reducido. Limpia timers, listeners y frames y restaura todos los atributos.
19
+
20
+ [link-preview](../link-preview/README.md) lo compone para previsualizar enlaces.
@@ -1,36 +1,84 @@
1
+ import { requestFrame } from '../../core/frame';
1
2
  import { createScope } from '../../core/scope';
2
3
  import { inert, type Instance } from '../../core/types';
3
4
 
4
5
  export interface HoverCardOptions {
5
6
  openDelay?: number;
6
7
  closeDelay?: number;
8
+ /** Preferred side; it flips when the viewport has no room there. */
7
9
  placement?: 'top' | 'bottom';
8
10
  onOpenChange?: (open: boolean) => void;
9
11
  }
10
12
  export interface HoverCardInstance extends Instance {
11
13
  open(): void;
12
14
  close(): void;
15
+ /** Place the open panel again (after its content changed size). */
16
+ reposition(): void;
13
17
  readonly isOpen: boolean;
14
18
  }
15
19
  let sequence = 0;
20
+ /** Distance kept from the viewport edges. */
21
+ const EDGE = 12;
22
+ const GAP = 12;
16
23
 
17
- /** Nonmodal preview: pointer, keyboard focus and tap all reveal the same panel. */
24
+ /**
25
+ * Nonmodal preview: pointer, keyboard focus and tap all reveal the same panel.
26
+ * The panel stays inside the viewport: it flips to the other side when its own
27
+ * has no room and shifts sideways (--mk-hover-shift) away from the edges, again
28
+ * on scroll and resize. Opened from the keyboard it appears without the entrance.
29
+ */
18
30
  export function createHoverCard(root: HTMLElement | null, options: HoverCardOptions = {}): HoverCardInstance {
19
31
  const trigger = root?.querySelector<HTMLElement>('[data-mk-hover-trigger]');
20
32
  const panel = root?.querySelector<HTMLElement>('[data-mk-hover-panel]');
21
- if (!root || !trigger || !panel) return inert({ open() {}, close() {}, isOpen: false });
33
+ if (!root || !trigger || !panel) return inert({ open() {}, close() {}, reposition() {}, isOpen: false });
22
34
  const scope = createScope();
23
35
  const openDelay = Number.isFinite(options.openDelay) ? Math.max(0, options.openDelay!) : 120;
24
36
  const closeDelay = Number.isFinite(options.closeDelay) ? Math.max(0, options.closeDelay!) : 140;
37
+ const preferred = options.placement ?? 'top';
25
38
  let opened = false;
26
39
  let timer: ReturnType<typeof setTimeout> | undefined;
27
40
  let pointerInside = false;
41
+ /** The last input was a pointer (focus that follows a click is not keyboard). */
42
+ let pointerInput = false;
28
43
  let id = panel.id;
29
44
  if (!id) { do { id = `mk-hover-${++sequence}`; } while (document.getElementById(id)); }
30
45
  scope.attr(panel, 'id', id);
31
46
  scope.attr(trigger, 'aria-controls', id);
32
- scope.attr(root, 'data-mk-placement', options.placement ?? 'top');
47
+ scope.attr(root, 'data-mk-placement', preferred);
48
+ scope.attr(root, 'data-mk-side', preferred);
49
+ scope.attr(root, 'style', root.getAttribute('style'));
33
50
  scope.attr(root, 'data-mk-ready', '');
51
+
52
+ const place = () => {
53
+ if (!opened || scope.disposed) return;
54
+ root.style.removeProperty('--mk-hover-shift');
55
+ const anchor = root.getBoundingClientRect();
56
+ const box = panel.getBoundingClientRect();
57
+ const view = { width: innerWidth, height: innerHeight };
58
+ const need = box.height + GAP + EDGE;
59
+ const above = anchor.top;
60
+ const below = view.height - anchor.bottom;
61
+ let side = preferred;
62
+ if (preferred === 'top' && above < need && below > above) side = 'bottom';
63
+ if (preferred === 'bottom' && below < need && above > below) side = 'top';
64
+ scope.attr(root, 'data-mk-side', side);
65
+ // The panel starts at the root's left edge; move it back inside the viewport.
66
+ const left = anchor.left;
67
+ let shift = 0;
68
+ if (left + box.width > view.width - EDGE) shift = view.width - EDGE - box.width - left;
69
+ if (left + shift < EDGE) shift = EDGE - left;
70
+ if (shift) root.style.setProperty('--mk-hover-shift', `${Math.round(shift)}px`);
71
+ };
72
+ let pending: (() => void) | null = null;
73
+ const schedulePlace = () => {
74
+ if (!opened || pending) return;
75
+ pending = requestFrame(() => {
76
+ pending = null;
77
+ place();
78
+ });
79
+ };
80
+ scope.add(() => pending?.());
81
+
34
82
  const cancel = () => { clearTimeout(timer); timer = undefined; };
35
83
  const set = (value: boolean) => {
36
84
  cancel();
@@ -40,22 +88,28 @@ export function createHoverCard(root: HTMLElement | null, options: HoverCardOpti
40
88
  scope.attr(trigger, 'aria-expanded', String(value));
41
89
  scope.attr(panel, 'hidden', value ? null : '');
42
90
  scope.attr(root, 'data-mk-open', value ? '' : null);
91
+ if (value && changed) {
92
+ scope.attr(root, 'data-mk-input', pointerInput ? 'pointer' : 'keyboard');
93
+ place();
94
+ }
43
95
  if (changed) {
44
96
  options.onOpenChange?.(value);
45
97
  root.dispatchEvent(new CustomEvent('mk:open-change', { bubbles: true, detail: { open: value } }));
46
98
  }
47
99
  };
48
100
  const schedule = (value: boolean, delay: number) => { cancel(); timer = setTimeout(() => set(value), delay); };
49
- scope.on(root, 'pointerenter', () => { pointerInside = true; schedule(true, openDelay); });
101
+ scope.on(root, 'pointerenter', () => { pointerInside = true; pointerInput = true; schedule(true, openDelay); });
50
102
  scope.on(root, 'pointerleave', () => {
51
103
  pointerInside = false;
52
104
  if (!root.contains(document.activeElement)) schedule(false, closeDelay);
53
105
  });
106
+ scope.on(root, 'pointerdown', () => { pointerInput = true; });
107
+ scope.on(document, 'keydown', () => { pointerInput = false; }, true);
54
108
  scope.on(root, 'focusin', () => set(true));
55
109
  scope.on(root, 'focusout', (event) => {
56
110
  if (!pointerInside && !root.contains((event as FocusEvent).relatedTarget as Node | null)) schedule(false, closeDelay);
57
111
  });
58
- scope.on(trigger, 'click', () => set(true));
112
+ scope.on(trigger, 'click', () => { pointerInput = true; set(true); });
59
113
  scope.on(document, 'pointerdown', (event) => { if (!root.contains(event.target as Node)) set(false); });
60
114
  scope.on(root, 'keydown', (event) => {
61
115
  if (event.key !== 'Escape' || !opened) return;
@@ -63,7 +117,15 @@ export function createHoverCard(root: HTMLElement | null, options: HoverCardOpti
63
117
  trigger.focus();
64
118
  set(false);
65
119
  });
120
+ scope.on(window, 'resize', schedulePlace);
121
+ scope.on(window, 'scroll', schedulePlace, { capture: true, passive: true });
66
122
  scope.add(cancel);
67
123
  set(false);
68
- return { open: () => set(true), close: () => set(false), get isOpen() { return opened; }, destroy: () => scope.dispose() };
124
+ return {
125
+ open: () => set(true),
126
+ close: () => set(false),
127
+ reposition: place,
128
+ get isOpen() { return opened; },
129
+ destroy: () => scope.dispose(),
130
+ };
69
131
  }
@@ -1,13 +1,19 @@
1
1
  .mk-hover-card, mk-hover-card { position: relative; display: inline-flex; }
2
2
  .mk-hover-card [data-mk-hover-panel], mk-hover-card [data-mk-hover-panel] {
3
- position: absolute; z-index: 10; bottom: calc(100% + .75rem); left: 0;
4
- width: min(19rem, calc(100vw - 3rem)); padding: 1.25rem; border-radius: var(--mk-radius-lg);
5
- color: var(--mk-text); background: var(--mk-surface-3); border: 1px solid var(--mk-outline-soft);
6
- box-shadow: 0 12px 40px color-mix(in srgb, var(--mk-shadow-color) 20%, transparent);
7
- animation: mk-hover-card-in .28s cubic-bezier(.2,.8,.2,1) both;
3
+ transform-origin: 50% 100%;
4
+ position: absolute; z-index: 10; bottom: calc(100% + .75rem); left: var(--mk-hover-shift, 0);
5
+ width: min(19rem, calc(100vw - 1.5rem)); padding: 1.25rem; border-radius: var(--mk-radius-lg);
6
+ color: var(--mk-on-glass-inverse); background: var(--mk-glass-inverse); border: 0; border-radius: var(--mk-radius-lg, 24px);
7
+ box-shadow: var(--mk-glass-bevel); backdrop-filter: blur(16px);
8
+ /* Melonmind animation-general-in-2: blur 32 → 0, scale .7 → 1, 300 ms. */
9
+ animation: mk-hover-card-in .3s cubic-bezier(.1,.8,0,1.1) both;
8
10
  }
11
+ /* Bridge over the gap so the pointer can travel into the panel. */
9
12
  .mk-hover-card [data-mk-hover-panel]::after, mk-hover-card [data-mk-hover-panel]::after { content: ''; position: absolute; inset: -1rem 0; z-index: -1; }
10
- .mk-hover-card[data-mk-placement=bottom] [data-mk-hover-panel], mk-hover-card[data-mk-placement=bottom] [data-mk-hover-panel] { top: calc(100% + .75rem); bottom: auto; }
13
+ :is(.mk-hover-card, mk-hover-card)[data-mk-side=bottom] [data-mk-hover-panel] { top: calc(100% + .75rem); bottom: auto; transform-origin: 50% 0; animation-name: mk-hover-card-down; }
14
+ /* Opened from the keyboard: no entrance. */
15
+ :is(.mk-hover-card, mk-hover-card)[data-mk-input=keyboard] [data-mk-hover-panel] { animation: none; }
11
16
  .mk-hover-card [hidden], mk-hover-card [hidden] { display: none !important; }
12
- @keyframes mk-hover-card-in { from { opacity: 0; transform: translateY(7px) scale(.97); } to { opacity: 1; transform: none; } }
17
+ @keyframes mk-hover-card-in { from { opacity: 0; filter: blur(32px); transform: translateY(8px) scale(.7); } to { opacity: 1; filter: none; transform: none; } }
18
+ @keyframes mk-hover-card-down { from { opacity: 0; filter: blur(32px); transform: translateY(-8px) scale(.7); } to { opacity: 1; filter: none; transform: none; } }
13
19
  @media (prefers-reduced-motion: reduce) { .mk-hover-card [data-mk-hover-panel], mk-hover-card [data-mk-hover-panel] { animation: none; } }
@@ -0,0 +1,16 @@
1
+ # image-preview
2
+
3
+ Visor de imágenes tipo diálogo: las miniaturas `[data-mk-image]` abren un `<dialog>` modal donde la imagen crece desde su miniatura (0.7 s, curva firma; el radio de la miniatura pasa a 24 px) sobre un fondo oscuro desenfocado. Anterior/siguiente (botones, flechas o deslizar) entran con desplazamiento y blur, y el contador rueda. Doble clic o doble toque hace zoom ×2 donde apuntas y arrastrar lo mueve. Arrastrar hacia abajo, Escape o cerrar devuelven la imagen a su miniatura y el foco a ella.
4
+
5
+ ```html
6
+ <mk-image-preview class="mk-image-preview">
7
+ <button data-mk-image data-mk-src="/big/1.jpg" data-mk-caption="Atardecer"><img src="/thumb/1.jpg" alt="Atardecer en la sierra"></button>
8
+ <button data-mk-image data-mk-src="/big/2.jpg"><img src="/thumb/2.jpg" alt="Lago"></button>
9
+ </mk-image-preview>
10
+ ```
11
+
12
+ `defineElements()`, `createImagePreview(root, options)` o `<ImagePreview images={[{ src, thumb, alt, caption }]} />`. La imagen grande sale de `data-mk-src`, el `href` o el `src` de la miniatura; el pie, de `data-mk-caption`.
13
+
14
+ Opciones: `labels {close, prev, next, dialog}`, `loop=true`, `onChange(index)`, `onOpenChange(open)`. API: `open(index)`, `close()`, `next()`, `prev()`, `isOpen`, `index`, `destroy()`. Eventos `mk:open`, `mk:change` `{index, previous}` y `mk:close`. Miniaturas que no son botones reciben `tabindex` y `role="button"` (Enter o Espacio abren).
15
+
16
+ Movimiento reducido: abre, cambia y cierra sin crecimiento ni deslizamientos.
@@ -0,0 +1,26 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createImagePreview, type ImagePreviewInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-image-preview class="mk-image-preview">
6
+ * <button data-mk-image data-mk-src="/big/1.jpg" data-mk-caption="Atardecer"><img src="/thumb/1.jpg" alt="Atardecer en la sierra"></button>
7
+ * </mk-image-preview>
8
+ * Emits mk:open, mk:change ({index, previous}) and mk:close.
9
+ */
10
+ export class MkImagePreviewElement extends MkElement<ImagePreviewInstance> {
11
+ static options: Record<string, OptionType> = { loop: 'boolean', labels: 'json' };
12
+
13
+ protected create(options: Record<string, unknown>) {
14
+ return createImagePreview(this, options);
15
+ }
16
+
17
+ open(index?: number) {
18
+ this.instance?.open(index);
19
+ }
20
+
21
+ close() {
22
+ this.instance?.close();
23
+ }
24
+ }
25
+
26
+ export const defineImagePreview = () => defineElement('mk-image-preview', MkImagePreviewElement);