@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,239 @@
1
+ /* activity-feed — day headings in small caps, entries on 12 px rows: a 2.25rem avatar (a picture, or initials on a hue
2
+ from the name) with a badge for the kind of action, the sentence, a quote on surface-3, a muted time. New entries
3
+ keep an accent-soft highlight that fades; the "novedades" pill sticks to the top. Give the root a max-height (or
4
+ --mk-af-height) to scroll inside it. */
5
+ .mk-activity-feed,
6
+ mk-activity-feed {
7
+ --mk-af-height: none;
8
+ --mk-af-avatar-l: 86%;
9
+ --mk-af-avatar-ink-l: 28%;
10
+ position: relative;
11
+ display: block;
12
+ max-height: var(--mk-af-height);
13
+ overflow-y: auto;
14
+ overscroll-behavior: contain;
15
+ color: var(--mk-text);
16
+ }
17
+
18
+ @media (prefers-color-scheme: dark) {
19
+ :where(:root:not([data-mk-mode='light'])) :is(.mk-activity-feed, mk-activity-feed) {
20
+ --mk-af-avatar-l: 28%;
21
+ --mk-af-avatar-ink-l: 86%;
22
+ }
23
+ }
24
+
25
+ :where(:root[data-mk-mode='dark']) :is(.mk-activity-feed, mk-activity-feed) {
26
+ --mk-af-avatar-l: 28%;
27
+ --mk-af-avatar-ink-l: 86%;
28
+ }
29
+
30
+ [data-mk-af-new] {
31
+ position: sticky;
32
+ top: 0.5rem;
33
+ z-index: 2;
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 0.15rem;
37
+ width: max-content;
38
+ height: 2rem;
39
+ margin: 0 auto 0.5rem;
40
+ padding: 0 0.9rem;
41
+ border: 0;
42
+ border-radius: 999px;
43
+ background: var(--mk-accent);
44
+ color: var(--mk-on-accent);
45
+ box-shadow: 0 8px 20px -8px color-mix(in srgb, var(--mk-shadow-color, #000) 45%, transparent);
46
+ font: inherit;
47
+ font-size: 0.8125rem;
48
+ font-weight: 600;
49
+ cursor: pointer;
50
+ }
51
+
52
+ [data-mk-af-new][hidden] {
53
+ display: none;
54
+ }
55
+
56
+ [data-mk-af-day] + [data-mk-af-day] {
57
+ margin-top: 1rem;
58
+ }
59
+
60
+ /* Section > heading: strong enough for a page's own h3 rules (a card's `.card h3`, say). */
61
+ [data-mk-af-day] > [data-mk-af-heading] {
62
+ margin: 0 0 0.35rem;
63
+ padding: 0 0.5rem;
64
+ color: var(--mk-muted);
65
+ font-size: 0.75rem;
66
+ font-weight: 700;
67
+ letter-spacing: 0.04em;
68
+ text-transform: uppercase;
69
+ }
70
+
71
+ [data-mk-af-day] ul {
72
+ display: grid;
73
+ gap: 0.15rem;
74
+ margin: 0;
75
+ padding: 0;
76
+ list-style: none;
77
+ }
78
+
79
+ [data-mk-af-item] {
80
+ display: grid;
81
+ grid-template-columns: 2.25rem minmax(0, 1fr);
82
+ gap: 0.75rem;
83
+ padding: 0.6rem 0.5rem;
84
+ border-radius: 12px;
85
+ outline: none;
86
+ }
87
+
88
+ [data-mk-af-item]:focus-visible {
89
+ box-shadow: inset 0 0 0 2px var(--mk-accent);
90
+ }
91
+
92
+ [data-mk-af-item][data-mk-fresh] {
93
+ animation: mk-af-fresh 2.4s ease-out both;
94
+ }
95
+
96
+ @keyframes mk-af-fresh {
97
+ from {
98
+ background: color-mix(in srgb, var(--mk-accent-soft) 90%, transparent);
99
+ }
100
+
101
+ to {
102
+ background: transparent;
103
+ }
104
+ }
105
+
106
+ [data-mk-af-avatar] {
107
+ position: relative;
108
+ display: grid;
109
+ place-items: center;
110
+ width: 2.25rem;
111
+ height: 2.25rem;
112
+ border-radius: 50%;
113
+ background: hsl(var(--mk-af-hue, 210) 55% var(--mk-af-avatar-l));
114
+ color: hsl(var(--mk-af-hue, 210) 50% var(--mk-af-avatar-ink-l));
115
+ font-size: 0.8125rem;
116
+ font-weight: 700;
117
+ }
118
+
119
+ [data-mk-af-avatar] img {
120
+ width: 100%;
121
+ height: 100%;
122
+ border-radius: 50%;
123
+ object-fit: cover;
124
+ }
125
+
126
+ [data-mk-af-badge] {
127
+ position: absolute;
128
+ right: -3px;
129
+ bottom: -3px;
130
+ display: grid;
131
+ place-items: center;
132
+ width: 1.05rem;
133
+ height: 1.05rem;
134
+ border-radius: 50%;
135
+ background: var(--mk-text);
136
+ color: var(--mk-surface-1, var(--mk-bg));
137
+ box-shadow: 0 0 0 2px var(--mk-surface-1, var(--mk-bg));
138
+ }
139
+
140
+ [data-mk-af-badge='comment'],
141
+ [data-mk-af-badge='mention'] {
142
+ background: var(--mk-accent);
143
+ color: var(--mk-on-accent);
144
+ }
145
+
146
+ [data-mk-af-badge='complete'] {
147
+ background: #1f9d5b;
148
+ color: #fff;
149
+ }
150
+
151
+ [data-mk-af-badge='delete'] {
152
+ background: var(--mk-error, #c62828);
153
+ color: var(--mk-on-error, #fff);
154
+ }
155
+
156
+ [data-mk-af-line] {
157
+ margin: 0;
158
+ font-size: 0.9375rem;
159
+ line-height: 1.45;
160
+ overflow-wrap: anywhere;
161
+ }
162
+
163
+ [data-mk-af-line] b {
164
+ font-weight: 600;
165
+ }
166
+
167
+ [data-mk-af-target] {
168
+ color: inherit;
169
+ font-weight: 600;
170
+ text-decoration: underline;
171
+ text-decoration-color: color-mix(in srgb, currentColor 30%, transparent);
172
+ text-underline-offset: 3px;
173
+ }
174
+
175
+ a[data-mk-af-target]:hover {
176
+ text-decoration-color: currentColor;
177
+ }
178
+
179
+ [data-mk-af-detail] {
180
+ margin: 0.4rem 0 0;
181
+ padding: 0.5rem 0.75rem;
182
+ border-radius: 10px;
183
+ background: var(--mk-surface-3);
184
+ font-size: 0.875rem;
185
+ line-height: 1.5;
186
+ overflow-wrap: anywhere;
187
+ }
188
+
189
+ [data-mk-af-time] {
190
+ display: block;
191
+ margin-top: 0.2rem;
192
+ color: var(--mk-muted);
193
+ font-size: 0.75rem;
194
+ }
195
+
196
+ [data-mk-af-more] {
197
+ display: block;
198
+ height: 2.25rem;
199
+ margin: 0.75rem auto 0;
200
+ padding: 0 1rem;
201
+ border: 0;
202
+ border-radius: 10px;
203
+ background: var(--mk-surface-3);
204
+ color: var(--mk-text);
205
+ font: inherit;
206
+ font-size: 0.875rem;
207
+ font-weight: 600;
208
+ cursor: pointer;
209
+ }
210
+
211
+ [data-mk-af-more][hidden],
212
+ [data-mk-af-empty][hidden] {
213
+ display: none;
214
+ }
215
+
216
+ [data-mk-af-more][aria-busy='true'] {
217
+ cursor: progress;
218
+ opacity: 0.7;
219
+ }
220
+
221
+ [data-mk-af-more]:focus-visible,
222
+ [data-mk-af-new]:focus-visible {
223
+ outline: 2px solid var(--mk-accent);
224
+ outline-offset: 2px;
225
+ }
226
+
227
+ [data-mk-af-end],
228
+ [data-mk-af-empty] {
229
+ margin: 0.75rem 0 0;
230
+ color: var(--mk-muted);
231
+ font-size: 0.875rem;
232
+ text-align: center;
233
+ }
234
+
235
+ @media (prefers-reduced-motion: reduce) {
236
+ [data-mk-af-item][data-mk-fresh] {
237
+ animation: none;
238
+ }
239
+ }
@@ -0,0 +1,36 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createActivityFeed, type ActivityFeedInstance, type ActivityItem } from './engine';
3
+
4
+ /**
5
+ * <mk-activity-feed class="mk-activity-feed" items='[{"id":"1","actor":"Ana Gómez","action":"comentó en","target":"Tarea 12","time":"2026-11-23T09:12","kind":"comment"}]'>
6
+ * </mk-activity-feed>. element.prepend([...]) / element.append([...]) for live and older activity.
7
+ */
8
+ export class MkActivityFeedElement extends MkElement<ActivityFeedInstance> {
9
+ static options: Record<string, OptionType> = { items: 'json', locale: 'string', label: 'string', emptyLabel: 'string' };
10
+
11
+ protected create(options: Record<string, unknown>) {
12
+ return createActivityFeed(this, options);
13
+ }
14
+
15
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
16
+ if (previous === next || !this.instance) return;
17
+ if (name === 'items') {
18
+ try {
19
+ this.instance.setItems(JSON.parse(next ?? '[]') as ActivityItem[]);
20
+ } catch {
21
+ // Malformed JSON: keep the current entries.
22
+ }
23
+ } else super.attributeChangedCallback(name, previous, next);
24
+ }
25
+
26
+ /** New activity on top (not Element.prepend). */
27
+ prependItems(items: ActivityItem[]) {
28
+ this.instance?.prepend(items);
29
+ }
30
+
31
+ appendItems(items: ActivityItem[]) {
32
+ this.instance?.append(items);
33
+ }
34
+ }
35
+
36
+ export const defineActivityFeed = () => defineElement('mk-activity-feed', MkActivityFeedElement);
@@ -0,0 +1,351 @@
1
+ import { gsap, EASE, Flip, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { TIMING } from '../../core/timing';
5
+ import { inert, type Instance } from '../../core/types';
6
+ import { dayKey, fromJson } from '../../core/dates';
7
+ import { dayLabel, everyMinute, fullTime, relativeTime, toTime, type TimeInput } from '../../core/relative-time';
8
+ import { blurIn, press, rollDigits } from '../../motion/micro';
9
+
10
+ /*
11
+ * Activity feed: what people did, newest first, under day headings ("Hoy",
12
+ * "Ayer", "Lunes, 12 de noviembre"). Each entry has an avatar (a picture, or
13
+ * initials on a colour from the name) with a badge for the kind of action,
14
+ * "<b>Ana</b> comentó en <a>Tarea 12</a>", a time ("hace 5 min", refreshed
15
+ * every minute) and an optional quote. New activity (prepend()) comes in at the
16
+ * top: the entries below glide down (layout spring) while the new ones drop in
17
+ * out of a blur and keep a soft highlight that fades. If the reader has
18
+ * scrolled down, a "3 novedades" pill pops in at the top instead (its count
19
+ * rolls); pressing it scrolls up and brings them in. "Cargar más" at the end
20
+ * asks onLoadMore() for older entries (working while it waits) and flows them in.
21
+ */
22
+
23
+ export interface ActivityActor {
24
+ name: string;
25
+ /** A picture URL; without it, the initials. */
26
+ avatar?: string;
27
+ }
28
+
29
+ export type ActivityKind = 'comment' | 'create' | 'update' | 'delete' | 'upload' | 'complete' | 'mention';
30
+
31
+ export interface ActivityItem {
32
+ id: string;
33
+ actor: ActivityActor | string;
34
+ /** The verb: "comentó en", "cerró", "subió". */
35
+ action: string;
36
+ /** What it was done to. */
37
+ target?: string;
38
+ /** A link for the target. */
39
+ href?: string;
40
+ time: TimeInput;
41
+ /** A quote under it (the comment). */
42
+ detail?: string;
43
+ kind?: ActivityKind;
44
+ }
45
+
46
+ export interface ActivityFeedOptions {
47
+ items?: ActivityItem[];
48
+ locale?: string;
49
+ /** Accessible name (default 'Actividad'). */
50
+ label?: string;
51
+ /** Older entries for "Cargar más": resolve with them, or with [] when there are no more. */
52
+ onLoadMore?: () => Promise<ActivityItem[]> | ActivityItem[];
53
+ emptyLabel?: string;
54
+ }
55
+
56
+ export interface ActivityFeedInstance extends Instance {
57
+ /** New activity on top (or behind the "novedades" pill when the reader is scrolled down). */
58
+ prepend(items: ActivityItem[]): void;
59
+ append(items: ActivityItem[]): void;
60
+ setItems(items: ActivityItem[]): void;
61
+ readonly items: ActivityItem[];
62
+ }
63
+
64
+ const SVG = (path: string) => `<svg aria-hidden="true" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="${path}"/></svg>`;
65
+ const KIND_ICON: Record<ActivityKind, string> = {
66
+ comment: SVG('M5 6h14v9H9l-4 4z'),
67
+ create: SVG('M12 5v14M5 12h14'),
68
+ update: SVG('M5 19l1-4L16 5l3 3L9 18z'),
69
+ delete: SVG('M6 7h12M9 7V5h6v2M8 7l1 12h6l1-12'),
70
+ upload: SVG('M12 19V6M6 12l6-6 6 6'),
71
+ complete: SVG('M5 12.5l4.5 4.5L19 7.5'),
72
+ mention: SVG('M16 12a4 4 0 1 1-1.2-2.8M16 9v4a2 2 0 0 0 4 0v-1a8 8 0 1 0-3 6.2'),
73
+ };
74
+
75
+ const nameOf = (actor: ActivityActor | string) => (typeof actor === 'string' ? actor : actor.name);
76
+ const initials = (name: string) =>
77
+ name
78
+ .split(/\s+/)
79
+ .filter(Boolean)
80
+ .slice(0, 2)
81
+ .map((part) => part[0]!.toLocaleUpperCase())
82
+ .join('');
83
+ /** A hue from the name, so a person keeps their colour. */
84
+ const hueOf = (name: string) => [...name].reduce((hash, char) => (hash * 31 + char.charCodeAt(0)) % 360, 7);
85
+
86
+ export function createActivityFeed(root: HTMLElement | null, options: ActivityFeedOptions = {}): ActivityFeedInstance {
87
+ if (!root) return inert({ prepend() {}, append() {}, setItems() {}, items: [] });
88
+ registerMotion();
89
+ const scope = createScope();
90
+ const locale = options.locale ?? root.closest('[lang]')?.getAttribute('lang') ?? 'es';
91
+ // JSON text too (data-mk-items through autoInit).
92
+ let items = [...(fromJson(options.items) ?? [])];
93
+ let waiting: ActivityItem[] = [];
94
+ const effects = new Set<gsap.core.Animation>();
95
+ const keep = (animation: gsap.core.Animation | null) => {
96
+ if (!animation) return;
97
+ effects.add(animation);
98
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
99
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
100
+ };
101
+
102
+ const pill = document.createElement('button');
103
+ pill.type = 'button';
104
+ pill.dataset.mkAfNew = '';
105
+ pill.hidden = true;
106
+ const pillCount = document.createElement('span');
107
+ pillCount.dataset.mkAfNewCount = '';
108
+ const pillText = document.createElement('span');
109
+ pill.append('↑ ', pillCount, pillText);
110
+ const feed = document.createElement('div');
111
+ feed.dataset.mkAfFeed = '';
112
+ feed.setAttribute('role', 'feed');
113
+ feed.setAttribute('aria-label', options.label ?? 'Actividad');
114
+ const empty = document.createElement('p');
115
+ empty.dataset.mkAfEmpty = '';
116
+ empty.textContent = options.emptyLabel ?? 'Sin actividad todavía';
117
+ const more = document.createElement('button');
118
+ more.type = 'button';
119
+ more.dataset.mkAfMore = '';
120
+ more.textContent = 'Cargar más';
121
+ more.hidden = !options.onLoadMore;
122
+ const status = document.createElement('span');
123
+ status.className = 'mk-sr-only';
124
+ status.setAttribute('role', 'status');
125
+ root.append(pill, feed, empty, more, status);
126
+ scope.add(() => {
127
+ for (const effect of effects) effect.kill();
128
+ effects.clear();
129
+ for (const part of [pill, feed, empty, more, status]) part.remove();
130
+ });
131
+ scope.attr(root, 'data-mk-ready', '');
132
+
133
+ const entry = (item: ActivityItem) => {
134
+ const li = document.createElement('li');
135
+ li.dataset.mkAfItem = '';
136
+ li.dataset.id = item.id;
137
+ li.setAttribute('role', 'article');
138
+ li.tabIndex = -1;
139
+ const name = nameOf(item.actor);
140
+ const avatar = document.createElement('span');
141
+ avatar.dataset.mkAfAvatar = '';
142
+ avatar.setAttribute('aria-hidden', 'true');
143
+ avatar.style.setProperty('--mk-af-hue', String(hueOf(name)));
144
+ const picture = typeof item.actor === 'string' ? undefined : item.actor.avatar;
145
+ if (picture) {
146
+ const img = document.createElement('img');
147
+ img.src = picture;
148
+ img.alt = '';
149
+ img.loading = 'lazy';
150
+ avatar.append(img);
151
+ } else avatar.textContent = initials(name);
152
+ if (item.kind) {
153
+ const badge = document.createElement('span');
154
+ badge.dataset.mkAfBadge = item.kind;
155
+ badge.innerHTML = KIND_ICON[item.kind];
156
+ avatar.append(badge);
157
+ }
158
+ const body = document.createElement('div');
159
+ body.dataset.mkAfBody = '';
160
+ const line = document.createElement('p');
161
+ line.dataset.mkAfLine = '';
162
+ const who = document.createElement('b');
163
+ who.textContent = name;
164
+ line.append(who, ` ${item.action}`);
165
+ if (item.target) {
166
+ line.append(' ');
167
+ const target = document.createElement(item.href ? 'a' : 'span');
168
+ target.dataset.mkAfTarget = '';
169
+ if (target instanceof HTMLAnchorElement) target.href = item.href!;
170
+ target.textContent = item.target;
171
+ line.append(target);
172
+ }
173
+ body.append(line);
174
+ if (item.detail) {
175
+ const quote = document.createElement('blockquote');
176
+ quote.dataset.mkAfDetail = '';
177
+ quote.textContent = item.detail;
178
+ body.append(quote);
179
+ }
180
+ const date = toTime(item.time);
181
+ if (date) {
182
+ const time = document.createElement('time');
183
+ time.dataset.mkAfTime = '';
184
+ time.dateTime = date.toISOString();
185
+ time.title = fullTime(date, locale);
186
+ time.textContent = relativeTime(date, new Date(), locale);
187
+ body.append(time);
188
+ }
189
+ li.append(avatar, body);
190
+ return li;
191
+ };
192
+
193
+ const render = () => {
194
+ const days = new Map<string, { date: Date | null; list: HTMLUListElement }>();
195
+ const sections: HTMLElement[] = [];
196
+ for (const item of items) {
197
+ const date = toTime(item.time);
198
+ const key = date ? dayKey(date) : '';
199
+ let group = days.get(key);
200
+ if (!group) {
201
+ const section = document.createElement('section');
202
+ section.dataset.mkAfDay = key;
203
+ const heading = document.createElement('h3');
204
+ heading.dataset.mkAfHeading = '';
205
+ heading.textContent = date ? dayLabel(date, new Date(), locale) : 'Sin fecha';
206
+ const list = document.createElement('ul');
207
+ section.append(heading, list);
208
+ sections.push(section);
209
+ group = { date, list };
210
+ days.set(key, group);
211
+ }
212
+ group.list.append(entry(item));
213
+ }
214
+ feed.replaceChildren(...sections);
215
+ empty.hidden = items.length > 0;
216
+ };
217
+
218
+ const refreshTimes = () => {
219
+ for (const time of feed.querySelectorAll<HTMLTimeElement>('[data-mk-af-time]')) time.textContent = relativeTime(new Date(time.dateTime), new Date(), locale);
220
+ for (const section of feed.querySelectorAll<HTMLElement>('[data-mk-af-day]')) {
221
+ const key = section.dataset.mkAfDay!;
222
+ if (key) section.querySelector('h3')!.textContent = dayLabel(toTime(key)!, new Date(), locale);
223
+ }
224
+ };
225
+ scope.add(everyMinute(refreshTimes));
226
+
227
+ /** Re-render with `change`; what stays glides (Flip), the given new ids come in from `from`. */
228
+ const reflow = (change: () => void, fresh: Set<string>, from: 'top' | 'bottom') => {
229
+ const all = () => [...feed.querySelectorAll<HTMLElement>('[data-mk-af-item], [data-mk-af-heading]')];
230
+ for (const element of all()) element.dataset.flipId = element.dataset.id ?? `day-${element.closest<HTMLElement>('[data-mk-af-day]')!.dataset.mkAfDay}`;
231
+ const state = reduced() ? null : Flip.getState(all());
232
+ change();
233
+ for (const element of all()) element.dataset.flipId = element.dataset.id ?? `day-${element.closest<HTMLElement>('[data-mk-af-day]')!.dataset.mkAfDay}`;
234
+ const arriving = [...feed.querySelectorAll<HTMLElement>('[data-mk-af-item]')].filter((li) => fresh.has(li.dataset.id!));
235
+ for (const li of arriving) if (from === 'top') li.setAttribute('data-mk-fresh', '');
236
+ if (!state) return;
237
+ keep(
238
+ Flip.from(state, {
239
+ targets: all(),
240
+ duration: TIMING.layoutTravel,
241
+ ease: EASE.layoutReflow,
242
+ onEnter: (entering) => gsap.fromTo(entering, { opacity: 0, y: from === 'top' ? -14 : 14, filter: 'blur(10px)' }, { opacity: 1, y: 0, filter: 'blur(0px)', duration: 0.45, stagger: 0.05, ease: 'power2.out', clearProps: 'opacity,transform,filter' }),
243
+ }),
244
+ );
245
+ };
246
+
247
+ const showPill = () => {
248
+ const count = waiting.length;
249
+ const text = String(count);
250
+ pillText.textContent = count === 1 ? ' novedad' : ' novedades';
251
+ status.textContent = `${count} ${count === 1 ? 'novedad' : 'novedades'}`;
252
+ if (pill.hidden) {
253
+ pill.hidden = false;
254
+ pillCount.textContent = text;
255
+ if (!reduced()) keep(gsap.fromTo(pill, { opacity: 0, y: -10, scale: 0.85, filter: 'blur(6px)' }, { opacity: 1, y: 0, scale: 1, filter: 'blur(0px)', duration: 0.35, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
256
+ } else keep(rollDigits(pillCount, text, 1, 0.3));
257
+ };
258
+
259
+ const bringIn = (fresh: ActivityItem[]) => {
260
+ const ids = new Set(fresh.map((item) => item.id));
261
+ reflow(() => {
262
+ items = [...fresh, ...items.filter((item) => !ids.has(item.id))];
263
+ render();
264
+ }, ids, 'top');
265
+ };
266
+
267
+ scope.on(pill, 'click', () => {
268
+ const fresh = waiting;
269
+ waiting = [];
270
+ const finish = () => {
271
+ pill.hidden = true;
272
+ bringIn(fresh);
273
+ feed.querySelector<HTMLElement>('[data-mk-af-item]')?.focus({ preventScroll: true });
274
+ };
275
+ if (reduced() || root.scrollTop === 0) {
276
+ root.scrollTop = 0;
277
+ return finish();
278
+ }
279
+ keep(gsap.to(root, { scrollTop: 0, duration: 0.5, ease: EASE.signature, onComplete: finish }));
280
+ });
281
+
282
+ scope.on(more, 'click', async () => {
283
+ if (!options.onLoadMore || more.getAttribute('aria-busy') === 'true') return;
284
+ keep(press(more));
285
+ more.setAttribute('aria-busy', 'true');
286
+ more.textContent = 'Cargando…';
287
+ let older: ActivityItem[] = [];
288
+ try {
289
+ older = await options.onLoadMore();
290
+ } catch {
291
+ more.textContent = 'Reintentar';
292
+ more.removeAttribute('aria-busy');
293
+ return;
294
+ }
295
+ if (scope.disposed) return;
296
+ more.removeAttribute('aria-busy');
297
+ if (!older.length) {
298
+ more.hidden = true;
299
+ status.textContent = 'No hay más actividad';
300
+ const end = document.createElement('p');
301
+ end.dataset.mkAfEnd = '';
302
+ end.textContent = 'No hay más actividad';
303
+ more.after(end);
304
+ scope.add(() => end.remove());
305
+ return;
306
+ }
307
+ more.textContent = 'Cargar más';
308
+ const known = new Set(items.map((item) => item.id));
309
+ const added = older.filter((item) => !known.has(item.id));
310
+ items = [...items, ...added];
311
+ render();
312
+ const arriving = [...feed.querySelectorAll<HTMLElement>('[data-mk-af-item]')].filter((li) => added.some((item) => item.id === li.dataset.id));
313
+ keep(blurIn(arriving, { from: 'rise', stagger: 0.04 }));
314
+ status.textContent = `${added.length} más`;
315
+ });
316
+
317
+ render();
318
+
319
+ return {
320
+ prepend(fresh) {
321
+ if (scope.disposed || !fresh.length) return;
322
+ if (root.scrollTop > 24) {
323
+ const ids = new Set(fresh.map((item) => item.id));
324
+ waiting = [...fresh, ...waiting.filter((item) => !ids.has(item.id))];
325
+ showPill();
326
+ return;
327
+ }
328
+ bringIn(fresh);
329
+ status.textContent = fresh.length === 1 ? `${nameOf(fresh[0]!.actor)} ${fresh[0]!.action}${fresh[0]!.target ? ` ${fresh[0]!.target}` : ''}` : `${fresh.length} novedades`;
330
+ },
331
+ append(older) {
332
+ if (scope.disposed) return;
333
+ const ids = new Set(older.map((item) => item.id));
334
+ reflow(() => {
335
+ items = [...items.filter((item) => !ids.has(item.id)), ...older];
336
+ render();
337
+ }, ids, 'bottom');
338
+ },
339
+ setItems(next) {
340
+ if (scope.disposed) return;
341
+ items = [...next];
342
+ waiting = [];
343
+ pill.hidden = true;
344
+ render();
345
+ },
346
+ get items() {
347
+ return [...items];
348
+ },
349
+ destroy: () => scope.dispose(),
350
+ };
351
+ }
@@ -0,0 +1,32 @@
1
+ 'use client';
2
+ import { forwardRef, useImperativeHandle, useRef, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createActivityFeed, type ActivityFeedInstance, type ActivityFeedOptions, type ActivityItem } from './engine';
5
+
6
+ export interface ActivityFeedProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, Omit<ActivityFeedOptions, 'items'> {
7
+ /** Initial entries; later activity goes through the handle (prepend / append). */
8
+ items?: ActivityItem[];
9
+ }
10
+
11
+ export interface ActivityFeedHandle {
12
+ prepend(items: ActivityItem[]): void;
13
+ append(items: ActivityItem[]): void;
14
+ }
15
+
16
+ /** <ActivityFeed ref={feed} items={recent} onLoadMore={loadOlder} />, then feed.current.prepend(live). */
17
+ export const ActivityFeed = forwardRef<ActivityFeedHandle, ActivityFeedProps>(function ActivityFeed({ items, locale, label, emptyLabel, onLoadMore, className, ...props }, handle) {
18
+ const ref = useRef<HTMLDivElement>(null);
19
+ const instance = useRef<ActivityFeedInstance | null>(null);
20
+ const load = useLatest(onLoadMore);
21
+ useImperativeHandle(handle, () => ({ prepend: (next) => instance.current?.prepend(next), append: (next) => instance.current?.append(next) }), []);
22
+ useIsomorphicLayoutEffect(() => {
23
+ const created = createActivityFeed(ref.current, { items, locale, label, emptyLabel, onLoadMore: onLoadMore ? () => load.current?.() ?? [] : undefined });
24
+ instance.current = created;
25
+ return () => {
26
+ created.destroy();
27
+ instance.current = null;
28
+ };
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ }, [locale, label, emptyLabel, Boolean(onLoadMore)]);
31
+ return <div {...props} ref={ref} className={['mk-activity-feed', className].filter(Boolean).join(' ')} />;
32
+ });
@@ -0,0 +1,27 @@
1
+ # attachment
2
+
3
+ Selector de archivos nativo con zona para arrastrar y soltar y una tarjeta por archivo (nombre, tipo, tamaño, progreso, estado y quitar). Valida `accept`, tamaño y cantidad; la subida pertenece a la app, que informa el progreso.
4
+
5
+ ```html
6
+ <mk-attachment class="mk-attachment" accept="image/*,.pdf" multiple max-files="4" max-size="5242880"></mk-attachment>
7
+ ```
8
+
9
+ ```ts
10
+ const attachment = createAttachment(root, {
11
+ accept: 'image/*,.pdf',
12
+ multiple: true,
13
+ maxFiles: 4,
14
+ maxSize: 5 * 1024 * 1024,
15
+ onFiles: (added) => added.forEach((file) => upload(file.file!, (pct) => attachment.setProgress(file.id, pct))),
16
+ });
17
+ ```
18
+
19
+ React: `<Attachment multiple accept=".pdf" onFiles={upload} uploads={{ [id]: { progress: 40 } }} />`, o `instanceRef` para llamar a la API.
20
+
21
+ Opciones: `accept` (extensiones, MIME y `tipo/*`), `multiple`, `maxFiles` (sólo multiple), `maxSize` (bytes), `disabled`, `name`, `initialFiles` (metadatos `{id?, name, size, type, preview?, progress?, status?}`), `hint`, `labels`, `onFiles(added, files)`, `onRemove(file, files)`, `onReject(rejections)`. API: `addFiles(files)` (devuelve los aceptados), `remove(id)`, `setProgress(id, 0–100, status?)`, `clear()`, `files` (copias), `destroy()`.
22
+
23
+ Eventos: `mk:files` `{added, files}`, `mk:remove` `{file, files}` y `mk:reject` `{file, code: 'type' | 'size' | 'count', message}` (uno por archivo). Los archivos válidos se conservan al añadir más; en modo simple el nuevo sustituye al anterior. Las vistas previas de imagen son object URLs que se revocan al quitar el archivo o al destruir. Los nombres se muestran siempre como texto.
24
+
25
+ `disabled` desactiva selector, soltar y quitar (también `remove()`). Con `name` y `DataTransfer` disponible, el input lleva los archivos conservados para un envío nativo. Quitar un archivo deja el foco en el siguiente botón o en el selector. `destroy()` elimina lo creado y restaura atributos. El custom element expone `addFiles`, `removeFile(id)` (`remove()` sigue siendo el de `HTMLElement`), `setProgress` y `files`, y conserva los archivos al cambiar un atributo.
26
+
27
+ Variables: `--mk-attachment-radius`, `--mk-attachment-accent`. Movimiento reducido elimina escalas y entradas.