@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,29 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createLinkPreview, type LinkPreviewInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-link-preview class="mk-link-preview" url="https://…" title="…" description="…" image="/thumb.jpg" site-name="Blog"></mk-link-preview>
6
+ * variant="hover" turns it into a link that previews the card (label = link text).
7
+ */
8
+ export class MkLinkPreviewElement extends MkElement<LinkPreviewInstance> {
9
+ static options: Record<string, OptionType> = {
10
+ url: 'string',
11
+ title: 'string',
12
+ description: 'string',
13
+ image: 'string',
14
+ imageAlt: 'string',
15
+ siteName: 'string',
16
+ variant: 'string',
17
+ label: 'string',
18
+ target: 'string',
19
+ openDelay: 'number',
20
+ closeDelay: 'number',
21
+ placement: 'string',
22
+ };
23
+
24
+ protected create(options: Record<string, unknown>) {
25
+ return createLinkPreview(this, options);
26
+ }
27
+ }
28
+
29
+ export const defineLinkPreview = () => defineElement('mk-link-preview', MkLinkPreviewElement);
@@ -0,0 +1,192 @@
1
+ import { createScope } from '../../core/scope';
2
+ import { inert, type Instance } from '../../core/types';
3
+ import { createHoverCard, type HoverCardInstance, type HoverCardOptions } from '../hover-card/engine';
4
+
5
+ /*
6
+ * Link preview: a resource card (thumbnail or domain initial, site, title,
7
+ * description) around a real link, or a link that previews that card in a
8
+ * hover card (pointer, focus, tap). The app supplies the metadata; nothing is
9
+ * fetched. Links accept http(s) and relative URLs only; strings are text; a
10
+ * thumbnail that fails to load gives way to the domain initial.
11
+ * Made into an empty root; with authored markup ([data-mk-link-card],
12
+ * img[data-mk-link-image], [data-mk-hover-trigger]) only the behaviour is added.
13
+ */
14
+
15
+ /** card: image on top (default); row: thumbnail beside the text; hover: a link that previews the card. */
16
+ export type LinkPreviewVariant = 'card' | 'row' | 'hover';
17
+
18
+ /** The arrow of the card's go button (decorative). */
19
+ const GO_ICON =
20
+ '<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"><path d="M7 17L17 7M9 7h8v8"/></svg>';
21
+
22
+ export interface LinkPreviewOptions extends Pick<HoverCardOptions, 'openDelay' | 'closeDelay' | 'placement' | 'onOpenChange'> {
23
+ url?: string;
24
+ title?: string;
25
+ description?: string;
26
+ /** Thumbnail URL (http(s), relative, data:image or blob). */
27
+ image?: string;
28
+ imageAlt?: string;
29
+ siteName?: string;
30
+ variant?: LinkPreviewVariant;
31
+ /** Text of the hover variant's link (default: the title). */
32
+ label?: string;
33
+ /** '_blank' opens a new tab (with rel="noopener noreferrer"). */
34
+ target?: string;
35
+ }
36
+
37
+ export interface LinkPreviewInstance extends Instance {
38
+ /** The link actually used (null when the URL was refused). */
39
+ readonly href: string | null;
40
+ readonly hoverCard: HoverCardInstance | null;
41
+ }
42
+
43
+ // Browsers drop tabs/newlines inside URLs ("java\nscript:"): check what they will see.
44
+ const squash = (url: string) => url.replace(/[\u0000- \u007f]+/g, '');
45
+ const schemeOf = (url: string) => /^([a-z][a-z\d+.-]*):/i.exec(squash(url))?.[1]?.toLowerCase() ?? null;
46
+
47
+ /** http(s) or relative links; anything else (javascript:, data:, vbscript:…) is null. */
48
+ export function safeHref(url: string | null | undefined): string | null {
49
+ if (typeof url !== 'string' || !url.trim()) return null;
50
+ const scheme = schemeOf(url.trim());
51
+ return scheme === null || scheme === 'http' || scheme === 'https' ? url.trim() : null;
52
+ }
53
+
54
+ /** Image sources: http(s), relative, blob: and data:image/… */
55
+ export function safeImage(url: string | null | undefined): string | null {
56
+ if (typeof url !== 'string' || !url.trim()) return null;
57
+ const value = url.trim();
58
+ const scheme = schemeOf(value);
59
+ if (scheme === null || scheme === 'http' || scheme === 'https' || scheme === 'blob') return value;
60
+ return scheme === 'data' && /^data:image\//i.test(squash(value)) ? value : null;
61
+ }
62
+
63
+ /** Host without www. of an absolute link; relative links have none (same on server and client). */
64
+ export function linkDomain(href: string | null) {
65
+ if (!href) return '';
66
+ try {
67
+ return new URL(href.startsWith('//') ? `https:${href}` : href).hostname.replace(/^www\./, '');
68
+ } catch {
69
+ return '';
70
+ }
71
+ }
72
+
73
+ let sequence = 0;
74
+
75
+ const part = (tag: string, name: string, text?: string) => {
76
+ const element = document.createElement(tag);
77
+ element.dataset.mkPart = name;
78
+ if (text !== undefined) element.textContent = text;
79
+ return element;
80
+ };
81
+
82
+ /** The card markup (also used by the hover panel). Text only; the href is already safe. */
83
+ function buildCard(options: LinkPreviewOptions, href: string | null) {
84
+ const domain = linkDomain(href);
85
+ // Spans (not divs): the hover variant lives inside running text.
86
+ const card = document.createElement(href ? 'a' : 'span');
87
+ card.dataset.mkLinkCard = '';
88
+ if (href) {
89
+ card.setAttribute('href', href);
90
+ if (options.target) {
91
+ card.setAttribute('target', options.target);
92
+ if (options.target === '_blank') card.setAttribute('rel', 'noopener noreferrer');
93
+ }
94
+ }
95
+ const id = `mk-link-${++sequence}`;
96
+ const media = part('span', 'media');
97
+ const fallback = part('span', 'fallback', (options.siteName || domain || options.title || '?').trim().charAt(0).toUpperCase());
98
+ fallback.setAttribute('aria-hidden', 'true');
99
+ media.append(fallback);
100
+ const image = safeImage(options.image);
101
+ if (image) {
102
+ const img = document.createElement('img');
103
+ img.dataset.mkLinkImage = '';
104
+ img.alt = options.imageAlt ?? '';
105
+ img.loading = 'lazy';
106
+ img.decoding = 'async';
107
+ img.src = image;
108
+ media.append(img);
109
+ }
110
+ const text = part('span', 'text');
111
+ const site = [options.siteName, domain].filter(Boolean).filter((value, i, all) => all.indexOf(value) === i).join(' · ');
112
+ if (site) {
113
+ const meta = part('span', 'meta');
114
+ const favicon = part('span', 'favicon', (options.siteName || domain).trim().charAt(0).toUpperCase());
115
+ favicon.setAttribute('aria-hidden', 'true');
116
+ meta.append(favicon, part('span', 'site', site));
117
+ text.append(meta);
118
+ }
119
+ const title = part('span', 'title', options.title || domain || href || '');
120
+ title.id = `${id}-title`;
121
+ text.append(title);
122
+ if (options.description) {
123
+ const description = part('span', 'description', options.description);
124
+ description.id = `${id}-description`;
125
+ text.append(description);
126
+ card.setAttribute('aria-describedby', description.id);
127
+ }
128
+ card.setAttribute('aria-labelledby', title.id);
129
+ const go = part('span', 'go');
130
+ go.setAttribute('aria-hidden', 'true');
131
+ go.innerHTML = GO_ICON;
132
+ card.append(media, text, go);
133
+ return card;
134
+ }
135
+
136
+ export function createLinkPreview(root: HTMLElement | null, options: LinkPreviewOptions = {}): LinkPreviewInstance {
137
+ if (!root) return inert({ href: null, hoverCard: null });
138
+ const scope = createScope();
139
+ const variant: LinkPreviewVariant = options.variant === 'hover' || options.variant === 'row' ? options.variant : 'card';
140
+ const href = safeHref(options.url);
141
+ const authored = root.querySelector('[data-mk-link-card], [data-mk-hover-trigger]');
142
+ const made: Node[] = [];
143
+ if (!authored) {
144
+ const card = buildCard(options, href);
145
+ if (variant === 'hover') {
146
+ const trigger = document.createElement(href ? 'a' : 'span');
147
+ trigger.dataset.mkHoverTrigger = '';
148
+ if (href) trigger.setAttribute('href', href);
149
+ else trigger.tabIndex = 0;
150
+ if (href && options.target) {
151
+ trigger.setAttribute('target', options.target);
152
+ if (options.target === '_blank') trigger.setAttribute('rel', 'noopener noreferrer');
153
+ }
154
+ trigger.textContent = options.label || options.title || linkDomain(href) || href || '';
155
+ const panel = document.createElement('span');
156
+ panel.dataset.mkHoverPanel = '';
157
+ panel.setAttribute('role', 'region');
158
+ panel.setAttribute('aria-label', `Vista previa: ${options.title || linkDomain(href)}`);
159
+ panel.hidden = true;
160
+ panel.append(card);
161
+ made.push(trigger, panel);
162
+ } else made.push(card);
163
+ root.append(...made);
164
+ scope.add(() => made.forEach((node) => node.parentNode?.removeChild(node)));
165
+ }
166
+ scope.attr(root, 'data-mk-link-variant', variant);
167
+ if (variant === 'hover' && !root.classList.contains('mk-hover-card')) {
168
+ scope.attr(root, 'class', root.getAttribute('class'));
169
+ root.classList.add('mk-hover-card');
170
+ }
171
+ scope.attr(root, 'data-mk-ready', '');
172
+
173
+ // A thumbnail that fails gives way to the initial (listeners go with destroy).
174
+ for (const img of root.querySelectorAll<HTMLImageElement>('img[data-mk-link-image]')) {
175
+ const failed = () => {
176
+ scope.attr(img, 'hidden', '');
177
+ scope.attr(root, 'data-mk-image', 'error');
178
+ };
179
+ scope.on(img, 'error', failed);
180
+ }
181
+
182
+ const hoverCard = root.querySelector('[data-mk-hover-trigger]') ? createHoverCard(root, options) : null;
183
+ if (hoverCard) scope.add(() => hoverCard.destroy());
184
+
185
+ return {
186
+ get href() {
187
+ return href ?? root.querySelector<HTMLAnchorElement>('a[data-mk-link-card], a[data-mk-hover-trigger]')?.getAttribute('href') ?? null;
188
+ },
189
+ hoverCard,
190
+ destroy: () => scope.dispose(),
191
+ };
192
+ }
@@ -0,0 +1,274 @@
1
+ /* link-preview — Melonmind content box: a 24 px card with the image on top (16 px inner radius, 8 px inset), favicon and
2
+ site, a two-line title, the description and a round go button that springs on hover. Row: thumbnail beside the text.
3
+ Hover: the card in Melonmind's inverse glass popover. */
4
+ .mk-link-preview,
5
+ mk-link-preview {
6
+ --mk-link-radius: var(--mk-radius-lg, 24px);
7
+ --mk-link-inset: 8px;
8
+ --mk-link-thumb: 5.5rem;
9
+ display: block;
10
+ color: var(--mk-text);
11
+ font-family: var(--mk-font);
12
+ }
13
+
14
+ :is(.mk-link-preview, mk-link-preview)[data-mk-link-variant='hover'] {
15
+ display: inline-flex;
16
+ }
17
+
18
+ :is(.mk-link-preview, mk-link-preview) [data-mk-link-card] {
19
+ position: relative;
20
+ display: grid;
21
+ gap: 0;
22
+ padding: var(--mk-link-inset);
23
+ border-radius: var(--mk-link-radius);
24
+ background: var(--mk-surface-1);
25
+ box-shadow: inset 0 0 0 1px var(--mk-outline-soft);
26
+ color: inherit;
27
+ text-decoration: none;
28
+ transition:
29
+ translate 0.5s var(--mk-ease-signature, cubic-bezier(0.2, 0.8, 0.2, 1)),
30
+ box-shadow 0.3s ease;
31
+ }
32
+
33
+ :is(.mk-link-preview, mk-link-preview) a[data-mk-link-card]:hover {
34
+ translate: 0 -3px;
35
+ box-shadow:
36
+ inset 0 0 0 1px var(--mk-outline-soft),
37
+ 0 18px 40px -22px color-mix(in srgb, var(--mk-shadow-color) 45%, transparent);
38
+ }
39
+
40
+ :is(.mk-link-preview, mk-link-preview) a[data-mk-link-card]:active {
41
+ animation: mk-link-press 500ms cubic-bezier(0.37, 1.42, 0.37, 1);
42
+ }
43
+
44
+ :is(.mk-link-preview, mk-link-preview) a[data-mk-link-card]:focus-visible,
45
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-trigger]:focus-visible {
46
+ outline: 2px solid var(--mk-accent);
47
+ outline-offset: 3px;
48
+ }
49
+
50
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='media'] {
51
+ position: relative;
52
+ display: grid;
53
+ place-items: center;
54
+ overflow: hidden;
55
+ aspect-ratio: 16 / 9;
56
+ border-radius: calc(var(--mk-link-radius) - var(--mk-link-inset));
57
+ background: var(--mk-surface-3);
58
+ color: var(--mk-muted);
59
+ }
60
+
61
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='fallback'] {
62
+ font-size: clamp(2rem, 8vw, 3.5rem);
63
+ font-weight: 500;
64
+ letter-spacing: -0.04em;
65
+ }
66
+
67
+ :is(.mk-link-preview, mk-link-preview) [data-mk-link-image] {
68
+ position: absolute;
69
+ inset: 0;
70
+ width: 100%;
71
+ height: 100%;
72
+ object-fit: cover;
73
+ transition: scale 0.7s var(--mk-ease-signature, cubic-bezier(0.2, 0.8, 0.2, 1));
74
+ }
75
+
76
+ :is(.mk-link-preview, mk-link-preview) a[data-mk-link-card]:hover [data-mk-link-image] {
77
+ scale: 1.04;
78
+ }
79
+
80
+ :is(.mk-link-preview, mk-link-preview) [data-mk-link-image][hidden] {
81
+ display: none;
82
+ }
83
+
84
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='text'] {
85
+ display: grid;
86
+ gap: 0.35rem;
87
+ min-width: 0;
88
+ padding: 0.85rem 3.25rem 0.6rem 0.6rem;
89
+ }
90
+
91
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='meta'] {
92
+ display: flex;
93
+ align-items: center;
94
+ gap: 0.45rem;
95
+ min-width: 0;
96
+ }
97
+
98
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='favicon'] {
99
+ display: grid;
100
+ flex: none;
101
+ place-items: center;
102
+ width: 1.25rem;
103
+ height: 1.25rem;
104
+ border-radius: 50%;
105
+ background: var(--mk-text);
106
+ color: var(--mk-bg);
107
+ font-size: 0.6875rem;
108
+ font-weight: 600;
109
+ }
110
+
111
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='site'] {
112
+ overflow: hidden;
113
+ color: var(--mk-muted);
114
+ font-size: 0.8125rem;
115
+ text-overflow: ellipsis;
116
+ white-space: nowrap;
117
+ }
118
+
119
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='title'] {
120
+ font-size: 1.0625rem;
121
+ font-weight: 500;
122
+ line-height: 1.25;
123
+ letter-spacing: -0.01em;
124
+ display: -webkit-box;
125
+ overflow: hidden;
126
+ -webkit-box-orient: vertical;
127
+ -webkit-line-clamp: 2;
128
+ }
129
+
130
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='description'] {
131
+ color: var(--mk-muted);
132
+ font-size: 0.875rem;
133
+ line-height: 1.4;
134
+ display: -webkit-box;
135
+ overflow: hidden;
136
+ -webkit-box-orient: vertical;
137
+ -webkit-line-clamp: 2;
138
+ }
139
+
140
+ /* The go button: bottom right, it turns and fills on hover (Melonmind playful curve). */
141
+ :is(.mk-link-preview, mk-link-preview) [data-mk-part='go'] {
142
+ position: absolute;
143
+ right: calc(var(--mk-link-inset) + 0.5rem);
144
+ bottom: calc(var(--mk-link-inset) + 0.5rem);
145
+ display: grid;
146
+ place-items: center;
147
+ width: 2.5rem;
148
+ height: 2.5rem;
149
+ border-radius: 50%;
150
+ background: var(--mk-surface-3);
151
+ color: var(--mk-text);
152
+ rotate: 45deg;
153
+ transition:
154
+ rotate 0.4s cubic-bezier(0.38, 0.49, 0, 2),
155
+ background-color 0.2s ease,
156
+ color 0.2s ease,
157
+ scale 0.4s cubic-bezier(0.38, 0.49, 0, 1.5);
158
+ }
159
+
160
+ :is(.mk-link-preview, mk-link-preview) a[data-mk-link-card]:is(:hover, :focus-visible) [data-mk-part='go'] {
161
+ rotate: 0deg;
162
+ scale: 1.08;
163
+ background: var(--mk-text);
164
+ color: var(--mk-bg);
165
+ }
166
+
167
+ :is(.mk-link-preview, mk-link-preview) span[data-mk-link-card] [data-mk-part='go'] {
168
+ display: none;
169
+ }
170
+
171
+ /* Row: the thumbnail beside the text. */
172
+ :is(.mk-link-preview, mk-link-preview)[data-mk-link-variant='row'] [data-mk-link-card] {
173
+ grid-template-columns: var(--mk-link-thumb) minmax(0, 1fr);
174
+ align-items: center;
175
+ gap: 0.25rem;
176
+ border-radius: var(--mk-radius-md, 16px);
177
+ }
178
+
179
+ :is(.mk-link-preview, mk-link-preview)[data-mk-link-variant='row'] [data-mk-part='media'] {
180
+ aspect-ratio: 1;
181
+ border-radius: 10px;
182
+ }
183
+
184
+ :is(.mk-link-preview, mk-link-preview)[data-mk-link-variant='row'] [data-mk-part='fallback'] {
185
+ font-size: 2rem;
186
+ }
187
+
188
+ :is(.mk-link-preview, mk-link-preview)[data-mk-link-variant='row'] [data-mk-part='text'] {
189
+ padding: 0.25rem 3.25rem 0.25rem 0.75rem;
190
+ }
191
+
192
+ :is(.mk-link-preview, mk-link-preview)[data-mk-link-variant='row'] [data-mk-part='go'] {
193
+ top: 50%;
194
+ bottom: auto;
195
+ margin-top: -1.25rem;
196
+ }
197
+
198
+ /* Hover: an underlined link; the card sits in the inverse glass. */
199
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-trigger] {
200
+ color: inherit;
201
+ font-weight: 500;
202
+ text-decoration: underline;
203
+ text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
204
+ text-decoration-thickness: 1.5px;
205
+ text-underline-offset: 0.22em;
206
+ cursor: pointer;
207
+ transition: text-decoration-color 0.2s ease;
208
+ }
209
+
210
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-trigger]:hover {
211
+ text-decoration-color: currentColor;
212
+ }
213
+
214
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] {
215
+ width: min(20rem, calc(100vw - 1.5rem));
216
+ padding: 0;
217
+ border: 0;
218
+ border-radius: var(--mk-link-radius);
219
+ background: var(--mk-glass-inverse);
220
+ box-shadow: var(--mk-glass-bevel);
221
+ backdrop-filter: blur(16px);
222
+ color: var(--mk-on-glass-inverse);
223
+ }
224
+
225
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] [data-mk-link-card] {
226
+ background: transparent;
227
+ box-shadow: none;
228
+ translate: none;
229
+ }
230
+
231
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] [data-mk-part='media'] {
232
+ background: color-mix(in srgb, var(--mk-on-glass-inverse) 12%, transparent);
233
+ color: var(--mk-on-glass-inverse);
234
+ }
235
+
236
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] :is([data-mk-part='site'], [data-mk-part='description']) {
237
+ color: color-mix(in srgb, var(--mk-on-glass-inverse) 70%, transparent);
238
+ }
239
+
240
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] [data-mk-part='favicon'] {
241
+ background: var(--mk-on-glass-inverse);
242
+ color: var(--mk-text);
243
+ }
244
+
245
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] [data-mk-part='go'] {
246
+ background: color-mix(in srgb, var(--mk-on-glass-inverse) 14%, transparent);
247
+ color: var(--mk-on-glass-inverse);
248
+ }
249
+
250
+ :is(.mk-link-preview, mk-link-preview) [data-mk-hover-panel] a[data-mk-link-card]:is(:hover, :focus-visible) [data-mk-part='go'] {
251
+ background: var(--mk-on-glass-inverse);
252
+ color: var(--mk-text);
253
+ }
254
+
255
+ @keyframes mk-link-press {
256
+ 50% {
257
+ transform: scale(0.97, 0.95);
258
+ }
259
+ }
260
+
261
+ @media (max-width: 420px) {
262
+ :is(.mk-link-preview, mk-link-preview) {
263
+ --mk-link-thumb: 4.25rem;
264
+ }
265
+ }
266
+
267
+ @media (prefers-reduced-motion: reduce) {
268
+ :is(.mk-link-preview, mk-link-preview) :is([data-mk-link-card], [data-mk-link-image], [data-mk-part='go']) {
269
+ transition: none;
270
+ animation: none !important;
271
+ translate: none !important;
272
+ scale: none !important;
273
+ }
274
+ }
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+ import { useEffect, useId, useRef, useState, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createHoverCard } from '../hover-card/engine';
5
+ import { linkDomain, safeHref, safeImage, type LinkPreviewOptions } from './engine';
6
+
7
+ export interface LinkPreviewProps extends Omit<HTMLAttributes<HTMLElement>, 'title'>, LinkPreviewOptions {}
8
+
9
+ function Card({ url, title, description, image, imageAlt, siteName, target }: LinkPreviewOptions) {
10
+ const id = useId();
11
+ const href = safeHref(url);
12
+ const domain = linkDomain(href);
13
+ const source = safeImage(image);
14
+ const [failed, setFailed] = useState(false);
15
+ useEffect(() => setFailed(false), [source]);
16
+ const site = [siteName, domain].filter(Boolean).filter((value, i, all) => all.indexOf(value) === i).join(' · ');
17
+ const content = (
18
+ <>
19
+ <span data-mk-part="media">
20
+ <span data-mk-part="fallback" aria-hidden="true">
21
+ {(siteName || domain || title || '?').trim().charAt(0).toUpperCase()}
22
+ </span>
23
+ {source && !failed ? <img data-mk-link-image="" src={source} alt={imageAlt ?? ''} loading="lazy" decoding="async" onError={() => setFailed(true)} /> : null}
24
+ </span>
25
+ <span data-mk-part="text">
26
+ {site && (
27
+ <span data-mk-part="meta">
28
+ <span data-mk-part="favicon" aria-hidden="true">
29
+ {(siteName || domain).trim().charAt(0).toUpperCase()}
30
+ </span>
31
+ <span data-mk-part="site">{site}</span>
32
+ </span>
33
+ )}
34
+ <span data-mk-part="title" id={`${id}-title`}>
35
+ {title || domain || href}
36
+ </span>
37
+ {description && (
38
+ <span data-mk-part="description" id={`${id}-description`}>
39
+ {description}
40
+ </span>
41
+ )}
42
+ </span>
43
+ <span data-mk-part="go" aria-hidden="true">
44
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round">
45
+ <path d="M7 17L17 7M9 7h8v8" />
46
+ </svg>
47
+ </span>
48
+ </>
49
+ );
50
+ const labelled = { 'aria-labelledby': `${id}-title`, 'aria-describedby': description ? `${id}-description` : undefined };
51
+ return href ? (
52
+ <a data-mk-link-card="" href={href} target={target} rel={target === '_blank' ? 'noopener noreferrer' : undefined} {...labelled}>
53
+ {content}
54
+ </a>
55
+ ) : (
56
+ <span data-mk-link-card="" {...labelled}>
57
+ {content}
58
+ </span>
59
+ );
60
+ }
61
+
62
+ /**
63
+ * <LinkPreview url="https://…" title="…" description="…" image="/thumb.jpg" siteName="Blog" />, or
64
+ * variant="hover" for a link that previews the card on pointer, focus or tap. Metadata comes from the app.
65
+ */
66
+ export function LinkPreview({ url, title, description, image, imageAlt, siteName, variant = 'card', label, target, openDelay, closeDelay, placement, onOpenChange, className, ...props }: LinkPreviewProps) {
67
+ const ref = useRef<HTMLSpanElement>(null);
68
+ const change = useLatest(onOpenChange);
69
+ const href = safeHref(url);
70
+ const hover = variant === 'hover';
71
+ useIsomorphicLayoutEffect(() => {
72
+ if (!hover) return;
73
+ const instance = createHoverCard(ref.current, { openDelay, closeDelay, placement, onOpenChange: (open) => change.current?.(open) });
74
+ return () => instance.destroy();
75
+ }, [hover, openDelay, closeDelay, placement]);
76
+ const card = <Card url={url} title={title} description={description} image={image} imageAlt={imageAlt} siteName={siteName} target={target} />;
77
+ if (!hover) {
78
+ return (
79
+ <div {...props} className={['mk-link-preview', className].filter(Boolean).join(' ')} data-mk-link-variant={variant === 'row' ? 'row' : 'card'}>
80
+ {card}
81
+ </div>
82
+ );
83
+ }
84
+ const text = label || title || linkDomain(href) || href;
85
+ return (
86
+ <span {...props} ref={ref} className={['mk-link-preview mk-hover-card', className].filter(Boolean).join(' ')} data-mk-link-variant="hover">
87
+ {href ? (
88
+ <a data-mk-hover-trigger="" href={href} target={target} rel={target === '_blank' ? 'noopener noreferrer' : undefined}>
89
+ {text}
90
+ </a>
91
+ ) : (
92
+ <span data-mk-hover-trigger="" tabIndex={0}>
93
+ {text}
94
+ </span>
95
+ )}
96
+ <span data-mk-hover-panel="" role="region" aria-label={`Vista previa: ${title || linkDomain(href)}`} hidden>
97
+ {card}
98
+ </span>
99
+ </span>
100
+ );
101
+ }
@@ -0,0 +1,45 @@
1
+ # loading-button
2
+
3
+ Botón que muestra su trabajo. Al pulsarlo (o con `run()`) hace el press de Melon Mind, su texto se desvanece con blur y entra un indicador de carga mientras el botón se desliza a su nuevo ancho (.4 s, curva swing). En éxito pasa al color de éxito con una marca que se dibuja sola (.35 s) y rebota; en error pasa al color de error, se sacude y dibuja una cruz. Tras `resetAfter` vuelve a su texto.
4
+
5
+ ## HTML / Astro
6
+
7
+ ```html
8
+ <mk-loading-button>
9
+ <button class="mk-button mk-button--primary">Guardar cambios</button>
10
+ </mk-loading-button>
11
+ <script>
12
+ const save = document.querySelector('mk-loading-button');
13
+ save.addEventListener('click', () => save.run(() => fetch('/api/guardar', { method: 'POST' })));
14
+ </script>
15
+ ```
16
+
17
+ El atributo `state` (`idle`, `loading`, `success`, `error`) lo controla desde fuera.
18
+
19
+ ## React
20
+
21
+ ```tsx
22
+ <LoadingButton className="mk-button mk-button--primary" action={() => api.save(form)} labels={{ success: 'Guardado' }}>
23
+ Guardar cambios
24
+ </LoadingButton>
25
+ ```
26
+
27
+ Con `action`, la promesa decide el resultado (se resuelve → éxito; se rechaza → error). Sin ella, `state` lo controla.
28
+
29
+ ## Opciones
30
+
31
+ | Opción | Atributo | Por defecto | Qué hace |
32
+ |---|---|---|---|
33
+ | `action` | — | — | Se ejecuta al pulsar; su promesa recorre los estados. |
34
+ | `labels` | `labels` (JSON) | Cargando… / Listo / Error | Textos que se anuncian (y se muestran si no es compacto). |
35
+ | `compact` | `compact` | `true` | Solo el icono mientras trabaja; con `false`, icono y texto. |
36
+ | `resetAfter` | `reset-after` | `1800` | ms en éxito o error antes de volver; `0` se queda. |
37
+ | `onStateChange(state)` | — | — | Cada cambio de estado. |
38
+
39
+ ## API y eventos
40
+
41
+ `setState(state)`, `run(action?)` (resuelve `true` si salió bien), `state`, `destroy()`. Evento `mk:state-change` con `{ state }`.
42
+
43
+ ## Accesibilidad y estilo
44
+
45
+ Mientras carga sigue siendo enfocable pero no responde (`aria-disabled`, `aria-busy`), y un estado discreto junto a él dice qué pasó. El botón conserva su propio estilo (`.mk-button` o el tuyo); el componente añade los colores de estado (`--mk-loading-success`, `--mk-error`) en `data-mk-state`. Con movimiento reducido el indicador queda quieto y no hay deslizamientos ni sacudida.
@@ -0,0 +1,34 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createLoadingButton, type LoadingButtonInstance, type LoadingState } from './engine';
3
+
4
+ /**
5
+ * <mk-loading-button><button class="mk-button">Guardar</button></mk-loading-button>
6
+ * The `state` attribute (idle | loading | success | error) drives it from outside; run(action) walks the states.
7
+ * Emits mk:state-change ({state}).
8
+ */
9
+ export class MkLoadingButtonElement extends MkElement<LoadingButtonInstance> {
10
+ static options: Record<string, OptionType> = { labels: 'json', compact: 'boolean', resetAfter: 'number', state: 'string' };
11
+
12
+ protected create(options: Record<string, unknown>) {
13
+ const { state, ...rest } = options as { state?: LoadingState };
14
+ const instance = createLoadingButton(this, rest);
15
+ if (state) instance.setState(state);
16
+ return instance;
17
+ }
18
+
19
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
20
+ if (previous === next || !this.instance) return;
21
+ if (name === 'state') this.instance.setState((next as LoadingState) || 'idle');
22
+ else super.attributeChangedCallback(name, previous, next);
23
+ }
24
+
25
+ run(action: () => unknown) {
26
+ return this.instance?.run(action) ?? Promise.resolve(false);
27
+ }
28
+
29
+ get state() {
30
+ return this.instance?.state ?? 'idle';
31
+ }
32
+ }
33
+
34
+ export const defineLoadingButton = () => defineElement('mk-loading-button', MkLoadingButtonElement);