@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,325 @@
1
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
+ import { reduced } from '../../core/reduced';
3
+ import { createScope } from '../../core/scope';
4
+ import { inert, type Instance } from '../../core/types';
5
+ import { rollDigits } from '../../motion/micro';
6
+
7
+ /*
8
+ * Image preview: thumbnails ([data-mk-image]) open a modal <dialog> where the
9
+ * picture grows out of its thumbnail (0.7 s on the signature curve, the
10
+ * thumbnail's radius easing to 24 px) over a blurred backdrop. Previous/next
11
+ * (buttons, arrow keys, a swipe) slide the next picture in out of a blur; the
12
+ * counter rolls. A double click or tap zooms ×2 where you point and a drag pans
13
+ * it. Dragging down, Escape or the close button send the picture back to its
14
+ * thumbnail, and focus returns to it.
15
+ */
16
+
17
+ export interface ImagePreviewOptions {
18
+ labels?: { close?: string; prev?: string; next?: string; dialog?: string };
19
+ /** Loop from the last picture to the first (default true). */
20
+ loop?: boolean;
21
+ onChange?: (index: number) => void;
22
+ onOpenChange?: (open: boolean) => void;
23
+ }
24
+
25
+ export interface ImagePreviewInstance extends Instance {
26
+ open(index?: number): void;
27
+ close(): void;
28
+ next(): void;
29
+ prev(): void;
30
+ readonly isOpen: boolean;
31
+ readonly index: number;
32
+ }
33
+
34
+ interface Picture {
35
+ thumb: HTMLElement;
36
+ src: string;
37
+ alt: string;
38
+ caption: string;
39
+ }
40
+
41
+ const button = (part: string, label: string, path: string) => {
42
+ const element = document.createElement('button');
43
+ element.type = 'button';
44
+ element.dataset.mkLightbox = part;
45
+ element.setAttribute('aria-label', label);
46
+ element.innerHTML = `<svg aria-hidden="true" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="${path}"/></svg>`;
47
+ return element;
48
+ };
49
+
50
+ export function createImagePreview(root: HTMLElement | null, options: ImagePreviewOptions = {}): ImagePreviewInstance {
51
+ if (!root) return inert({ open() {}, close() {}, next() {}, prev() {}, isOpen: false, index: -1 });
52
+ registerMotion();
53
+ const scope = createScope();
54
+ const loop = options.loop ?? true;
55
+ const labels = { close: 'Cerrar', prev: 'Imagen anterior', next: 'Imagen siguiente', dialog: 'Vista de imagen', ...options.labels };
56
+
57
+ const pictures = (): Picture[] =>
58
+ [...root.querySelectorAll<HTMLElement>('[data-mk-image]')].map((thumb) => {
59
+ const img = thumb.matches('img') ? (thumb as HTMLImageElement) : thumb.querySelector('img');
60
+ return {
61
+ thumb,
62
+ src: thumb.dataset.mkSrc || thumb.getAttribute('href') || img?.getAttribute('src') || img?.currentSrc || '',
63
+ alt: img?.alt ?? thumb.getAttribute('aria-label') ?? '',
64
+ caption: thumb.dataset.mkCaption ?? '',
65
+ };
66
+ });
67
+
68
+ const dialog = document.createElement('dialog');
69
+ dialog.dataset.mkLightboxDialog = '';
70
+ dialog.setAttribute('aria-label', labels.dialog);
71
+ const backdrop = document.createElement('div');
72
+ backdrop.dataset.mkLightbox = 'backdrop';
73
+ const stage = document.createElement('figure');
74
+ stage.dataset.mkLightbox = 'stage';
75
+ const image = document.createElement('img');
76
+ image.dataset.mkLightbox = 'image';
77
+ image.draggable = false;
78
+ const caption = document.createElement('figcaption');
79
+ caption.dataset.mkLightbox = 'caption';
80
+ stage.append(image, caption);
81
+ const close = button('close', labels.close, 'M6 6l12 12M18 6L6 18');
82
+ const prev = button('prev', labels.prev, 'M15 5l-7 7 7 7');
83
+ const next = button('next', labels.next, 'M9 5l7 7-7 7');
84
+ const counter = document.createElement('span');
85
+ counter.dataset.mkLightbox = 'count';
86
+ counter.setAttribute('aria-live', 'polite');
87
+ dialog.append(backdrop, stage, prev, next, close, counter);
88
+ root.append(dialog);
89
+ scope.add(() => dialog.remove());
90
+ scope.attr(root, 'data-mk-ready', '');
91
+ for (const { thumb } of pictures()) {
92
+ if (thumb.matches('button, a[href]') || thumb.tabIndex >= 0) continue;
93
+ scope.attr(thumb, 'tabindex', '0');
94
+ scope.attr(thumb, 'role', 'button');
95
+ }
96
+
97
+ let isOpen = false;
98
+ let index = -1;
99
+ let zoomed = false;
100
+ let list: Picture[] = [];
101
+ const effects = new Set<gsap.core.Animation>();
102
+ const keep = (animation: gsap.core.Animation | null) => {
103
+ if (!animation) return;
104
+ effects.add(animation);
105
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
106
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
107
+ };
108
+ const thumbImg = (picture?: Picture) => (picture ? (picture.thumb.matches('img') ? picture.thumb : picture.thumb.querySelector('img') ?? picture.thumb) : null);
109
+ scope.add(() => {
110
+ for (const effect of effects) effect.kill();
111
+ effects.clear();
112
+ gsap.killTweensOf([image, backdrop, stage]);
113
+ for (const picture of list) thumbImg(picture)?.style.removeProperty('visibility');
114
+ if (dialog.open) dialog.close();
115
+ });
116
+
117
+ const write = (i: number) => {
118
+ const picture = list[i]!;
119
+ image.src = picture.src;
120
+ image.alt = picture.alt;
121
+ caption.textContent = picture.caption;
122
+ caption.hidden = !picture.caption;
123
+ const text = `${i + 1} / ${list.length}`;
124
+ if (counter.textContent && !reduced()) keep(rollDigits(counter, text, i >= index ? 1 : -1, 0.35));
125
+ else counter.textContent = text;
126
+ prev.disabled = !loop && i === 0;
127
+ next.disabled = !loop && i === list.length - 1;
128
+ prev.hidden = next.hidden = list.length < 2;
129
+ };
130
+
131
+ /** Transform that puts the big image over the thumbnail. */
132
+ const fromThumb = (picture: Picture) => {
133
+ const small = thumbImg(picture)!.getBoundingClientRect();
134
+ const big = image.getBoundingClientRect();
135
+ if (!small.width || !big.width) return null;
136
+ const radius = parseFloat(getComputedStyle(thumbImg(picture)!).borderRadius) || 0;
137
+ return {
138
+ x: small.left + small.width / 2 - (big.left + big.width / 2),
139
+ y: small.top + small.height / 2 - (big.top + big.height / 2),
140
+ scaleX: small.width / big.width,
141
+ scaleY: small.height / big.height,
142
+ borderRadius: radius / (small.width / big.width),
143
+ };
144
+ };
145
+
146
+ const unzoom = () => {
147
+ zoomed = false;
148
+ dialog.removeAttribute('data-mk-zoomed');
149
+ keep(gsap.to(image, { scale: 1, x: 0, y: 0, duration: reduced() ? 0 : 0.4, ease: EASE.signature }));
150
+ };
151
+
152
+ const open = (at = 0) => {
153
+ if (scope.disposed) return;
154
+ list = pictures();
155
+ if (!list.length) return;
156
+ const target = Math.max(0, Math.min(list.length - 1, Math.round(at)));
157
+ if (isOpen) return go(target);
158
+ isOpen = true;
159
+ index = target;
160
+ counter.textContent = '';
161
+ write(target);
162
+ if (typeof dialog.showModal === 'function') dialog.showModal();
163
+ else dialog.setAttribute('open', '');
164
+ close.focus({ preventScroll: true });
165
+ const picture = list[target]!;
166
+ options.onOpenChange?.(true);
167
+ root.dispatchEvent(new CustomEvent('mk:open', { bubbles: true, detail: { index } }));
168
+ if (reduced()) return;
169
+ keep(gsap.fromTo(backdrop, { opacity: 0 }, { opacity: 1, duration: 0.4, ease: 'power2.out', clearProps: 'opacity' }));
170
+ keep(gsap.fromTo([prev, next, close, counter, caption], { opacity: 0 }, { opacity: 1, duration: 0.3, delay: 0.3, clearProps: 'opacity' }));
171
+ const grow = () => {
172
+ const start = fromThumb(picture);
173
+ if (!start) return void keep(gsap.fromTo(image, { opacity: 0, scale: 0.92 }, { opacity: 1, scale: 1, duration: 0.35, ease: EASE.pop, clearProps: 'opacity,scale,transform' }));
174
+ thumbImg(picture)!.style.visibility = 'hidden';
175
+ keep(gsap.fromTo(image, start, { x: 0, y: 0, scaleX: 1, scaleY: 1, borderRadius: 24, duration: 0.7, ease: EASE.signature, clearProps: 'transform,borderRadius', onComplete: () => {
176
+ thumbImg(picture)?.style.removeProperty('visibility');
177
+ } }));
178
+ };
179
+ if (image.complete && image.naturalWidth) grow();
180
+ else image.addEventListener('load', grow, { once: true });
181
+ };
182
+
183
+ const finish = () => {
184
+ if (dialog.open) dialog.close();
185
+ gsap.set([image, backdrop], { clearProps: 'all' });
186
+ const picture = list[index];
187
+ if (picture) thumbImg(picture)?.style.removeProperty('visibility');
188
+ isOpen = false;
189
+ zoomed = false;
190
+ dialog.removeAttribute('data-mk-zoomed');
191
+ options.onOpenChange?.(false);
192
+ root.dispatchEvent(new CustomEvent('mk:close', { bubbles: true, detail: { index } }));
193
+ picture?.thumb.focus({ preventScroll: true });
194
+ };
195
+
196
+ const shut = () => {
197
+ if (!isOpen) return;
198
+ const picture = list[index];
199
+ if (reduced() || !picture) return finish();
200
+ gsap.killTweensOf(image);
201
+ gsap.set(image, { scale: 1, x: 0, y: 0 });
202
+ const end = fromThumb(picture);
203
+ keep(gsap.to(backdrop, { opacity: 0, duration: 0.35, ease: 'power2.in' }));
204
+ keep(gsap.to([prev, next, close, counter, caption], { opacity: 0, duration: 0.15 }));
205
+ if (!end) return void keep(gsap.to(image, { opacity: 0, scale: 0.92, duration: 0.25, ease: 'power2.in', onComplete: finish }));
206
+ thumbImg(picture)!.style.visibility = 'hidden';
207
+ keep(gsap.to(image, { ...end, duration: 0.5, ease: EASE.signature, onComplete: () => {
208
+ gsap.set([prev, next, close, counter, caption], { clearProps: 'opacity' });
209
+ finish();
210
+ } }));
211
+ };
212
+
213
+ function go(to: number) {
214
+ if (!isOpen || list.length < 2) return;
215
+ let target = to;
216
+ if (loop) target = (to + list.length) % list.length;
217
+ else if (to < 0 || to >= list.length) return;
218
+ if (target === index) return;
219
+ const direction = to > index ? 1 : -1;
220
+ if (zoomed) unzoom();
221
+ const before = index;
222
+ index = target;
223
+ options.onChange?.(index);
224
+ root!.dispatchEvent(new CustomEvent('mk:change', { bubbles: true, detail: { index, previous: before } }));
225
+ if (reduced()) return write(target);
226
+ gsap.killTweensOf(image);
227
+ keep(
228
+ gsap
229
+ .timeline()
230
+ .to(image, { x: -60 * direction, opacity: 0, filter: 'blur(12px)', duration: 0.18, ease: 'power2.in' })
231
+ .call(() => write(target))
232
+ .fromTo(image, { x: 60 * direction, opacity: 0, filter: 'blur(12px)' }, { x: 0, opacity: 1, filter: 'blur(0px)', duration: 0.4, ease: EASE.signature, clearProps: 'transform,opacity,filter' }),
233
+ );
234
+ }
235
+
236
+ scope.on(root, 'click', (event) => {
237
+ const thumb = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-image]');
238
+ if (!thumb || !root.contains(thumb) || dialog.contains(thumb)) return;
239
+ event.preventDefault();
240
+ open(pictures().findIndex((picture) => picture.thumb === thumb));
241
+ });
242
+ scope.on(root, 'keydown', (event) => {
243
+ const key = (event as KeyboardEvent).key;
244
+ const thumb = (event.target as Element | null)?.closest<HTMLElement>('[data-mk-image]');
245
+ if (thumb && !dialog.contains(thumb) && (key === 'Enter' || key === ' ') && !thumb.matches('button, a[href]')) {
246
+ event.preventDefault();
247
+ open(pictures().findIndex((picture) => picture.thumb === thumb));
248
+ }
249
+ });
250
+ scope.on(dialog, 'keydown', (event) => {
251
+ const key = (event as KeyboardEvent).key;
252
+ if (key === 'ArrowRight') go(index + 1);
253
+ else if (key === 'ArrowLeft') go(index - 1);
254
+ else return;
255
+ event.preventDefault();
256
+ });
257
+ scope.on(dialog, 'cancel', (event) => {
258
+ event.preventDefault();
259
+ shut();
260
+ });
261
+ scope.on(close, 'click', shut);
262
+ scope.on(prev, 'click', () => go(index - 1));
263
+ scope.on(next, 'click', () => go(index + 1));
264
+ scope.on(backdrop, 'click', shut);
265
+
266
+ // Double click / tap zooms where you point; drag pans the zoom, swipes, or drags down to close.
267
+ scope.on(image, 'dblclick', (event) => {
268
+ const mouse = event as MouseEvent;
269
+ if (zoomed) return unzoom();
270
+ const box = image.getBoundingClientRect();
271
+ zoomed = true;
272
+ dialog.setAttribute('data-mk-zoomed', '');
273
+ const x = (box.left + box.width / 2 - mouse.clientX) || 0;
274
+ const y = (box.top + box.height / 2 - mouse.clientY) || 0;
275
+ keep(gsap.to(image, { scale: 2, x, y, duration: reduced() ? 0 : 0.45, ease: EASE.signature }));
276
+ });
277
+ let drag: { x: number; y: number; startX: number; startY: number; id: number } | null = null;
278
+ scope.on(image, 'pointerdown', (event) => {
279
+ const pointer = event as PointerEvent;
280
+ if (pointer.button !== 0) return;
281
+ drag = { x: pointer.clientX, y: pointer.clientY, startX: Number(gsap.getProperty(image, 'x')) || 0, startY: Number(gsap.getProperty(image, 'y')) || 0, id: pointer.pointerId };
282
+ image.setPointerCapture?.(pointer.pointerId);
283
+ });
284
+ scope.on(image, 'pointermove', (event) => {
285
+ const pointer = event as PointerEvent;
286
+ if (!drag || pointer.pointerId !== drag.id) return;
287
+ const dx = pointer.clientX - drag.x;
288
+ const dy = pointer.clientY - drag.y;
289
+ if (zoomed) gsap.set(image, { x: drag.startX + dx, y: drag.startY + dy });
290
+ else if (!reduced()) {
291
+ gsap.set(image, { x: dx * 0.6, y: Math.max(0, dy) });
292
+ gsap.set(backdrop, { opacity: 1 - Math.min(0.6, Math.max(0, dy) / 400) });
293
+ }
294
+ });
295
+ const release = (event: Event) => {
296
+ const pointer = event as PointerEvent;
297
+ if (!drag || pointer.pointerId !== drag.id) return;
298
+ const dx = pointer.clientX - drag.x;
299
+ const dy = pointer.clientY - drag.y;
300
+ drag = null;
301
+ if (zoomed) return;
302
+ if (dy > 120) return shut();
303
+ if (Math.abs(dx) > 70 && Math.abs(dx) > Math.abs(dy)) return go(index + (dx < 0 ? 1 : -1));
304
+ if (!reduced()) {
305
+ keep(gsap.to(image, { x: 0, y: 0, duration: 0.45, ease: EASE.swatch, clearProps: 'transform' }));
306
+ keep(gsap.to(backdrop, { opacity: 1, duration: 0.3, clearProps: 'opacity' }));
307
+ }
308
+ };
309
+ scope.on(image, 'pointerup', release);
310
+ scope.on(image, 'pointercancel', release);
311
+
312
+ return {
313
+ open,
314
+ close: shut,
315
+ next: () => go(index + 1),
316
+ prev: () => go(index - 1),
317
+ get isOpen() {
318
+ return isOpen;
319
+ },
320
+ get index() {
321
+ return index;
322
+ },
323
+ destroy: () => scope.dispose(),
324
+ };
325
+ }
@@ -0,0 +1,194 @@
1
+ /* image-preview — thumbnails in Melonmind cards (16 px radius) and a full-screen dialog: a blurred dark backdrop, the
2
+ picture at 16 px radius, round glass buttons and a counter pill. */
3
+ .mk-image-preview,
4
+ mk-image-preview {
5
+ display: grid;
6
+ grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
7
+ gap: 0.75rem;
8
+ }
9
+
10
+ :is(.mk-image-preview, mk-image-preview) [data-mk-image] {
11
+ display: block;
12
+ padding: 0;
13
+ overflow: hidden;
14
+ border: 0;
15
+ border-radius: var(--mk-radius-md, 16px);
16
+ background: var(--mk-surface-3);
17
+ cursor: zoom-in;
18
+ transition: scale 0.5s cubic-bezier(0.38, 0.49, 0, 1.5);
19
+ }
20
+
21
+ :is(.mk-image-preview, mk-image-preview) [data-mk-image]:hover {
22
+ scale: 1.03;
23
+ }
24
+
25
+ :is(.mk-image-preview, mk-image-preview) [data-mk-image]:focus-visible {
26
+ outline: 2px solid var(--mk-accent);
27
+ outline-offset: 3px;
28
+ }
29
+
30
+ :is(.mk-image-preview, mk-image-preview) [data-mk-image] img {
31
+ display: block;
32
+ width: 100%;
33
+ aspect-ratio: 1;
34
+ border-radius: inherit;
35
+ object-fit: cover;
36
+ }
37
+
38
+ [data-mk-lightbox-dialog] {
39
+ position: fixed;
40
+ inset: 0;
41
+ width: 100vw;
42
+ max-width: none;
43
+ height: 100dvh;
44
+ max-height: none;
45
+ margin: 0;
46
+ padding: 0;
47
+ overflow: hidden;
48
+ border: 0;
49
+ background: transparent;
50
+ color: #fff;
51
+ }
52
+
53
+ [data-mk-lightbox-dialog][open] {
54
+ display: grid;
55
+ place-items: center;
56
+ }
57
+
58
+ [data-mk-lightbox-dialog]::backdrop {
59
+ background: transparent;
60
+ }
61
+
62
+ [data-mk-lightbox='backdrop'] {
63
+ position: absolute;
64
+ inset: 0;
65
+ background: rgb(8 8 10 / 0.82);
66
+ backdrop-filter: blur(20px);
67
+ }
68
+
69
+ [data-mk-lightbox='stage'] {
70
+ position: relative;
71
+ display: grid;
72
+ justify-items: center;
73
+ gap: 0.85rem;
74
+ margin: 0;
75
+ pointer-events: none;
76
+ }
77
+
78
+ [data-mk-lightbox='image'] {
79
+ display: block;
80
+ max-width: min(92vw, 1100px);
81
+ max-height: 78dvh;
82
+ border-radius: 16px;
83
+ object-fit: contain;
84
+ box-shadow: 0 30px 70px -30px rgb(0 0 0 / 0.7);
85
+ cursor: grab;
86
+ pointer-events: auto;
87
+ touch-action: none;
88
+ user-select: none;
89
+ }
90
+
91
+ [data-mk-lightbox-dialog][data-mk-zoomed] [data-mk-lightbox='image'] {
92
+ cursor: move;
93
+ }
94
+
95
+ [data-mk-lightbox='caption'] {
96
+ max-width: min(36rem, 90vw);
97
+ padding: 0.4rem 0.9rem;
98
+ border-radius: 64px;
99
+ background: rgb(255 255 255 / 0.1);
100
+ color: rgb(255 255 255 / 0.85);
101
+ font-size: 0.875rem;
102
+ text-align: center;
103
+ }
104
+
105
+ [data-mk-lightbox='caption'][hidden] {
106
+ display: none;
107
+ }
108
+
109
+ [data-mk-lightbox-dialog] button[data-mk-lightbox] {
110
+ position: absolute;
111
+ display: grid;
112
+ place-items: center;
113
+ width: 3rem;
114
+ height: 3rem;
115
+ padding: 0;
116
+ border: 0;
117
+ border-radius: 50%;
118
+ background: rgb(255 255 255 / 0.12);
119
+ box-shadow:
120
+ inset 1px 1px 1px rgb(255 255 255 / 0.16),
121
+ inset -1px -1px 1px rgb(255 255 255 / 0.06);
122
+ backdrop-filter: blur(16px);
123
+ color: #fff;
124
+ cursor: pointer;
125
+ transition:
126
+ background-color 0.2s ease,
127
+ scale 0.4s cubic-bezier(0.38, 0.49, 0, 1.5);
128
+ }
129
+
130
+ [data-mk-lightbox-dialog] button[data-mk-lightbox]:hover:not(:disabled) {
131
+ background: rgb(255 255 255 / 0.22);
132
+ scale: 1.08;
133
+ }
134
+
135
+ [data-mk-lightbox-dialog] button[data-mk-lightbox]:focus-visible {
136
+ outline: 2px solid #fff;
137
+ outline-offset: 3px;
138
+ }
139
+
140
+ [data-mk-lightbox-dialog] button[data-mk-lightbox]:disabled {
141
+ opacity: 0.3;
142
+ cursor: default;
143
+ }
144
+
145
+ [data-mk-lightbox-dialog] button[data-mk-lightbox][hidden] {
146
+ display: none;
147
+ }
148
+
149
+ [data-mk-lightbox='close'] {
150
+ top: 1rem;
151
+ right: 1rem;
152
+ }
153
+
154
+ [data-mk-lightbox='prev'] {
155
+ left: 1rem;
156
+ top: 50%;
157
+ margin-top: -1.5rem;
158
+ }
159
+
160
+ [data-mk-lightbox='next'] {
161
+ right: 1rem;
162
+ top: 50%;
163
+ margin-top: -1.5rem;
164
+ }
165
+
166
+ [data-mk-lightbox='count'] {
167
+ position: absolute;
168
+ top: 1.6rem;
169
+ left: 50%;
170
+ display: inline-flex;
171
+ padding: 0.25rem 0.75rem;
172
+ border-radius: 64px;
173
+ background: rgb(255 255 255 / 0.1);
174
+ font-size: 0.8125rem;
175
+ font-variant-numeric: tabular-nums;
176
+ translate: -50% 0;
177
+ }
178
+
179
+ @media (max-width: 680px) {
180
+ [data-mk-lightbox='prev'],
181
+ [data-mk-lightbox='next'] {
182
+ top: auto;
183
+ bottom: 1.25rem;
184
+ margin: 0;
185
+ }
186
+ }
187
+
188
+ @media (prefers-reduced-motion: reduce) {
189
+ :is(.mk-image-preview, mk-image-preview) [data-mk-image],
190
+ [data-mk-lightbox-dialog] button[data-mk-lightbox] {
191
+ transition: none;
192
+ scale: none !important;
193
+ }
194
+ }
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { useRef, type HTMLAttributes, type MutableRefObject } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createImagePreview, type ImagePreviewInstance, type ImagePreviewOptions } from './engine';
5
+
6
+ export interface PreviewImage {
7
+ src: string;
8
+ /** Smaller image for the thumbnail (default: src). */
9
+ thumb?: string;
10
+ alt: string;
11
+ caption?: string;
12
+ }
13
+
14
+ export interface ImagePreviewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'>, ImagePreviewOptions {
15
+ images: PreviewImage[];
16
+ instanceRef?: MutableRefObject<ImagePreviewInstance | null>;
17
+ }
18
+
19
+ /** <ImagePreview images={[{ src, thumb, alt, caption }]} />: a grid of thumbnails that open a viewer. */
20
+ export function ImagePreview({ images, labels, loop, onChange, onOpenChange, instanceRef, className, ...props }: ImagePreviewProps) {
21
+ const ref = useRef<HTMLDivElement>(null);
22
+ const change = useLatest(onChange);
23
+ const openChange = useLatest(onOpenChange);
24
+ const key = JSON.stringify(labels ?? {});
25
+ useIsomorphicLayoutEffect(() => {
26
+ const created = createImagePreview(ref.current, { labels, loop, onChange: (i) => change.current?.(i), onOpenChange: (open) => openChange.current?.(open) });
27
+ if (instanceRef) instanceRef.current = created;
28
+ return () => {
29
+ created.destroy();
30
+ if (instanceRef) instanceRef.current = null;
31
+ };
32
+ // eslint-disable-next-line react-hooks/exhaustive-deps
33
+ }, [key, loop]);
34
+ return (
35
+ <div {...props} ref={ref} className={['mk-image-preview', className].filter(Boolean).join(' ')}>
36
+ {images.map((image, i) => (
37
+ <button key={`${image.src}-${i}`} type="button" data-mk-image="" data-mk-src={image.src} data-mk-caption={image.caption} aria-label={`Ver ${image.alt}`}>
38
+ <img src={image.thumb ?? image.src} alt={image.alt} loading="lazy" decoding="async" />
39
+ </button>
40
+ ))}
41
+ </div>
42
+ );
43
+ }
@@ -0,0 +1,56 @@
1
+ # json-viewer
2
+
3
+ Visualizador de JSON con nodos expandibles, montado sobre `tree-view`. Muestra cada clave con su valor coloreado por tipo: textos en verde, números en azul, booleanos en violeta y `null` atenuado en cursiva; los colores (`"#7c5cff"`, `"rgb(…)"`) llevan su muestra. Los objetos y las listas se resumen («6 claves», «3 elementos») y se abren por niveles con el movimiento del árbol: la altura crece (.32 s, curva signature), los hijos salen de un blur y la flecha gira.
4
+
5
+ Cada fila tiene un botón de copiar que se vuelve una palomita con un pop; Ctrl/⌘ + C copia el valor de la fila enfocada. La búsqueda resalta claves y valores y abre lo que los contiene. Con `setData()` se conserva lo abierto: los valores que cambiaron destellan un momento en el acento y las claves nuevas entran saliendo de un blur, ideal para respuestas que llegan en vivo. La ruta de la fila seleccionada (`$.user.roles[0]`) aparece abajo, lista para copiar.
6
+
7
+ ## HTML / Astro
8
+
9
+ ```html
10
+ <mk-json-viewer class="mk-json-viewer" expand-depth="2" search
11
+ data='{"user":{"name":"Ana","roles":["admin","editor"],"color":"#7c5cff"},"total":1250.5}'></mk-json-viewer>
12
+
13
+ <!-- O el JSON escrito dentro: -->
14
+ <mk-json-viewer class="mk-json-viewer" root-name="respuesta">
15
+ <script type="application/json">{ "ok": true, "items": [1, 2, 3] }</script>
16
+ </mk-json-viewer>
17
+
18
+ <script>
19
+ const viewer = document.querySelector('mk-json-viewer');
20
+ viewer.addEventListener('mk:copy', (event) => console.log('Copiado', event.detail.path));
21
+ </script>
22
+ ```
23
+
24
+ ## React
25
+
26
+ ```tsx
27
+ <JsonViewer data={respuesta} expandDepth={2} search onSelect={(path, value) => console.log(path, value)} />
28
+ ```
29
+
30
+ Cambiar `data` conserva lo abierto y marca lo que cambió.
31
+
32
+ ## Opciones
33
+
34
+ | Opción | Atributo | Por defecto | Qué hace |
35
+ |---|---|---|---|
36
+ | `data` | `data` (JSON) | el JSON escrito dentro | El valor a mostrar. |
37
+ | `expandDepth` | `expand-depth` | `1` | Niveles abiertos al empezar: `1` muestra las claves de arriba cerradas, `2` las abre… (`Infinity`, todo). |
38
+ | `rootName` | `root-name` | — | Una fila para el valor completo con este nombre; sin ella, las claves de arriba son las primeras filas. |
39
+ | `search` | `search` | `false` | Campo de búsqueda; un texto es su marcador. |
40
+ | `toolbar` | `toolbar` | `true` | Botones para expandir, contraer y copiar todo. |
41
+ | `sortKeys` | `sort-keys` | `false` | Claves en orden alfabético. |
42
+ | `label` | `label` | `'Datos JSON'` | Nombre accesible. |
43
+ | `onSelect(path, value)` | — | — | Se seleccionó una fila. |
44
+ | `onCopy(text, path)` | — | — | Se copió un valor. |
45
+
46
+ ## API y eventos
47
+
48
+ `setData(data)`, `expandAll()`, `collapseAll()`, `reveal(path)`, `filter(texto)`, `copy(path = '$')` (promesa: `true` si se copió), `data`, `destroy()`. Eventos `mk:select` con `{ path, value }` y `mk:copy` con `{ text, path }`. El ayudante `jsonPath(padre, clave)` arma rutas como las del visor.
49
+
50
+ ## Variables CSS
51
+
52
+ `--mk-jv-height` (alto máximo antes de desplazarse dentro), `--mk-jv-string`, `--mk-jv-number`, `--mk-jv-boolean` (colores de los valores; cambian solos en modo oscuro).
53
+
54
+ ## Accesibilidad
55
+
56
+ Es un árbol de WAI-ARIA con todo su teclado (↑ ↓, → abre, ← cierra, `*`, Inicio, Fin, escribir para saltar), con Ctrl/⌘ + C para copiar sin buscar el botón. Los botones de copiar no son paradas de tabulación y dicen qué copian («Copiar $.user.name»). Un estado discreto anuncia cada copia, las coincidencias y lo que cambió con datos nuevos. Con movimiento reducido no hay destellos ni animaciones.
@@ -0,0 +1,37 @@
1
+ import { MkElement, defineElement, type OptionType } from '../../elements/base';
2
+ import { createJsonViewer, type JsonViewerInstance } from './engine';
3
+
4
+ /**
5
+ * <mk-json-viewer class="mk-json-viewer" expand-depth="2" search data='{"user":{"name":"Ana","roles":["admin"]}}'></mk-json-viewer>
6
+ * Or the JSON as the element's text. Emits mk:select ({path, value}) and mk:copy ({text, path}).
7
+ */
8
+ export class MkJsonViewerElement extends MkElement<JsonViewerInstance> {
9
+ static options: Record<string, OptionType> = {
10
+ data: 'json',
11
+ expandDepth: 'number',
12
+ rootName: 'string',
13
+ search: 'string',
14
+ toolbar: 'boolean',
15
+ sortKeys: 'boolean',
16
+ label: 'string',
17
+ };
18
+
19
+ protected create(options: Record<string, unknown>) {
20
+ // search="" (present without text) turns the field on with its default placeholder.
21
+ const search = this.hasAttribute('search') ? (this.getAttribute('search') || true) : undefined;
22
+ return createJsonViewer(this, { ...options, search });
23
+ }
24
+
25
+ attributeChangedCallback(name: string, previous: string | null, next: string | null) {
26
+ if (previous === next || !this.instance) return;
27
+ if (name === 'data') {
28
+ try {
29
+ this.instance.setData(JSON.parse(next ?? 'null'));
30
+ } catch {
31
+ // Malformed JSON: keep the current data.
32
+ }
33
+ } else super.attributeChangedCallback(name, previous, next);
34
+ }
35
+ }
36
+
37
+ export const defineJsonViewer = () => defineElement('mk-json-viewer', MkJsonViewerElement);