@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,469 @@
1
+ import { gsap } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import { blurIn, blurOut, collapse, shake } from '../../motion/micro';
6
+
7
+ /*
8
+ * Attachment: native file chooser plus drop zone, and a card per file with
9
+ * name, type, size, upload progress/status and a remove button. Files are
10
+ * validated (accept, maxSize, maxFiles) and rejected with a reason; accepted
11
+ * ones are kept when more arrive. The app owns the upload: it reads
12
+ * onFiles / mk:files and reports back with setProgress(id, value, status).
13
+ * Image previews are object URLs, revoked when the file leaves or on destroy.
14
+ * Parts (made when absent): [data-mk-attachment-input], [data-mk-attachment-drop],
15
+ * [data-mk-attachment-list].
16
+ */
17
+
18
+ export type AttachmentStatus = 'idle' | 'uploading' | 'complete' | 'error';
19
+ export type AttachmentRejectCode = 'type' | 'size' | 'count';
20
+
21
+ export interface AttachmentFile {
22
+ id: string;
23
+ name: string;
24
+ size: number;
25
+ type: string;
26
+ /** The picked File (absent for files described by metadata only). */
27
+ file?: File;
28
+ /** 0–100 once the app reports it. */
29
+ progress?: number;
30
+ status: AttachmentStatus;
31
+ /** Object URL of an image preview, or a URL supplied with initialFiles. */
32
+ preview?: string;
33
+ }
34
+
35
+ export interface AttachmentRejection {
36
+ file: File;
37
+ code: AttachmentRejectCode;
38
+ message: string;
39
+ }
40
+
41
+ export interface AttachmentLabels {
42
+ choose?: string;
43
+ drop?: string;
44
+ remove?: string;
45
+ list?: string;
46
+ uploading?: string;
47
+ complete?: string;
48
+ error?: string;
49
+ added?: (count: number) => string;
50
+ removed?: (name: string) => string;
51
+ rejected?: (rejection: { name: string; code: AttachmentRejectCode; limit: string }) => string;
52
+ }
53
+
54
+ export interface AttachmentOptions {
55
+ /** Like input[accept]: extensions (.pdf), MIME types (image/png) and wildcards (image/*). */
56
+ accept?: string;
57
+ multiple?: boolean;
58
+ /** Most files kept (multiple only). */
59
+ maxFiles?: number;
60
+ /** Largest file, in bytes. */
61
+ maxSize?: number;
62
+ disabled?: boolean;
63
+ /** Form name of the file input (it carries the kept files where the browser allows). */
64
+ name?: string;
65
+ /** Files shown from the start (e.g. already uploaded). */
66
+ initialFiles?: Array<Partial<AttachmentFile> & { name: string }>;
67
+ /** Visible hint under the drop text (default: the limits). */
68
+ hint?: string;
69
+ labels?: AttachmentLabels;
70
+ onFiles?: (added: AttachmentFile[], files: AttachmentFile[]) => void;
71
+ onRemove?: (removed: AttachmentFile, files: AttachmentFile[]) => void;
72
+ onReject?: (rejections: AttachmentRejection[]) => void;
73
+ }
74
+
75
+ export interface AttachmentInstance extends Instance {
76
+ /** Validate and add files; returns the accepted ones. */
77
+ addFiles(files: Iterable<File> | ArrayLike<File>): AttachmentFile[];
78
+ remove(id: string): void;
79
+ /** Report upload progress (0–100) and, optionally, the status. */
80
+ setProgress(id: string, value: number, status?: AttachmentStatus): void;
81
+ clear(): void;
82
+ readonly files: AttachmentFile[];
83
+ }
84
+
85
+ const DEFAULT_LABELS = {
86
+ choose: 'Elige archivos',
87
+ drop: 'o arrástralos aquí',
88
+ remove: 'Quitar',
89
+ list: 'Archivos adjuntos',
90
+ uploading: 'Subiendo',
91
+ complete: 'Listo',
92
+ error: 'Error al subir',
93
+ added: (count: number) => (count === 1 ? '1 archivo añadido' : `${count} archivos añadidos`),
94
+ removed: (name: string) => `${name} quitado`,
95
+ rejected: ({ name, code, limit }: { name: string; code: AttachmentRejectCode; limit: string }) =>
96
+ code === 'type' ? `${name}: tipo no permitido` : code === 'size' ? `${name}: supera ${limit}` : `${name}: máximo ${limit}`,
97
+ } satisfies Required<AttachmentLabels>;
98
+
99
+ let sequence = 0;
100
+
101
+ /** 1 KB = 1024 B; one decimal under 10. */
102
+ export function formatBytes(bytes: number) {
103
+ if (!Number.isFinite(bytes) || bytes < 0) return '';
104
+ const units = ['B', 'KB', 'MB', 'GB'];
105
+ let value = bytes;
106
+ let unit = 0;
107
+ while (value >= 1024 && unit < units.length - 1) {
108
+ value /= 1024;
109
+ unit += 1;
110
+ }
111
+ const digits = unit && value < 10 ? 1 : 0;
112
+ return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: digits }).format(value)} ${units[unit]}`;
113
+ }
114
+
115
+ const KINDS: Record<string, string> = { image: 'imágenes', video: 'vídeos', audio: 'audio', text: 'texto' };
116
+
117
+ /** Readable accept list: image/* → imágenes, .pdf → PDF, application/zip → ZIP. */
118
+ export function describeAccept(accept: string | undefined) {
119
+ return (accept ?? '')
120
+ .split(',')
121
+ .map((rule) => rule.trim().toLowerCase())
122
+ .filter(Boolean)
123
+ .map((rule) => (rule.startsWith('.') ? rule.slice(1).toUpperCase() : rule.endsWith('/*') ? KINDS[rule.slice(0, -2)] ?? rule : (rule.split('/')[1] ?? rule).toUpperCase()))
124
+ .filter((label, i, all) => all.indexOf(label) === i)
125
+ .join(', ');
126
+ }
127
+
128
+ /** Does a file match an accept list (extensions, MIME types, type/*)? */
129
+ export function acceptsFile(file: { name: string; type: string }, accept: string | undefined) {
130
+ const rules = (accept ?? '').split(',').map((rule) => rule.trim().toLowerCase()).filter(Boolean);
131
+ if (!rules.length) return true;
132
+ const name = file.name.toLowerCase();
133
+ const type = (file.type || '').toLowerCase();
134
+ return rules.some((rule) => {
135
+ if (rule.startsWith('.')) return name.endsWith(rule);
136
+ if (rule.endsWith('/*')) return type.startsWith(rule.slice(0, -1));
137
+ return type === rule;
138
+ });
139
+ }
140
+
141
+ const extension = (name: string) => {
142
+ const dot = name.lastIndexOf('.');
143
+ return dot > 0 && dot < name.length - 1 ? name.slice(dot + 1).slice(0, 4).toUpperCase() : 'FILE';
144
+ };
145
+ const canPreview = (type: string) => /^image\//i.test(type) && typeof URL !== 'undefined' && typeof URL.createObjectURL === 'function';
146
+
147
+ export function createAttachment(root: HTMLElement | null, options: AttachmentOptions = {}): AttachmentInstance {
148
+ if (!root) return inert({ addFiles: () => [], remove() {}, setProgress() {}, clear() {}, files: [] });
149
+ const scope = createScope();
150
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
151
+ const multiple = options.multiple ?? false;
152
+ const maxFiles = multiple ? (options.maxFiles && options.maxFiles > 0 ? Math.floor(options.maxFiles) : Infinity) : 1;
153
+ const maxSize = options.maxSize && options.maxSize > 0 ? options.maxSize : Infinity;
154
+ const disabled = options.disabled ?? false;
155
+ const files: AttachmentFile[] = [];
156
+ const rows = new Map<string, HTMLElement>();
157
+ /** Rows on their way out (removed when their exit ends, or on destroy). */
158
+ const leaving = new Set<HTMLElement>();
159
+ const effects = new Set<gsap.core.Animation>();
160
+ const keep = (animation: gsap.core.Animation | null) => {
161
+ if (!animation) return;
162
+ effects.add(animation);
163
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
164
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
165
+ };
166
+ /** Object URLs this instance made (revoked when their file leaves). */
167
+ const owned = new Set<string>();
168
+
169
+ const made = <K extends keyof HTMLElementTagNameMap>(tag: K, parent: Element = root) => {
170
+ const element = document.createElement(tag);
171
+ parent.append(element);
172
+ scope.add(() => element.remove());
173
+ return element;
174
+ };
175
+
176
+ let drop = root.querySelector<HTMLElement>('[data-mk-attachment-drop]');
177
+ let input = root.querySelector<HTMLInputElement>('input[type="file"][data-mk-attachment-input], [data-mk-attachment-drop] input[type="file"]');
178
+ if (!drop) {
179
+ drop = made('label');
180
+ drop.dataset.mkAttachmentDrop = '';
181
+ const icon = document.createElement('span');
182
+ icon.dataset.mkPart = 'icon';
183
+ icon.setAttribute('aria-hidden', 'true');
184
+ const text = document.createElement('span');
185
+ text.dataset.mkPart = 'prompt';
186
+ const choose = document.createElement('strong');
187
+ choose.textContent = labels.choose;
188
+ text.append(choose, ` ${labels.drop}`);
189
+ const hint = document.createElement('small');
190
+ hint.dataset.mkPart = 'hint';
191
+ const kinds = describeAccept(options.accept);
192
+ const limits = [kinds && kinds.charAt(0).toUpperCase() + kinds.slice(1), Number.isFinite(maxSize) ? `hasta ${formatBytes(maxSize)}` : '', Number.isFinite(maxFiles) && multiple ? `máx. ${maxFiles} archivos` : '']
193
+ .filter(Boolean)
194
+ .join(' · ');
195
+ hint.textContent = options.hint ?? limits;
196
+ drop.append(icon, text);
197
+ if (hint.textContent) drop.append(hint);
198
+ }
199
+ if (!input) {
200
+ input = made('input', drop);
201
+ input.type = 'file';
202
+ input.dataset.mkAttachmentInput = '';
203
+ }
204
+ let list = root.querySelector<HTMLElement>('[data-mk-attachment-list]');
205
+ if (!list) {
206
+ list = made('ul');
207
+ list.dataset.mkAttachmentList = '';
208
+ }
209
+ const error = made('p');
210
+ error.dataset.mkAttachmentError = '';
211
+ error.setAttribute('role', 'alert');
212
+ error.hidden = true;
213
+ const status = made('p');
214
+ status.dataset.mkAttachmentStatus = '';
215
+ status.className = 'mk-sr-only';
216
+ status.setAttribute('role', 'status');
217
+
218
+ scope.attr(input, 'accept', options.accept ?? input.getAttribute('accept'));
219
+ scope.attr(input, 'multiple', multiple ? '' : null);
220
+ scope.attr(input, 'disabled', disabled ? '' : null);
221
+ if (options.name) scope.attr(input, 'name', options.name);
222
+ scope.attr(list, 'aria-label', list.getAttribute('aria-label') ?? labels.list);
223
+ scope.attr(root, 'data-mk-ready', '');
224
+ scope.attr(root, 'data-mk-disabled', disabled ? '' : null);
225
+
226
+ const snapshot = (file: AttachmentFile): AttachmentFile => ({ ...file });
227
+ const all = () => files.map(snapshot);
228
+ const announce = (text: string) => {
229
+ status.textContent = text;
230
+ };
231
+
232
+ const describe = (file: AttachmentFile) => {
233
+ const parts = [extension(file.name), formatBytes(file.size)];
234
+ if (file.status === 'uploading') parts.push(`${labels.uploading} ${Math.round(file.progress ?? 0)} %`);
235
+ else if (file.status === 'complete') parts.push(labels.complete);
236
+ else if (file.status === 'error') parts.push(labels.error);
237
+ return parts.filter(Boolean).join(' · ');
238
+ };
239
+
240
+ const paint = (file: AttachmentFile) => {
241
+ const row = rows.get(file.id);
242
+ if (!row) return;
243
+ row.dataset.mkStatus = file.status;
244
+ row.querySelector<HTMLElement>('[data-mk-part="detail"]')!.textContent = describe(file);
245
+ const progress = row.querySelector('progress')!;
246
+ progress.hidden = file.progress === undefined || file.status === 'idle';
247
+ progress.value = file.progress ?? 0;
248
+ };
249
+
250
+ const renderRow = (file: AttachmentFile) => {
251
+ const row = document.createElement('li');
252
+ row.dataset.mkAttachmentItem = '';
253
+ row.dataset.mkId = file.id;
254
+ const thumb = document.createElement('span');
255
+ thumb.dataset.mkPart = 'thumb';
256
+ thumb.setAttribute('aria-hidden', 'true');
257
+ if (file.preview) {
258
+ const image = document.createElement('img');
259
+ image.alt = '';
260
+ image.src = file.preview;
261
+ image.addEventListener('error', () => image.replaceWith(extension(file.name)), { once: true });
262
+ thumb.append(image);
263
+ } else thumb.textContent = extension(file.name);
264
+ const meta = document.createElement('span');
265
+ meta.dataset.mkPart = 'meta';
266
+ const name = document.createElement('span');
267
+ name.dataset.mkPart = 'name';
268
+ name.textContent = file.name;
269
+ name.title = file.name;
270
+ const detail = document.createElement('span');
271
+ detail.dataset.mkPart = 'detail';
272
+ const progress = document.createElement('progress');
273
+ progress.max = 100;
274
+ progress.setAttribute('aria-label', `${labels.uploading} ${file.name}`);
275
+ meta.append(name, detail, progress);
276
+ const remove = document.createElement('button');
277
+ remove.type = 'button';
278
+ remove.dataset.mkAttachmentRemove = '';
279
+ remove.setAttribute('aria-label', `${labels.remove} ${file.name}`);
280
+ remove.disabled = disabled;
281
+ remove.innerHTML = '<svg aria-hidden="true" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>';
282
+ row.append(thumb, meta, remove);
283
+ list!.append(row);
284
+ rows.set(file.id, row);
285
+ paint(file);
286
+ };
287
+
288
+ const revoke = (file: AttachmentFile) => {
289
+ if (file.preview && owned.delete(file.preview)) URL.revokeObjectURL(file.preview);
290
+ };
291
+
292
+ /** Where the browser allows it, the input carries the kept files for a native form submit. */
293
+ const syncInput = () => {
294
+ if (!options.name || typeof DataTransfer === 'undefined') return;
295
+ try {
296
+ const transfer = new DataTransfer();
297
+ for (const file of files) if (file.file) transfer.items.add(file.file);
298
+ input!.files = transfer.files;
299
+ } catch {
300
+ // Older engines cannot assign files: the app reads them from onFiles.
301
+ }
302
+ };
303
+
304
+ const discard = (file: AttachmentFile, notify: boolean) => {
305
+ const at = files.indexOf(file);
306
+ if (at < 0) return;
307
+ files.splice(at, 1);
308
+ revoke(file);
309
+ const row = rows.get(file.id);
310
+ rows.delete(file.id);
311
+ // Keep the keyboard in the list: the next remove button, else the chooser.
312
+ const focused = row?.contains(document.activeElement);
313
+ const next = [row?.nextElementSibling, row?.previousElementSibling].find((sibling) => sibling && !leaving.has(sibling as HTMLElement));
314
+ if (row) {
315
+ // Out of reach at once; it slides left, blurs and folds (Melonmind item-out-left + collapse).
316
+ row.inert = true;
317
+ row.setAttribute('aria-hidden', 'true');
318
+ leaving.add(row);
319
+ const done = () => {
320
+ leaving.delete(row);
321
+ row.remove();
322
+ };
323
+ if (reduced()) done();
324
+ else keep(gsap.timeline({ onComplete: done }).add(blurOut(row, { to: 'left' }) ?? gsap.timeline()).add(collapse(row) ?? gsap.timeline(), 0.18));
325
+ }
326
+ if (focused) (next?.querySelector<HTMLElement>('[data-mk-attachment-remove]') ?? input)?.focus();
327
+ syncInput();
328
+ if (!notify) return;
329
+ announce(labels.removed(file.name));
330
+ const detail = { file: snapshot(file), files: all() };
331
+ options.onRemove?.(detail.file, detail.files);
332
+ root.dispatchEvent(new CustomEvent('mk:remove', { bubbles: true, detail }));
333
+ };
334
+
335
+ const create = (seed: Partial<AttachmentFile> & { name: string }, source?: File): AttachmentFile => {
336
+ const type = seed.type ?? source?.type ?? '';
337
+ let preview = seed.preview;
338
+ // A blob URL carried over from a destroyed instance was revoked with it: make a new one.
339
+ if (source && canPreview(type) && (!preview || preview.startsWith('blob:'))) {
340
+ preview = URL.createObjectURL(source);
341
+ owned.add(preview);
342
+ }
343
+ const progress = Number.isFinite(seed.progress) ? Math.min(100, Math.max(0, seed.progress!)) : undefined;
344
+ return { id: seed.id ?? `mk-file-${++sequence}`, name: seed.name, size: seed.size ?? source?.size ?? 0, type, file: source ?? seed.file, progress, status: seed.status ?? 'idle', preview };
345
+ };
346
+
347
+ const addFiles = (incoming: Iterable<File> | ArrayLike<File>) => {
348
+ if (scope.disposed || disabled) return [];
349
+ const incomingFiles = Array.from(incoming as ArrayLike<File>);
350
+ const accepted: AttachmentFile[] = [];
351
+ const rejected: AttachmentRejection[] = [];
352
+ const reject = (file: File, code: AttachmentRejectCode) => {
353
+ const limit = code === 'size' ? formatBytes(maxSize) : code === 'count' ? `${maxFiles} ${maxFiles === 1 ? 'archivo' : 'archivos'}` : options.accept ?? '';
354
+ rejected.push({ file, code, message: labels.rejected({ name: file.name, code, limit }) });
355
+ };
356
+ for (const file of incomingFiles) {
357
+ if (!acceptsFile(file, options.accept)) reject(file, 'type');
358
+ else if (file.size > maxSize) reject(file, 'size');
359
+ else if (multiple && files.length + accepted.length >= maxFiles) reject(file, 'count');
360
+ else if (!multiple && accepted.length) reject(file, 'count');
361
+ else accepted.push(create({ name: file.name }, file));
362
+ }
363
+ // One file at a time: a new valid pick replaces the current one.
364
+ if (!multiple && accepted.length) for (const old of [...files]) discard(old, true);
365
+ for (const file of accepted) {
366
+ files.push(file);
367
+ renderRow(file);
368
+ }
369
+ keep(blurIn(accepted.map((file) => rows.get(file.id)!).filter(Boolean)));
370
+ if (rejected.length) keep(shake(drop));
371
+ error.hidden = !rejected.length;
372
+ error.textContent = rejected.map((item) => item.message).join(' · ');
373
+ if (accepted.length) {
374
+ syncInput();
375
+ announce(labels.added(accepted.length));
376
+ const detail = { added: accepted.map(snapshot), files: all() };
377
+ options.onFiles?.(detail.added, detail.files);
378
+ root.dispatchEvent(new CustomEvent('mk:files', { bubbles: true, detail }));
379
+ }
380
+ if (rejected.length) {
381
+ options.onReject?.(rejected);
382
+ for (const rejection of rejected) root.dispatchEvent(new CustomEvent('mk:reject', { bubbles: true, detail: rejection }));
383
+ }
384
+ return accepted.map(snapshot);
385
+ };
386
+
387
+ for (const seed of options.initialFiles ?? []) {
388
+ const file = create(seed, seed.file);
389
+ files.push(file);
390
+ renderRow(file);
391
+ }
392
+
393
+ scope.on(input, 'change', () => {
394
+ if (disabled || !input!.files?.length) return;
395
+ const picked = [...input!.files];
396
+ addFiles(picked);
397
+ // Without DataTransfer the same file could not be picked twice: clear the pick.
398
+ if (!options.name || typeof DataTransfer === 'undefined') input!.value = '';
399
+ });
400
+ scope.on(list, 'click', (event) => {
401
+ const button = (event.target as Element | null)?.closest<HTMLButtonElement>('[data-mk-attachment-remove]');
402
+ const id = button?.closest<HTMLElement>('[data-mk-attachment-item]')?.dataset.mkId;
403
+ if (!button || button.disabled || !id) return;
404
+ const file = files.find((item) => item.id === id);
405
+ if (file) discard(file, true);
406
+ });
407
+ let depth = 0;
408
+ const dragging = (on: boolean) => scope.attr(drop, 'data-mk-dragging', on ? '' : null);
409
+ const hasFiles = (event: Event) => [...((event as DragEvent).dataTransfer?.types ?? ['Files'])].includes('Files');
410
+ scope.on(drop, 'dragenter', (event) => {
411
+ if (disabled || !hasFiles(event)) return;
412
+ event.preventDefault();
413
+ depth += 1;
414
+ dragging(true);
415
+ });
416
+ scope.on(drop, 'dragover', (event) => {
417
+ if (disabled || !hasFiles(event)) return;
418
+ event.preventDefault();
419
+ const transfer = (event as DragEvent).dataTransfer;
420
+ if (transfer) transfer.dropEffect = 'copy';
421
+ });
422
+ scope.on(drop, 'dragleave', () => {
423
+ depth = Math.max(0, depth - 1);
424
+ if (!depth) dragging(false);
425
+ });
426
+ scope.on(drop, 'drop', (event) => {
427
+ event.preventDefault();
428
+ depth = 0;
429
+ dragging(false);
430
+ if (disabled) return;
431
+ const dropped = (event as DragEvent).dataTransfer?.files;
432
+ if (dropped?.length) addFiles(dropped);
433
+ });
434
+ scope.add(() => {
435
+ for (const effect of effects) effect.kill();
436
+ effects.clear();
437
+ for (const row of leaving) row.remove();
438
+ leaving.clear();
439
+ for (const file of files) revoke(file);
440
+ for (const row of rows.values()) row.remove();
441
+ rows.clear();
442
+ });
443
+
444
+ return {
445
+ addFiles,
446
+ remove(id) {
447
+ if (scope.disposed || disabled) return;
448
+ const file = files.find((item) => item.id === id);
449
+ if (file) discard(file, true);
450
+ },
451
+ setProgress(id, value, nextStatus) {
452
+ if (scope.disposed) return;
453
+ const file = files.find((item) => item.id === id);
454
+ if (!file || !Number.isFinite(value)) return;
455
+ file.progress = Math.min(100, Math.max(0, value));
456
+ file.status = nextStatus ?? (file.progress >= 100 ? 'complete' : 'uploading');
457
+ paint(file);
458
+ if (file.status === 'complete' || file.status === 'error') announce(`${file.name}: ${file.status === 'complete' ? labels.complete : labels.error}`);
459
+ },
460
+ clear() {
461
+ if (scope.disposed || disabled) return;
462
+ for (const file of [...files]) discard(file, true);
463
+ },
464
+ get files() {
465
+ return all();
466
+ },
467
+ destroy: () => scope.dispose(),
468
+ };
469
+ }
@@ -0,0 +1,65 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createAttachment, type AttachmentFile, type AttachmentInstance, type AttachmentOptions, type AttachmentStatus } from './engine';
5
+
6
+ export interface AttachmentUpload {
7
+ progress: number;
8
+ status?: AttachmentStatus;
9
+ }
10
+
11
+ export interface AttachmentProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, AttachmentOptions {
12
+ /** Upload state by file id, reported by the app (from onFiles ids). */
13
+ uploads?: Record<string, AttachmentUpload>;
14
+ /** Receives the live instance (addFiles, remove, setProgress…). */
15
+ instanceRef?: MutableRefObject<AttachmentInstance | null>;
16
+ }
17
+
18
+ /**
19
+ * <Attachment multiple accept="image/*,.pdf" maxSize={5_000_000} onFiles={upload} uploads={progressById} />.
20
+ * The engine draws the chooser and cards; picked files survive rerenders and option changes.
21
+ */
22
+ export function Attachment({ accept, multiple, maxFiles, maxSize, disabled, name, initialFiles, hint, labels, onFiles, onRemove, onReject, uploads, instanceRef, className, ...props }: AttachmentProps) {
23
+ const ref = useRef<HTMLDivElement>(null);
24
+ const instance = useRef<AttachmentInstance | null>(null);
25
+ const kept = useRef<AttachmentFile[] | undefined>(undefined);
26
+ const files = useLatest(onFiles);
27
+ const removed = useLatest(onRemove);
28
+ const rejected = useLatest(onReject);
29
+ const latestUploads = useLatest(uploads);
30
+ const seed = useLatest(initialFiles);
31
+ const labelKey = JSON.stringify(labels ?? {});
32
+ useIsomorphicLayoutEffect(() => {
33
+ const created = createAttachment(ref.current, {
34
+ accept,
35
+ multiple,
36
+ maxFiles,
37
+ maxSize,
38
+ disabled,
39
+ name,
40
+ hint,
41
+ labels,
42
+ initialFiles: kept.current ?? seed.current,
43
+ onFiles: (added, all) => files.current?.(added, all),
44
+ onRemove: (file, all) => removed.current?.(file, all),
45
+ onReject: (list) => rejected.current?.(list),
46
+ });
47
+ for (const [id, upload] of Object.entries(latestUploads.current ?? {})) created.setProgress(id, upload.progress, upload.status);
48
+ instance.current = created;
49
+ if (instanceRef) instanceRef.current = created;
50
+ return () => {
51
+ kept.current = created.files;
52
+ created.destroy();
53
+ instance.current = null;
54
+ if (instanceRef) instanceRef.current = null;
55
+ };
56
+ // labels are compared by content (labelKey); callbacks are read through refs.
57
+ // eslint-disable-next-line react-hooks/exhaustive-deps
58
+ }, [accept, multiple, maxFiles, maxSize, disabled, name, hint, labelKey]);
59
+ const uploadKey = JSON.stringify(uploads ?? {});
60
+ useIsomorphicLayoutEffect(() => {
61
+ for (const [id, upload] of Object.entries(uploads ?? {})) instance.current?.setProgress(id, upload.progress, upload.status);
62
+ // eslint-disable-next-line react-hooks/exhaustive-deps
63
+ }, [uploadKey]);
64
+ return <div {...props} ref={ref} className={['mk-attachment', className].filter(Boolean).join(' ')} />;
65
+ }
@@ -0,0 +1,38 @@
1
+ # breadcrumb
2
+
3
+ Ruta de navegación jerárquica: una lista ordenada dentro de `<nav aria-label>`, donde el último elemento es la página actual (`aria-current="page"`). Cuando no cabe (o pasa de `maxItems`), los niveles intermedios se pliegan en un botón «…»; al pulsarlo aparecen en su sitio, entrando desde la izquierda con blur (40 ms entre niveles) mientras el resto se desliza para hacerles hueco.
4
+
5
+ `setItems()` conserva el principio común de la ruta: lo que se queda se desliza a su nuevo sitio (resorte de reflujo), lo nuevo entra desde la derecha y la antigua página actual se vuelve enlace. Los enlaces se subrayan desde la izquierda al señalarlos.
6
+
7
+ ## HTML / Astro
8
+
9
+ ```html
10
+ <mk-breadcrumb class="mk-breadcrumb"
11
+ items='[{"label":"Inicio","href":"/"},{"label":"Proyectos","href":"/proyectos"},{"label":"Melon"}]'></mk-breadcrumb>
12
+
13
+ <!-- O con enlaces escritos: -->
14
+ <div class="mk-breadcrumb" data-mk="breadcrumb"><a href="/">Inicio</a><a href="/docs">Docs</a><span>API</span></div>
15
+ ```
16
+
17
+ ## React
18
+
19
+ ```tsx
20
+ <Breadcrumb items={path} onNavigate={(item, index, event) => { event.preventDefault(); router.push(item.href!); }} />
21
+ ```
22
+
23
+ ## Opciones
24
+
25
+ | Opción | Atributo | Por defecto | Qué hace |
26
+ |---|---|---|---|
27
+ | `items` | `items` (JSON) | lo escrito dentro | `{ label, href? }[]`; el último es la página actual. |
28
+ | `label` | `label` | `'Ruta de navegación'` | Nombre accesible del `<nav>`. |
29
+ | `maxItems` | `max-items` | `'auto'` | Número máximo de niveles visibles; `auto` pliega solo cuando no cabe. |
30
+ | `onNavigate(item, index, event)` | — | — | Antes de seguir un enlace; `event.preventDefault()` deja la navegación a tu router. |
31
+
32
+ ## API y eventos
33
+
34
+ `setItems(items)`, `items`, `destroy()`. Evento `mk:navigate` con `{ item, index }`, cancelable.
35
+
36
+ ## Estilo
37
+
38
+ Enlaces atenuados con chevrons entre niveles; la página actual en el color del texto y con elipsis como último recurso; el «…» es un botón de 8 px sobre `--mk-surface-3`. Con movimiento reducido todo cambia sin transición.
@@ -0,0 +1,109 @@
1
+ /* breadcrumb — muted links separated by chevrons, the current page in the text colour; links underline from the left
2
+ on hover (.25 s, wipe curve); the folded middle is a small 8 px "…" button on surface-3. The row never scrolls: it
3
+ folds, and the current page ellipsizes as a last resort. */
4
+ .mk-breadcrumb,
5
+ mk-breadcrumb {
6
+ display: block;
7
+ min-width: 0;
8
+ }
9
+
10
+ [data-mk-breadcrumb-list] {
11
+ display: flex;
12
+ flex-wrap: nowrap;
13
+ align-items: center;
14
+ gap: 0.25rem;
15
+ min-width: 0;
16
+ margin: 0;
17
+ padding: 0;
18
+ overflow: hidden;
19
+ color: var(--mk-muted);
20
+ font-size: 0.875rem;
21
+ line-height: 1.4;
22
+ list-style: none;
23
+ }
24
+
25
+ [data-mk-breadcrumb-nav][data-mk-expanded] [data-mk-breadcrumb-list] {
26
+ flex-wrap: wrap;
27
+ overflow: visible;
28
+ }
29
+
30
+ [data-mk-crumb] {
31
+ display: inline-flex;
32
+ flex: none;
33
+ align-items: center;
34
+ gap: 0.25rem;
35
+ min-width: 0;
36
+ }
37
+
38
+ [data-mk-crumb]:last-child {
39
+ flex: 0 1 auto;
40
+ }
41
+
42
+ /* While the engine measures, the current page keeps its full width. */
43
+ [data-mk-breadcrumb-nav][data-mk-measuring] [data-mk-crumb]:last-child {
44
+ flex: none;
45
+ }
46
+
47
+ [data-mk-crumb] + [data-mk-crumb]::before {
48
+ content: '';
49
+ flex: none;
50
+ width: 1rem;
51
+ height: 1rem;
52
+ background: currentColor;
53
+ opacity: 0.6;
54
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 1rem no-repeat;
55
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M9 6l6 6-6 6' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 1rem no-repeat;
56
+ }
57
+
58
+ [data-mk-crumb-link] {
59
+ padding: 0.15rem 0;
60
+ background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat;
61
+ color: inherit;
62
+ text-decoration: none;
63
+ transition: background-size 0.25s cubic-bezier(0.38, 0.49, 0, 1), color 0.2s ease;
64
+ }
65
+
66
+ [data-mk-crumb-link]:hover {
67
+ background-size: 100% 1px;
68
+ color: var(--mk-text);
69
+ }
70
+
71
+ [data-mk-crumb-link]:focus-visible,
72
+ [data-mk-crumb-more]:focus-visible {
73
+ border-radius: 6px;
74
+ outline: 2px solid var(--mk-accent);
75
+ outline-offset: 2px;
76
+ }
77
+
78
+ [data-mk-crumb-current] {
79
+ overflow: hidden;
80
+ color: var(--mk-text);
81
+ font-weight: 600;
82
+ text-overflow: ellipsis;
83
+ white-space: nowrap;
84
+ }
85
+
86
+ [data-mk-crumb-more] {
87
+ height: 1.5rem;
88
+ padding: 0 0.45rem;
89
+ border: 0;
90
+ border-radius: 8px;
91
+ background: var(--mk-surface-3);
92
+ color: inherit;
93
+ font: inherit;
94
+ line-height: 1;
95
+ cursor: pointer;
96
+ transition: background-color 0.2s ease, color 0.2s ease;
97
+ }
98
+
99
+ [data-mk-crumb-more]:hover {
100
+ background: var(--mk-surface-4, var(--mk-surface-3));
101
+ color: var(--mk-text);
102
+ }
103
+
104
+ @media (prefers-reduced-motion: reduce) {
105
+ [data-mk-crumb-link],
106
+ [data-mk-crumb-more] {
107
+ transition: none;
108
+ }
109
+ }