@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,74 @@
1
+ /* scratch-card — a Melonmind card (20 px radius) whose content hides under a metallic cover; a small glass
2
+ "Revelar" pill sits on top for the keyboard. */
3
+ .mk-scratch-card,
4
+ mk-scratch-card {
5
+ position: relative;
6
+ display: block;
7
+ overflow: hidden;
8
+ border-radius: 20px;
9
+ background: var(--mk-surface-1);
10
+ box-shadow: inset 0 0 0 1px var(--mk-outline-soft);
11
+ isolation: isolate;
12
+ }
13
+
14
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-content] {
15
+ position: relative;
16
+ min-height: 9rem;
17
+ }
18
+
19
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover] {
20
+ position: absolute;
21
+ inset: 0;
22
+ z-index: 1;
23
+ cursor: crosshair;
24
+ touch-action: none;
25
+ }
26
+
27
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover] canvas {
28
+ display: block;
29
+ width: 100%;
30
+ height: 100%;
31
+ }
32
+
33
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover][data-mk-scratch-fallback] {
34
+ display: grid;
35
+ place-items: center;
36
+ background: linear-gradient(135deg, #c9ccd2, #eef0f3 45%, #d7dadf 55%, #a9adb5);
37
+ color: rgb(40 42 48 / 0.75);
38
+ font-weight: 600;
39
+ cursor: default;
40
+ }
41
+
42
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-cover][hidden] {
43
+ display: none;
44
+ }
45
+
46
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-reveal] {
47
+ position: absolute;
48
+ right: 0.75rem;
49
+ bottom: 0.75rem;
50
+ z-index: 2;
51
+ min-height: 2.25rem;
52
+ padding: 0 0.9rem;
53
+ border: 0;
54
+ border-radius: 64px;
55
+ background: rgb(20 21 24 / 0.72);
56
+ box-shadow:
57
+ inset 1px 1px 1px rgb(255 255 255 / 0.14),
58
+ inset -1px -1px 1px rgb(255 255 255 / 0.05);
59
+ backdrop-filter: blur(12px);
60
+ color: #fff;
61
+ font: inherit;
62
+ font-size: 0.8125rem;
63
+ font-weight: 500;
64
+ cursor: pointer;
65
+ }
66
+
67
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-reveal]:focus-visible {
68
+ outline: 2px solid var(--mk-accent);
69
+ outline-offset: 2px;
70
+ }
71
+
72
+ :is(.mk-scratch-card, mk-scratch-card) [data-mk-scratch-reveal][hidden] {
73
+ display: none;
74
+ }
@@ -0,0 +1,15 @@
1
+ # skeleton
2
+
3
+ Marcador de carga para texto, avatar, imagen o tarjeta. El placeholder es decorativo (`aria-hidden`) y el root marca `aria-busy` con un estado oculto que nombra lo que carga. El contenido real queda oculto sólo mientras carga.
4
+
5
+ ```html
6
+ <mk-skeleton class="mk-skeleton" shape="card" animation="shimmer" label="Cargando perfil">
7
+ <article data-mk-skeleton-content>…contenido real…</article>
8
+ </mk-skeleton>
9
+ ```
10
+
11
+ Con `[data-mk-skeleton-placeholder]` propio se usa ese marcado; sin él, el motor crea los huesos de `shape`. `defineElements()` (atributo `loaded` para terminar), `createSkeleton(root, options)` o `<Skeleton loading={pending} shape="avatar" label="Cargando autor"><Author /></Skeleton>`.
12
+
13
+ Opciones: `loading=true`, `animation='pulse'|'shimmer'|'none'`, `shape='text'|'avatar'|'image'|'card'`, `lines=3` (texto), `label='Cargando'`. API: `setLoading(boolean)`, `loading`, `destroy()`; evento `mk:loading` con `{loading}`. `skeletonBones(shape, lines)` devuelve los huesos para dibujar el mismo placeholder desde otro framework.
14
+
15
+ El contenido que el autor ya tenía `hidden` sigue oculto al terminar. En React, placeholder y contenido son subárboles distintos de React; el motor sólo cambia `hidden` y el estado. `destroy()` quita lo generado y restaura atributos. Variables: `--mk-skeleton-bone`, `--mk-skeleton-sheen`, `--mk-skeleton-radius`, `--mk-skeleton-duration` (1.6s). Movimiento reducido: superficie estática, sin pulso ni brillo.
@@ -0,0 +1,32 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createSkeleton, type SkeletonInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-skeleton class="mk-skeleton" shape="card" label="Cargando perfil">
6
+ * <article data-mk-skeleton-content>…</article>
7
+ * </mk-skeleton>
8
+ * Loading until `loaded` is set (or `setLoading(false)`); a placeholder is made from `shape`.
9
+ */
10
+ export class MkSkeletonElement extends MkElement<SkeletonInstance> {
11
+ static options: Record<string, OptionType> = { loaded: 'boolean', animation: 'string', shape: 'string', lines: 'number', label: 'string' };
12
+
13
+ protected create(options: Record<string, unknown>) {
14
+ return createSkeleton(this, { ...options, loading: !options.loaded });
15
+ }
16
+
17
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
18
+ if (previous === next || !this.instance) return;
19
+ if (name === 'loaded') this.instance.setLoading(next === null || next === 'false');
20
+ else super.attributeChangedCallback(name, previous, next);
21
+ }
22
+
23
+ setLoading(loading: boolean) {
24
+ this.instance?.setLoading(loading);
25
+ }
26
+
27
+ get loading() {
28
+ return this.instance?.loading ?? false;
29
+ }
30
+ }
31
+
32
+ export const defineSkeleton = () => defineElement('mk-skeleton', MkSkeletonElement);
@@ -0,0 +1,104 @@
1
+ import type { gsap } from '../../core/gsap';
2
+ import { createScope } from '../../core/scope';
3
+ import { inert, type Instance } from '../../core/types';
4
+ import { blurIn } from '../../motion/micro';
5
+
6
+ /*
7
+ * Skeleton: a decorative placeholder ([data-mk-skeleton-placeholder], made from
8
+ * `shape` when absent) stands in for the real content ([data-mk-skeleton-content])
9
+ * while loading. The root is aria-busy and a hidden status names what loads; the
10
+ * placeholder is aria-hidden. Content the author hid stays hidden. Pulse and
11
+ * shimmer are CSS; reduced motion keeps a still surface.
12
+ */
13
+
14
+ export type SkeletonShape = 'text' | 'avatar' | 'image' | 'card';
15
+ export type SkeletonAnimation = 'pulse' | 'shimmer' | 'none';
16
+
17
+ export interface SkeletonOptions {
18
+ loading?: boolean;
19
+ animation?: SkeletonAnimation;
20
+ /** Placeholder made when the markup has none. */
21
+ shape?: SkeletonShape;
22
+ /** Text lines of a generated text placeholder (default 3). */
23
+ lines?: number;
24
+ /** Announced while loading (default 'Cargando'). */
25
+ label?: string;
26
+ }
27
+
28
+ export interface SkeletonInstance extends Instance {
29
+ setLoading(loading: boolean): void;
30
+ readonly loading: boolean;
31
+ }
32
+
33
+ /** Bones of each shape: block (image), circle (avatar), line (text). */
34
+ export function skeletonBones(shape: SkeletonShape, lines = 3): Array<'block' | 'circle' | 'line' | 'short'> {
35
+ const count = Math.max(1, Math.round(lines) || 1);
36
+ const text = Array.from({ length: count }, (_, i) => (i === count - 1 && count > 1 ? 'short' : 'line') as 'line' | 'short');
37
+ if (shape === 'avatar') return ['circle', 'line', 'short'];
38
+ if (shape === 'image') return ['block'];
39
+ if (shape === 'card') return ['block', 'line', 'short'];
40
+ return text;
41
+ }
42
+
43
+ export function createSkeleton(root: HTMLElement | null, options: SkeletonOptions = {}): SkeletonInstance {
44
+ if (!root) return inert({ setLoading() {}, loading: false });
45
+ const scope = createScope();
46
+ const shape = options.shape ?? 'text';
47
+ const animation = options.animation ?? 'pulse';
48
+ let loading = options.loading ?? true;
49
+
50
+ let placeholder = root.querySelector<HTMLElement>('[data-mk-skeleton-placeholder]');
51
+ if (!placeholder) {
52
+ const made = document.createElement('div');
53
+ made.dataset.mkSkeletonPlaceholder = '';
54
+ made.dataset.mkShape = shape;
55
+ for (const bone of skeletonBones(shape, options.lines)) {
56
+ const element = document.createElement('span');
57
+ element.dataset.mkBone = bone;
58
+ made.append(element);
59
+ }
60
+ root.prepend(made);
61
+ scope.add(() => made.remove());
62
+ placeholder = made;
63
+ }
64
+ const contents = [...root.querySelectorAll<HTMLElement>('[data-mk-skeleton-content]')];
65
+ const authoredHidden = new Map(contents.map((content) => [content, content.hidden]));
66
+ const status = document.createElement('span');
67
+ status.className = 'mk-sr-only';
68
+ status.dataset.mkSkeletonStatus = '';
69
+ root.append(status);
70
+ scope.add(() => status.remove());
71
+
72
+ scope.attr(placeholder, 'aria-hidden', 'true');
73
+ scope.attr(root, 'data-mk-shape', shape);
74
+ scope.attr(root, 'data-mk-animation', animation);
75
+ scope.attr(root, 'data-mk-ready', '');
76
+
77
+ // The reveal animates the content's inline style: the authored one comes back on destroy.
78
+ for (const content of contents) scope.attr(content, 'style', content.getAttribute('style'));
79
+ let reveal: gsap.core.Animation | null = null;
80
+ scope.add(() => reveal?.progress(1).kill());
81
+ const render = () => {
82
+ scope.attr(root, 'aria-busy', String(loading));
83
+ scope.attr(placeholder!, 'hidden', loading ? null : '');
84
+ for (const content of contents) scope.attr(content, 'hidden', loading || authoredHidden.get(content) ? '' : null);
85
+ status.textContent = loading ? options.label ?? 'Cargando' : '';
86
+ };
87
+ render();
88
+
89
+ return {
90
+ setLoading(next) {
91
+ if (scope.disposed || next === loading) return;
92
+ loading = next;
93
+ render();
94
+ // Melonmind's list entrance (blur 16, y −20, 50 ms apart) for what was waiting.
95
+ reveal?.progress(1).kill();
96
+ if (!loading) reveal = blurIn(contents.filter((content) => !content.hidden), { stagger: 0.05 });
97
+ root.dispatchEvent(new CustomEvent('mk:loading', { bubbles: true, detail: { loading } }));
98
+ },
99
+ get loading() {
100
+ return loading;
101
+ },
102
+ destroy: () => scope.dispose(),
103
+ };
104
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type ReactNode } from 'react';
3
+ import { useIsomorphicLayoutEffect } from '../../react/use-recipe';
4
+ import { createSkeleton, skeletonBones, type SkeletonInstance, type SkeletonOptions } from './engine';
5
+
6
+ export interface SkeletonProps extends Omit<HTMLAttributes<HTMLDivElement>, 'children'>, SkeletonOptions {
7
+ /** The real content, shown when loading ends. */
8
+ children?: ReactNode;
9
+ /** Custom placeholder instead of the shape's bones. */
10
+ placeholder?: ReactNode;
11
+ }
12
+
13
+ /**
14
+ * <Skeleton loading={pending} shape="card" label="Cargando perfil"><ProfileCard /></Skeleton>.
15
+ * React owns placeholder and content; the engine only toggles hidden and the busy state.
16
+ */
17
+ export function Skeleton({ loading = true, animation, shape = 'text', lines, label, placeholder, children, className, ...props }: SkeletonProps) {
18
+ const ref = useRef<HTMLDivElement>(null);
19
+ const instance = useRef<SkeletonInstance | null>(null);
20
+ const current = useRef(loading);
21
+ current.current = loading;
22
+ useIsomorphicLayoutEffect(() => {
23
+ const created = createSkeleton(ref.current, { loading: current.current, animation, shape, lines, label });
24
+ instance.current = created;
25
+ return () => {
26
+ created.destroy();
27
+ instance.current = null;
28
+ };
29
+ }, [animation, shape, lines, label]);
30
+ useIsomorphicLayoutEffect(() => {
31
+ instance.current?.setLoading(loading);
32
+ }, [loading]);
33
+ return (
34
+ <div {...props} ref={ref} className={['mk-skeleton', className].filter(Boolean).join(' ')} data-mk-ssr-loading={loading ? '' : undefined}>
35
+ <div data-mk-skeleton-placeholder="" data-mk-shape={shape}>
36
+ {placeholder ?? skeletonBones(shape, lines).map((bone, i) => <span key={i} data-mk-bone={bone} />)}
37
+ </div>
38
+ <div data-mk-skeleton-content="">{children}</div>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,107 @@
1
+ /* skeleton — soft bones on the surface tone; pulse (opacity) or shimmer (a passing sheen). */
2
+ .mk-skeleton,
3
+ mk-skeleton {
4
+ --mk-skeleton-bone: var(--mk-surface-3);
5
+ --mk-skeleton-sheen: color-mix(in srgb, var(--mk-surface-1) 80%, transparent);
6
+ --mk-skeleton-radius: var(--mk-radius-md, 16px);
7
+ /* Melonmind animation-loading-2: a sheen crossing in 1 s. */
8
+ --mk-skeleton-duration: 1s;
9
+ display: block;
10
+ }
11
+
12
+ :is(.mk-skeleton, mk-skeleton) [data-mk-skeleton-placeholder] {
13
+ display: grid;
14
+ gap: 0.625rem;
15
+ }
16
+
17
+ :is(.mk-skeleton, mk-skeleton) [data-mk-skeleton-placeholder][hidden],
18
+ :is(.mk-skeleton, mk-skeleton) [data-mk-skeleton-content][hidden] {
19
+ display: none !important;
20
+ }
21
+
22
+ /* Before the engine runs (server render), the React state decides what shows. */
23
+ :is(.mk-skeleton, mk-skeleton):not([data-mk-ready])[data-mk-ssr-loading] > [data-mk-skeleton-content],
24
+ :is(.mk-skeleton, mk-skeleton):not([data-mk-ready]):not([data-mk-ssr-loading]) > [data-mk-skeleton-placeholder] {
25
+ display: none;
26
+ }
27
+
28
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone] {
29
+ position: relative;
30
+ display: block;
31
+ overflow: hidden;
32
+ height: 0.875rem;
33
+ border-radius: var(--mk-radius-full);
34
+ background: var(--mk-skeleton-bone);
35
+ }
36
+
37
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone='short'] {
38
+ width: 62%;
39
+ }
40
+
41
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone='circle'] {
42
+ width: 3.5rem;
43
+ height: 3.5rem;
44
+ border-radius: 50%;
45
+ }
46
+
47
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone='block'] {
48
+ height: auto;
49
+ aspect-ratio: 16 / 9;
50
+ border-radius: var(--mk-skeleton-radius);
51
+ }
52
+
53
+ /* Avatar: circle beside two lines. */
54
+ :is(.mk-skeleton, mk-skeleton) [data-mk-shape='avatar'] {
55
+ grid-template-columns: auto minmax(0, 1fr);
56
+ align-items: center;
57
+ column-gap: 1rem;
58
+ }
59
+
60
+ :is(.mk-skeleton, mk-skeleton) [data-mk-shape='avatar'] [data-mk-bone='circle'] {
61
+ grid-row: span 2;
62
+ }
63
+
64
+ :is(.mk-skeleton, mk-skeleton) [data-mk-shape='card'] {
65
+ padding: 0.5rem 0.5rem 1rem;
66
+ border-radius: var(--mk-radius-lg, 24px);
67
+ background: var(--mk-surface-1);
68
+ box-shadow: inset 0 0 0 1px var(--mk-outline-soft);
69
+ }
70
+
71
+ :is(.mk-skeleton, mk-skeleton)[data-mk-animation='pulse'][aria-busy='true'] [data-mk-bone] {
72
+ animation: mk-skeleton-pulse var(--mk-skeleton-duration) ease-in-out infinite;
73
+ }
74
+
75
+ :is(.mk-skeleton, mk-skeleton)[data-mk-animation='shimmer'][aria-busy='true'] [data-mk-bone]::after {
76
+ content: '';
77
+ position: absolute;
78
+ inset: 0;
79
+ background: linear-gradient(90deg, transparent, var(--mk-skeleton-sheen), transparent);
80
+ transform: translateX(-100%);
81
+ animation: mk-skeleton-shimmer var(--mk-skeleton-duration) linear infinite;
82
+ }
83
+
84
+
85
+ @keyframes mk-skeleton-pulse {
86
+ 50% { opacity: 0.45; }
87
+ }
88
+
89
+ @keyframes mk-skeleton-shimmer {
90
+ to { transform: translateX(100%); }
91
+ }
92
+
93
+ @keyframes mk-skeleton-reveal {
94
+ from { opacity: 0; transform: translateY(4px); }
95
+ }
96
+
97
+ @media (prefers-reduced-motion: reduce) {
98
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone],
99
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone]::after,
100
+ :is(.mk-skeleton, mk-skeleton) [data-mk-skeleton-content] {
101
+ animation: none !important;
102
+ }
103
+
104
+ :is(.mk-skeleton, mk-skeleton) [data-mk-bone]::after {
105
+ display: none;
106
+ }
107
+ }
@@ -0,0 +1,50 @@
1
+ # split-view
2
+
3
+ Vista dividida en dos áreas con tamaño configurable, lado a lado o una sobre otra. Por dentro usa el divisor de `resizable-panel`: se arrastra (cede como goma en los límites y regresa con un resorte), responde al teclado, cierra un lado de golpe si lo arrastras muy por debajo de su mínimo, y con doble clic vuelve a la proporción inicial. Sus botones cierran cualquiera de los dos lados y lo vuelven a abrir (.3 s, curva swing).
4
+
5
+ `setRatio()` se desliza hasta la proporción pedida (.45 s, curva signature). Con `stackBelow`, cuando la vista es más angosta que esos píxeles, las áreas se apilan y el divisor gira con ellas. La proporción se puede recordar (`storageKey`).
6
+
7
+ ## HTML / Astro
8
+
9
+ ```html
10
+ <mk-split-view class="mk-split-view" ratio="0.35" stack-below="640" labels="Bandeja,Mensaje" style="height: 28rem">
11
+ <section>…lista…</section>
12
+ <section>…mensaje…</section>
13
+ </mk-split-view>
14
+ ```
15
+
16
+ ## React
17
+
18
+ ```tsx
19
+ const split = useRef<SplitViewHandle>(null);
20
+
21
+ <SplitView ref={split} ratio={0.35} stackBelow={640} labels={['Bandeja', 'Mensaje']} onChange={guardar}>
22
+ {[<Bandeja key="bandeja" />, <Mensaje key="mensaje" />]}
23
+ </SplitView>;
24
+
25
+ split.current?.toggle('start');
26
+ ```
27
+
28
+ Una `ratio` nueva se desliza hasta ahí.
29
+
30
+ ## Opciones
31
+
32
+ | Opción | Atributo | Por defecto | Qué hace |
33
+ |---|---|---|---|
34
+ | `orientation` | `orientation` | `'horizontal'` | `'horizontal'`: lado a lado; `'vertical'`: una sobre otra. |
35
+ | `ratio` | `ratio` | `0.5` | Parte de la primera área (0–1). |
36
+ | `min` | `min` | `15` | Tamaño mínimo de cada área, en %. |
37
+ | `collapsible` | `collapsible` | `true` | Qué lados se pueden cerrar: `true` (ambos), `'start'`, `'end'` o `false`. |
38
+ | `stackBelow` | `stack-below` | `0` | Apilar las áreas por debajo de este ancho en px. |
39
+ | `storageKey` | `storage-key` | — | Recordar la proporción en `localStorage`. |
40
+ | `labels` | `labels` | `'Primera área', 'Segunda área'` | Nombres de las áreas (o su `data-label`). |
41
+ | `onChange(ratio)` | — | — | Al terminar un cambio. |
42
+ | `onCollapse(side)` | — | — | `'start'`, `'end'` o `null` al abrirse. |
43
+
44
+ ## API
45
+
46
+ `setRatio(ratio, animate = true)`, `collapse('start' | 'end')`, `expand()`, `toggle(side)`, `ratio`, `collapsed`, `orientation`, `destroy()`.
47
+
48
+ ## Accesibilidad
49
+
50
+ El divisor es un `separator` de WAI-ARIA con su valor, su texto y una etiqueta con los nombres de las áreas; ← → (o ↑ ↓) lo mueven, Inicio y Fin lo llevan a los límites e Intro cierra o abre la primera área. Un área cerrada queda oculta también para los lectores de pantalla. Con movimiento reducido los cambios son inmediatos.
@@ -0,0 +1,34 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createSplitView, type SplitViewInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-split-view class="mk-split-view" ratio="0.35" stack-below="640" labels="Bandeja,Mensaje">
6
+ * <section>…</section><section>…</section>
7
+ * </mk-split-view>. Emits mk:resize and mk:collapse from its separator.
8
+ */
9
+ export class MkSplitViewElement extends MkElement<SplitViewInstance> {
10
+ static options: Record<string, OptionType> = {
11
+ orientation: 'string',
12
+ ratio: 'number',
13
+ min: 'number',
14
+ collapsible: 'string',
15
+ stackBelow: 'number',
16
+ storageKey: 'string',
17
+ labels: 'list',
18
+ };
19
+
20
+ protected create(options: Record<string, unknown>) {
21
+ const raw = this.getAttribute('collapsible');
22
+ const collapsible = raw === null || raw === '' || raw === 'true' ? true : raw === 'false' ? false : raw === 'start' || raw === 'end' ? raw : true;
23
+ const labels = Array.isArray(options.labels) && options.labels.length === 2 ? (options.labels as [string, string]) : undefined;
24
+ return createSplitView(this, { ...options, collapsible, labels });
25
+ }
26
+
27
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
28
+ if (previous === next || !this.instance) return;
29
+ if (name === 'ratio' && next !== null && Number.isFinite(Number(next))) this.instance.setRatio(Number(next));
30
+ else super.attributeChangedCallback(name, previous, next);
31
+ }
32
+ }
33
+
34
+ export const defineSplitView = () => defineElement('mk-split-view', MkSplitViewElement);
@@ -0,0 +1,114 @@
1
+ import { registerMotion } from '../../core/gsap';
2
+ import { createScope } from '../../core/scope';
3
+ import { inert, type Instance } from '../../core/types';
4
+ import { createResizablePanel, type PanelOrientation } from '../resizable-panel/engine';
5
+
6
+ /*
7
+ * Split view: two areas side by side (or one over the other) with
8
+ * resizable-panel's separator between them: it drags with a rubber band at the
9
+ * limits, keys, snaps a side shut when dragged well past its minimum, and a
10
+ * double click centres it back. Its buttons shut either side and open it
11
+ * again (.3 s, swing). setRatio() glides to a split (.45 s, signature). Under
12
+ * `stackBelow` pixels of width the areas stack, the separator turning with
13
+ * them. The split can be remembered (storageKey).
14
+ */
15
+
16
+ export type SplitSide = 'start' | 'end';
17
+
18
+ export interface SplitViewOptions {
19
+ /** 'horizontal' (default): side by side; 'vertical': one over the other. */
20
+ orientation?: PanelOrientation;
21
+ /** The first area's share, 0–1 (default .5). */
22
+ ratio?: number;
23
+ /** Smallest share of an area in percent (default 15). */
24
+ min?: number;
25
+ /** Which sides can shut: true (default) both, 'start', 'end' or false. */
26
+ collapsible?: boolean | SplitSide;
27
+ /** Stack the areas when the view is narrower than this many pixels (default 0: never). */
28
+ stackBelow?: number;
29
+ storageKey?: string;
30
+ /** Names of the two areas (default 'Primera área', 'Segunda área'; or their data-label). */
31
+ labels?: [string, string];
32
+ /** After a change of the split ends. */
33
+ onChange?: (ratio: number) => void;
34
+ onCollapse?: (side: SplitSide | null) => void;
35
+ }
36
+
37
+ export interface SplitViewInstance extends Instance {
38
+ setRatio(ratio: number, animate?: boolean): void;
39
+ collapse(side: SplitSide): void;
40
+ /** Open the side that is shut. */
41
+ expand(): void;
42
+ toggle(side: SplitSide): void;
43
+ readonly ratio: number;
44
+ readonly collapsed: SplitSide | null;
45
+ readonly orientation: PanelOrientation;
46
+ }
47
+
48
+ export function createSplitView(root: HTMLElement | null, options: SplitViewOptions = {}): SplitViewInstance {
49
+ const areas = root ? [...root.children].filter((child): child is HTMLElement => child instanceof HTMLElement) : [];
50
+ if (!root || areas.length !== 2) {
51
+ return inert({ setRatio() {}, collapse() {}, expand() {}, toggle() {}, ratio: 0.5, collapsed: null, orientation: 'horizontal' as PanelOrientation });
52
+ }
53
+ registerMotion();
54
+ const scope = createScope();
55
+ const ratio = Math.min(1, Math.max(0, Number.isFinite(options.ratio) ? options.ratio! : 0.5));
56
+ const sides = options.collapsible === false ? [] : options.collapsible === 'start' ? [0] : options.collapsible === 'end' ? [1] : [0, 1];
57
+ const preferred: PanelOrientation = options.orientation === 'vertical' ? 'vertical' : 'horizontal';
58
+ const sideOf = (index: number): SplitSide => (index === 0 ? 'start' : 'end');
59
+ scope.attr(root, 'data-mk-split', '');
60
+ const panel = createResizablePanel(root, {
61
+ orientation: preferred,
62
+ sizes: [ratio * 100, (1 - ratio) * 100],
63
+ min: options.min ?? 15,
64
+ collapsible: sides,
65
+ handleButtons: sides.length > 0,
66
+ storageKey: options.storageKey,
67
+ labels: options.labels ?? [areas[0]!.dataset.label ?? 'Primera área', areas[1]!.dataset.label ?? 'Segunda área'],
68
+ onResizeEnd: (sizes) => options.onChange?.(Math.round(sizes[0]! * 10) / 1000),
69
+ onCollapse: (index, shut) => options.onCollapse?.(shut ? sideOf(index) : null),
70
+ });
71
+ scope.add(() => panel.destroy());
72
+
73
+ // Narrow: stacked.
74
+ if (options.stackBelow && options.stackBelow > 0 && typeof ResizeObserver === 'function') {
75
+ const limit = options.stackBelow;
76
+ scope.observe(
77
+ new ResizeObserver(() => {
78
+ const width = root.getBoundingClientRect().width;
79
+ panel.setOrientation(width && width < limit ? 'vertical' : preferred);
80
+ }),
81
+ ).observe(root);
82
+ }
83
+
84
+ const collapsedSide = () => (panel.collapsed.includes(0) ? 'start' : panel.collapsed.includes(1) ? 'end' : null);
85
+ return {
86
+ setRatio(next, animate = true) {
87
+ const share = Math.min(1, Math.max(0, next));
88
+ panel.setSizes([share * 100, (1 - share) * 100], animate);
89
+ },
90
+ collapse(side) {
91
+ const other = side === 'start' ? 1 : 0;
92
+ if (panel.collapsed.includes(other)) panel.expand(other);
93
+ panel.collapse(side === 'start' ? 0 : 1);
94
+ },
95
+ expand() {
96
+ for (const index of panel.collapsed) panel.expand(index);
97
+ },
98
+ toggle(side) {
99
+ const index = side === 'start' ? 0 : 1;
100
+ if (panel.collapsed.includes(index)) panel.expand(index);
101
+ else this.collapse(side);
102
+ },
103
+ get ratio() {
104
+ return Math.round(panel.sizes[0]! * 10) / 1000;
105
+ },
106
+ get collapsed() {
107
+ return collapsedSide();
108
+ },
109
+ get orientation() {
110
+ return panel.orientation;
111
+ },
112
+ destroy: () => scope.dispose(),
113
+ };
114
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+ import { forwardRef, useImperativeHandle, useRef, type HTMLAttributes, type ReactNode } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createSplitView, type SplitViewInstance, type SplitViewOptions } from './engine';
5
+
6
+ export interface SplitViewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange' | 'children'>, SplitViewOptions {
7
+ /** The two areas. */
8
+ children: [ReactNode, ReactNode];
9
+ }
10
+
11
+ export type SplitViewHandle = Pick<SplitViewInstance, 'setRatio' | 'collapse' | 'expand' | 'toggle'>;
12
+
13
+ /** <SplitView ratio={0.35} stackBelow={640} labels={['Bandeja', 'Mensaje']}><List /><Detail /></SplitView>. A new `ratio` glides there. */
14
+ export const SplitView = forwardRef<SplitViewHandle, SplitViewProps>(function SplitView(
15
+ { orientation, ratio, min, collapsible, stackBelow, storageKey, labels, onChange, onCollapse, className, children, ...props },
16
+ handle,
17
+ ) {
18
+ const ref = useRef<HTMLDivElement>(null);
19
+ const instance = useRef<SplitViewInstance | null>(null);
20
+ const change = useLatest(onChange);
21
+ const collapse = useLatest(onCollapse);
22
+ const shown = useRef(ratio);
23
+ useImperativeHandle(
24
+ handle,
25
+ () => ({
26
+ setRatio: (next, animate) => instance.current?.setRatio(next, animate),
27
+ collapse: (side) => instance.current?.collapse(side),
28
+ expand: () => instance.current?.expand(),
29
+ toggle: (side) => instance.current?.toggle(side),
30
+ }),
31
+ [],
32
+ );
33
+ useIsomorphicLayoutEffect(() => {
34
+ const created = createSplitView(ref.current, {
35
+ orientation,
36
+ ratio: shown.current,
37
+ min,
38
+ collapsible,
39
+ stackBelow,
40
+ storageKey,
41
+ labels,
42
+ onChange: (next) => change.current?.(next),
43
+ onCollapse: (side) => collapse.current?.(side),
44
+ });
45
+ instance.current = created;
46
+ return () => {
47
+ created.destroy();
48
+ instance.current = null;
49
+ };
50
+ // eslint-disable-next-line react-hooks/exhaustive-deps
51
+ }, [orientation, min, String(collapsible), stackBelow, storageKey, String(labels)]);
52
+ useIsomorphicLayoutEffect(() => {
53
+ if (ratio === undefined || ratio === shown.current) return;
54
+ shown.current = ratio;
55
+ instance.current?.setRatio(ratio);
56
+ }, [ratio]);
57
+ return (
58
+ <div {...props} ref={ref} className={['mk-split-view', className].filter(Boolean).join(' ')}>
59
+ {children[0]}
60
+ {children[1]}
61
+ </div>
62
+ );
63
+ });
@@ -0,0 +1,18 @@
1
+ /* split-view — two areas on a 20 px panel (surface-1, outline-soft border) with resizable-panel's separator between
2
+ them (its hairline, grip, bubble and the buttons that shut a side). Give it a height; each area scrolls on its own. */
3
+ .mk-split-view,
4
+ mk-split-view {
5
+ position: relative;
6
+ box-sizing: border-box;
7
+ display: flex;
8
+ min-width: 0;
9
+ min-height: 12rem;
10
+ overflow: hidden;
11
+ border: 1px solid var(--mk-outline-soft);
12
+ border-radius: 20px;
13
+ background: var(--mk-surface-1);
14
+ }
15
+
16
+ :is(.mk-split-view, mk-split-view)[data-mk-orientation='vertical'] {
17
+ flex-direction: column;
18
+ }
@@ -0,0 +1,14 @@
1
+ # star-rating
2
+
3
+ Valoración con estrellas sobre radios nativos (flechas del teclado y formularios incluidos). Al señalar, las estrellas se previsualizan en cascada de 20 ms; al elegir, la estrella rebota desde 1.4 con curva elástica, salta un destello y la etiqueta («Excelente») cambia con blur. `allowClear` permite quitar la valoración pulsando la estrella elegida. `readonly` dibuja un valor fijo (con fracciones) como imagen con su valor de nombre.
4
+
5
+ ```html
6
+ <mk-star-rating class="mk-star-rating" name="valoracion" value="4" allow-clear></mk-star-rating>
7
+ <mk-star-rating class="mk-star-rating" readonly value="4.5"></mk-star-rating>
8
+ ```
9
+
10
+ `defineElements()`, `createStarRating(root, options)` o `<StarRating value={stars} onChange={setStars} />` (con `value` es controlado; `defaultValue` si no).
11
+
12
+ Opciones: `max=5`, `value=0`, `name` (formulario), `label='Valoración'`, `labels` (un texto por estrella; por defecto Mala → Excelente), `readonly`, `allowClear`, `disabled`, `onChange(value)`. API: `setValue(n, notify=false)`, `value`, `destroy()`. Evento `mk:change` con `{value}`.
13
+
14
+ Variables: `--mk-star-size` (2rem), `--mk-star-color` (#f5b301), `--mk-star-empty`. Movimiento reducido: sin rebote, cascada ni destello.