@yewdiel21/motion-kit 0.9.1 → 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 (458) hide show
  1. package/README.md +72 -0
  2. package/dist/auto/index.d.ts +72 -3
  3. package/dist/auto/index.js +139 -3
  4. package/dist/auto/index.js.map +1 -1
  5. package/dist/{chunk-BFC27F2G.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-XDWBVFHQ.js → chunk-EY4BJKC4.js} +1203 -29
  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/calendar.css +159 -0
  17. package/dist/css/components/chart-tooltip.css +168 -0
  18. package/dist/css/components/chart.css +296 -0
  19. package/dist/css/components/code-block.css +264 -0
  20. package/dist/css/components/color-area.css +82 -0
  21. package/dist/css/components/color-field.css +67 -0
  22. package/dist/css/components/color-picker.css +131 -0
  23. package/dist/css/components/color-slider.css +113 -0
  24. package/dist/css/components/command-search.css +252 -0
  25. package/dist/css/components/context-menu.css +23 -0
  26. package/dist/css/components/credit-card.css +288 -0
  27. package/dist/css/components/data-table.css +591 -0
  28. package/dist/css/components/date-range-picker.css +168 -0
  29. package/dist/css/components/date-time-picker.css +178 -0
  30. package/dist/css/components/drawer.css +58 -0
  31. package/dist/css/components/editable-table.css +201 -0
  32. package/dist/css/components/empty-state.css +61 -0
  33. package/dist/css/components/file-explorer.css +633 -0
  34. package/dist/css/components/filter-builder.css +232 -0
  35. package/dist/css/components/hold-confirm.css +87 -0
  36. package/dist/css/components/hover-card.css +20 -0
  37. package/dist/css/components/image-preview.css +195 -0
  38. package/dist/css/components/input-otp.css +10 -0
  39. package/dist/css/components/json-viewer.css +272 -0
  40. package/dist/css/components/like-button.css +78 -0
  41. package/dist/css/components/link-preview.css +275 -0
  42. package/dist/css/components/loading-button.css +77 -0
  43. package/dist/css/components/multi-select.css +267 -0
  44. package/dist/css/components/notification-bell.css +127 -0
  45. package/dist/css/components/pagination.css +11 -0
  46. package/dist/css/components/password-strength.css +183 -0
  47. package/dist/css/components/popover.css +25 -0
  48. package/dist/css/components/progress.css +140 -0
  49. package/dist/css/components/quantity.css +16 -0
  50. package/dist/css/components/questionnaire.css +217 -0
  51. package/dist/css/components/range-calendar.css +186 -0
  52. package/dist/css/components/range-slider.css +192 -0
  53. package/dist/css/components/resizable-panel.css +243 -0
  54. package/dist/css/components/rich-text-editor.css +284 -0
  55. package/dist/css/components/scratch-card.css +75 -0
  56. package/dist/css/components/skeleton.css +108 -0
  57. package/dist/css/components/slot-picker.css +126 -0
  58. package/dist/css/components/split-view.css +19 -0
  59. package/dist/css/components/star-rating.css +80 -0
  60. package/dist/css/components/stat-card.css +164 -0
  61. package/dist/css/components/studio-dock.css +35 -0
  62. package/dist/css/components/swipe-actions.css +105 -0
  63. package/dist/css/components/tag-input.css +244 -0
  64. package/dist/css/components/time-field.css +88 -0
  65. package/dist/css/components/timeline.css +219 -0
  66. package/dist/css/components/tooltip.css +79 -0
  67. package/dist/css/components/trailer.css +37 -0
  68. package/dist/css/components/tree-view.css +210 -0
  69. package/dist/css/components/virtual-list.css +195 -0
  70. package/dist/css/controls.css +65 -0
  71. package/dist/css/tokens.css +185 -181
  72. package/dist/elements/index.d.ts +742 -5
  73. package/dist/elements/index.js +1527 -8
  74. package/dist/elements/index.js.map +1 -1
  75. package/dist/{engine-GDGwQnJE.d.ts → engine-Cn6IhE3z.d.ts} +1 -1
  76. package/dist/engine-QECQnGz1.d.ts +33 -0
  77. package/dist/index.css +10113 -181
  78. package/dist/index.d.ts +89 -5
  79. package/dist/index.js +285 -9
  80. package/dist/index.js.map +1 -1
  81. package/dist/react/index.d.ts +678 -23
  82. package/dist/react/index.js +2389 -7
  83. package/dist/react/index.js.map +1 -1
  84. package/dist/{reduced-B98f8LPs.d.ts → reduced-BBUusld6.d.ts} +206 -2
  85. package/dist/reveal-DEvp6U6T.d.ts +2381 -0
  86. package/dist/{store-pHiUvxP8.d.ts → store-DjkTBX7f.d.ts} +2 -2
  87. package/package.json +1 -1
  88. package/registry.json +583 -104
  89. package/skills/motion-kit/SKILL.md +209 -189
  90. package/src/auto/auto-init.ts +136 -0
  91. package/src/components/activity-feed/README.md +56 -0
  92. package/src/components/activity-feed/activity-feed.css +239 -0
  93. package/src/components/activity-feed/element.ts +36 -0
  94. package/src/components/activity-feed/engine.ts +351 -0
  95. package/src/components/activity-feed/react.tsx +32 -0
  96. package/src/components/attachment/README.md +27 -0
  97. package/src/components/attachment/attachment.css +265 -0
  98. package/src/components/attachment/element.ts +50 -0
  99. package/src/components/attachment/engine.ts +469 -0
  100. package/src/components/attachment/react.tsx +65 -0
  101. package/src/components/breadcrumb/README.md +38 -0
  102. package/src/components/breadcrumb/breadcrumb.css +109 -0
  103. package/src/components/breadcrumb/element.ts +27 -0
  104. package/src/components/breadcrumb/engine.ts +249 -0
  105. package/src/components/breadcrumb/react.tsx +30 -0
  106. package/src/components/calendar/README.md +82 -0
  107. package/src/components/calendar/calendar.css +158 -0
  108. package/src/components/calendar/element.ts +27 -0
  109. package/src/components/calendar/engine.ts +412 -0
  110. package/src/components/calendar/react.tsx +69 -0
  111. package/src/components/chart/README.md +24 -0
  112. package/src/components/chart/chart.css +295 -0
  113. package/src/components/chart/element.ts +41 -0
  114. package/src/components/chart/engine.ts +811 -0
  115. package/src/components/chart/react.tsx +50 -0
  116. package/src/components/chart-tooltip/README.md +53 -0
  117. package/src/components/chart-tooltip/chart-tooltip.css +167 -0
  118. package/src/components/chart-tooltip/element.ts +37 -0
  119. package/src/components/chart-tooltip/engine.ts +442 -0
  120. package/src/components/chart-tooltip/react.tsx +47 -0
  121. package/src/components/code-block/README.md +19 -0
  122. package/src/components/code-block/code-block.css +263 -0
  123. package/src/components/code-block/element.ts +34 -0
  124. package/src/components/code-block/engine.ts +254 -0
  125. package/src/components/code-block/highlight.ts +244 -0
  126. package/src/components/code-block/react.tsx +94 -0
  127. package/src/components/color-area/README.md +50 -0
  128. package/src/components/color-area/color-area.css +81 -0
  129. package/src/components/color-area/element.ts +26 -0
  130. package/src/components/color-area/engine.ts +246 -0
  131. package/src/components/color-area/react.tsx +42 -0
  132. package/src/components/color-field/README.md +43 -0
  133. package/src/components/color-field/color-field.css +66 -0
  134. package/src/components/color-field/element.ts +26 -0
  135. package/src/components/color-field/engine.ts +174 -0
  136. package/src/components/color-field/react.tsx +42 -0
  137. package/src/components/color-picker/README.md +45 -0
  138. package/src/components/color-picker/color-picker.css +130 -0
  139. package/src/components/color-picker/element.ts +45 -0
  140. package/src/components/color-picker/engine.ts +314 -0
  141. package/src/components/color-picker/react.tsx +47 -0
  142. package/src/components/color-slider/README.md +55 -0
  143. package/src/components/color-slider/color-slider.css +112 -0
  144. package/src/components/color-slider/element.ts +26 -0
  145. package/src/components/color-slider/engine.ts +234 -0
  146. package/src/components/color-slider/react.tsx +45 -0
  147. package/src/components/command-search/README.md +15 -0
  148. package/src/components/command-search/command-search.css +251 -0
  149. package/src/components/command-search/element.ts +36 -0
  150. package/src/components/command-search/engine.ts +356 -0
  151. package/src/components/command-search/match.ts +53 -0
  152. package/src/components/command-search/react.tsx +66 -0
  153. package/src/components/context-menu/README.md +31 -0
  154. package/src/components/context-menu/context-menu.css +22 -0
  155. package/src/components/context-menu/element.ts +40 -0
  156. package/src/components/context-menu/engine.ts +153 -0
  157. package/src/components/context-menu/react.tsx +57 -0
  158. package/src/components/credit-card/README.md +20 -0
  159. package/src/components/credit-card/card.ts +77 -0
  160. package/src/components/credit-card/credit-card.css +287 -0
  161. package/src/components/credit-card/element.ts +33 -0
  162. package/src/components/credit-card/engine.ts +410 -0
  163. package/src/components/credit-card/react.tsx +36 -0
  164. package/src/components/data-table/README.md +83 -0
  165. package/src/components/data-table/data-table.css +590 -0
  166. package/src/components/data-table/element.ts +54 -0
  167. package/src/components/data-table/engine.ts +1059 -0
  168. package/src/components/data-table/react.tsx +94 -0
  169. package/src/components/date-range-picker/README.md +51 -0
  170. package/src/components/date-range-picker/date-range-picker.css +167 -0
  171. package/src/components/date-range-picker/element.ts +52 -0
  172. package/src/components/date-range-picker/engine.ts +352 -0
  173. package/src/components/date-range-picker/react.tsx +57 -0
  174. package/src/components/date-time-picker/README.md +48 -0
  175. package/src/components/date-time-picker/date-time-picker.css +177 -0
  176. package/src/components/date-time-picker/element.ts +47 -0
  177. package/src/components/date-time-picker/engine.ts +350 -0
  178. package/src/components/date-time-picker/react.tsx +54 -0
  179. package/src/components/dialog/README.md +103 -102
  180. package/src/components/dialog/element.ts +123 -118
  181. package/src/components/dialog/engine.ts +449 -432
  182. package/src/components/drawer/README.md +28 -0
  183. package/src/components/drawer/drawer.css +57 -0
  184. package/src/components/drawer/element.ts +21 -0
  185. package/src/components/drawer/engine.ts +56 -0
  186. package/src/components/drawer/react.tsx +85 -0
  187. package/src/components/editable-table/README.md +67 -0
  188. package/src/components/editable-table/editable-table.css +200 -0
  189. package/src/components/editable-table/element.ts +45 -0
  190. package/src/components/editable-table/engine.ts +775 -0
  191. package/src/components/editable-table/react.tsx +79 -0
  192. package/src/components/empty-state/README.md +41 -0
  193. package/src/components/empty-state/element.ts +23 -0
  194. package/src/components/empty-state/empty-state.css +60 -0
  195. package/src/components/empty-state/engine.ts +232 -0
  196. package/src/components/empty-state/react.tsx +34 -0
  197. package/src/components/file-explorer/README.md +67 -0
  198. package/src/components/file-explorer/element.ts +39 -0
  199. package/src/components/file-explorer/engine.ts +1307 -0
  200. package/src/components/file-explorer/file-explorer.css +632 -0
  201. package/src/components/file-explorer/react.tsx +77 -0
  202. package/src/components/filter-builder/README.md +47 -0
  203. package/src/components/filter-builder/element.ts +34 -0
  204. package/src/components/filter-builder/engine.ts +365 -0
  205. package/src/components/filter-builder/filter-builder.css +231 -0
  206. package/src/components/filter-builder/react.tsx +36 -0
  207. package/src/components/hold-confirm/README.md +13 -0
  208. package/src/components/hold-confirm/element.ts +24 -0
  209. package/src/components/hold-confirm/engine.ts +216 -0
  210. package/src/components/hold-confirm/hold-confirm.css +86 -0
  211. package/src/components/hold-confirm/react.tsx +28 -0
  212. package/src/components/hover-card/README.md +20 -0
  213. package/src/components/hover-card/element.ts +8 -0
  214. package/src/components/hover-card/engine.ts +131 -0
  215. package/src/components/hover-card/hover-card.css +19 -0
  216. package/src/components/hover-card/react.tsx +19 -0
  217. package/src/components/image-preview/README.md +16 -0
  218. package/src/components/image-preview/element.ts +26 -0
  219. package/src/components/image-preview/engine.ts +325 -0
  220. package/src/components/image-preview/image-preview.css +194 -0
  221. package/src/components/image-preview/react.tsx +43 -0
  222. package/src/components/input-otp/README.md +18 -0
  223. package/src/components/input-otp/element.ts +14 -0
  224. package/src/components/input-otp/engine.ts +106 -0
  225. package/src/components/input-otp/input-otp.css +9 -0
  226. package/src/components/input-otp/react.tsx +21 -0
  227. package/src/components/json-viewer/README.md +56 -0
  228. package/src/components/json-viewer/element.ts +37 -0
  229. package/src/components/json-viewer/engine.ts +440 -0
  230. package/src/components/json-viewer/json-viewer.css +271 -0
  231. package/src/components/json-viewer/react.tsx +56 -0
  232. package/src/components/like-button/README.md +13 -0
  233. package/src/components/like-button/element.ts +47 -0
  234. package/src/components/like-button/engine.ts +145 -0
  235. package/src/components/like-button/like-button.css +77 -0
  236. package/src/components/like-button/react.tsx +39 -0
  237. package/src/components/link-preview/README.md +19 -0
  238. package/src/components/link-preview/element.ts +29 -0
  239. package/src/components/link-preview/engine.ts +192 -0
  240. package/src/components/link-preview/link-preview.css +274 -0
  241. package/src/components/link-preview/react.tsx +101 -0
  242. package/src/components/loading-button/README.md +45 -0
  243. package/src/components/loading-button/element.ts +34 -0
  244. package/src/components/loading-button/engine.ts +201 -0
  245. package/src/components/loading-button/loading-button.css +76 -0
  246. package/src/components/loading-button/react.tsx +43 -0
  247. package/src/components/multi-select/README.md +48 -0
  248. package/src/components/multi-select/element.ts +28 -0
  249. package/src/components/multi-select/engine.ts +294 -0
  250. package/src/components/multi-select/multi-select.css +266 -0
  251. package/src/components/multi-select/react.tsx +39 -0
  252. package/src/components/notification-bell/README.md +14 -0
  253. package/src/components/notification-bell/element.ts +48 -0
  254. package/src/components/notification-bell/engine.ts +202 -0
  255. package/src/components/notification-bell/notification-bell.css +126 -0
  256. package/src/components/notification-bell/react.tsx +57 -0
  257. package/src/components/pagination/README.md +17 -0
  258. package/src/components/pagination/element.ts +14 -0
  259. package/src/components/pagination/engine.ts +102 -0
  260. package/src/components/pagination/pagination.css +10 -0
  261. package/src/components/pagination/react.tsx +22 -0
  262. package/src/components/password-strength/README.md +17 -0
  263. package/src/components/password-strength/element.ts +22 -0
  264. package/src/components/password-strength/engine.ts +212 -0
  265. package/src/components/password-strength/password-strength.css +182 -0
  266. package/src/components/password-strength/react.tsx +36 -0
  267. package/src/components/password-strength/score.ts +62 -0
  268. package/src/components/popover/README.md +44 -0
  269. package/src/components/popover/element.ts +31 -0
  270. package/src/components/popover/engine.ts +114 -0
  271. package/src/components/popover/popover.css +24 -0
  272. package/src/components/popover/react.tsx +50 -0
  273. package/src/components/progress/README.md +17 -0
  274. package/src/components/progress/element.ts +30 -0
  275. package/src/components/progress/engine.ts +173 -0
  276. package/src/components/progress/progress.css +139 -0
  277. package/src/components/progress/react.tsx +35 -0
  278. package/src/components/quantity/README.md +20 -0
  279. package/src/components/quantity/element.ts +14 -0
  280. package/src/components/quantity/engine.ts +85 -0
  281. package/src/components/quantity/quantity.css +15 -0
  282. package/src/components/quantity/react.tsx +21 -0
  283. package/src/components/questionnaire/README.md +32 -0
  284. package/src/components/questionnaire/element.ts +118 -0
  285. package/src/components/questionnaire/engine.ts +255 -0
  286. package/src/components/questionnaire/questionnaire.css +216 -0
  287. package/src/components/questionnaire/react.tsx +140 -0
  288. package/src/components/range-calendar/README.md +58 -0
  289. package/src/components/range-calendar/element.ts +42 -0
  290. package/src/components/range-calendar/engine.ts +390 -0
  291. package/src/components/range-calendar/range-calendar.css +185 -0
  292. package/src/components/range-calendar/react.tsx +56 -0
  293. package/src/components/range-slider/README.md +23 -0
  294. package/src/components/range-slider/element.ts +53 -0
  295. package/src/components/range-slider/engine.ts +316 -0
  296. package/src/components/range-slider/range-slider.css +191 -0
  297. package/src/components/range-slider/react.tsx +66 -0
  298. package/src/components/resizable-panel/README.md +61 -0
  299. package/src/components/resizable-panel/element.ts +36 -0
  300. package/src/components/resizable-panel/engine.ts +551 -0
  301. package/src/components/resizable-panel/react.tsx +74 -0
  302. package/src/components/resizable-panel/resizable-panel.css +242 -0
  303. package/src/components/rich-text-editor/README.md +57 -0
  304. package/src/components/rich-text-editor/element.ts +34 -0
  305. package/src/components/rich-text-editor/engine.ts +673 -0
  306. package/src/components/rich-text-editor/react.tsx +60 -0
  307. package/src/components/rich-text-editor/rich-text-editor.css +283 -0
  308. package/src/components/scratch-card/README.md +15 -0
  309. package/src/components/scratch-card/element.ts +30 -0
  310. package/src/components/scratch-card/engine.ts +257 -0
  311. package/src/components/scratch-card/react.tsx +40 -0
  312. package/src/components/scratch-card/scratch-card.css +74 -0
  313. package/src/components/skeleton/README.md +15 -0
  314. package/src/components/skeleton/element.ts +32 -0
  315. package/src/components/skeleton/engine.ts +104 -0
  316. package/src/components/skeleton/react.tsx +41 -0
  317. package/src/components/skeleton/skeleton.css +107 -0
  318. package/src/components/slot-picker/README.md +68 -0
  319. package/src/components/slot-picker/element.ts +27 -0
  320. package/src/components/slot-picker/engine.ts +412 -0
  321. package/src/components/slot-picker/react.tsx +76 -0
  322. package/src/components/slot-picker/slot-picker.css +125 -0
  323. package/src/components/split-view/README.md +50 -0
  324. package/src/components/split-view/element.ts +34 -0
  325. package/src/components/split-view/engine.ts +114 -0
  326. package/src/components/split-view/react.tsx +63 -0
  327. package/src/components/split-view/split-view.css +18 -0
  328. package/src/components/star-rating/README.md +14 -0
  329. package/src/components/star-rating/element.ts +26 -0
  330. package/src/components/star-rating/engine.ts +217 -0
  331. package/src/components/star-rating/react.tsx +33 -0
  332. package/src/components/star-rating/star-rating.css +79 -0
  333. package/src/components/stat-card/README.md +42 -0
  334. package/src/components/stat-card/element.ts +40 -0
  335. package/src/components/stat-card/engine.ts +283 -0
  336. package/src/components/stat-card/react.tsx +36 -0
  337. package/src/components/stat-card/stat-card.css +163 -0
  338. package/src/components/studio-dock/README.md +44 -0
  339. package/src/components/studio-dock/element.ts +11 -0
  340. package/src/components/studio-dock/engine.ts +212 -0
  341. package/src/components/studio-dock/react.tsx +23 -0
  342. package/src/components/studio-dock/studio-dock.css +34 -0
  343. package/src/components/swipe-actions/README.md +19 -0
  344. package/src/components/swipe-actions/element.ts +26 -0
  345. package/src/components/swipe-actions/engine.ts +260 -0
  346. package/src/components/swipe-actions/react.tsx +80 -0
  347. package/src/components/swipe-actions/swipe-actions.css +104 -0
  348. package/src/components/tag-input/README.md +42 -0
  349. package/src/components/tag-input/element.ts +39 -0
  350. package/src/components/tag-input/engine.ts +313 -0
  351. package/src/components/tag-input/react.tsx +54 -0
  352. package/src/components/tag-input/tag-input.css +243 -0
  353. package/src/components/time-field/README.md +54 -0
  354. package/src/components/time-field/element.ts +38 -0
  355. package/src/components/time-field/engine.ts +110 -0
  356. package/src/components/time-field/react.tsx +45 -0
  357. package/src/components/time-field/time-field.css +87 -0
  358. package/src/components/timeline/README.md +57 -0
  359. package/src/components/timeline/element.ts +31 -0
  360. package/src/components/timeline/engine.ts +300 -0
  361. package/src/components/timeline/react.tsx +59 -0
  362. package/src/components/timeline/timeline.css +218 -0
  363. package/src/components/tooltip/README.md +46 -0
  364. package/src/components/tooltip/element.ts +22 -0
  365. package/src/components/tooltip/engine.ts +288 -0
  366. package/src/components/tooltip/react.tsx +30 -0
  367. package/src/components/tooltip/tooltip.css +78 -0
  368. package/src/components/trailer/README.md +28 -0
  369. package/src/components/trailer/element.ts +13 -0
  370. package/src/components/trailer/engine.ts +189 -0
  371. package/src/components/trailer/react.tsx +16 -0
  372. package/src/components/trailer/trailer.css +36 -0
  373. package/src/components/tree-view/README.md +61 -0
  374. package/src/components/tree-view/element.ts +32 -0
  375. package/src/components/tree-view/engine.ts +569 -0
  376. package/src/components/tree-view/react.tsx +78 -0
  377. package/src/components/tree-view/tree-view.css +209 -0
  378. package/src/components/virtual-list/README.md +81 -0
  379. package/src/components/virtual-list/element.ts +42 -0
  380. package/src/components/virtual-list/engine.ts +678 -0
  381. package/src/components/virtual-list/react.tsx +101 -0
  382. package/src/components/virtual-list/virtual-list.css +194 -0
  383. package/src/core/chips.ts +196 -0
  384. package/src/core/color.ts +304 -0
  385. package/src/core/date-range.ts +67 -0
  386. package/src/core/dates.ts +108 -0
  387. package/src/core/files.ts +107 -0
  388. package/src/core/filter.ts +229 -0
  389. package/src/core/gsap.ts +80 -65
  390. package/src/core/listbox.ts +332 -0
  391. package/src/core/mark.ts +29 -0
  392. package/src/core/position.ts +89 -0
  393. package/src/core/relative-time.ts +77 -0
  394. package/src/core/segment-field.ts +373 -0
  395. package/src/core/segments.ts +153 -0
  396. package/src/core/tree.ts +116 -0
  397. package/src/core/virtual.ts +79 -0
  398. package/src/css/controls/README.md +14 -0
  399. package/src/css/controls/status.css +63 -0
  400. package/src/css/tokens.css +186 -182
  401. package/src/elements/index.ts +171 -0
  402. package/src/index.ts +97 -0
  403. package/src/motion/micro.ts +248 -0
  404. package/src/motion/sheet.ts +88 -65
  405. package/src/motion/surface.ts +9 -0
  406. package/src/react/index.ts +78 -0
  407. package/src/react/use-action-button.ts +6 -0
  408. package/src/react/use-editorial-reveal.ts +7 -0
  409. package/src/react/use-film-hero.ts +6 -0
  410. package/src/react/use-font-cycle.ts +7 -0
  411. package/src/react/use-image-zoom.ts +5 -0
  412. package/src/react/use-joined-cta.ts +5 -0
  413. package/src/react/use-local-clock.ts +5 -0
  414. package/src/react/use-scroll-dock.ts +5 -0
  415. package/src/react/use-sticker.ts +5 -0
  416. package/src/react/use-studio-dock.ts +8 -0
  417. package/src/react/use-thumbnail-nav.ts +7 -0
  418. package/src/react/use-trailer.ts +6 -0
  419. package/src/react/use-weight-hover.ts +5 -0
  420. package/src/recipes/action-button/README.md +31 -0
  421. package/src/recipes/action-button/action-button.css +113 -0
  422. package/src/recipes/action-button/create-action-button.ts +56 -0
  423. package/src/recipes/editorial-reveal/README.md +35 -0
  424. package/src/recipes/editorial-reveal/create-editorial-reveal.ts +91 -0
  425. package/src/recipes/editorial-reveal/editorial-reveal.css +17 -0
  426. package/src/recipes/film-hero/README.md +23 -0
  427. package/src/recipes/film-hero/create-film-hero.ts +65 -0
  428. package/src/recipes/film-hero/film-hero.css +29 -0
  429. package/src/recipes/font-cycle/README.md +11 -0
  430. package/src/recipes/font-cycle/create-font-cycle.ts +126 -0
  431. package/src/recipes/font-cycle/font-cycle.css +36 -0
  432. package/src/recipes/image-zoom/README.md +16 -0
  433. package/src/recipes/image-zoom/create-image-zoom.ts +27 -0
  434. package/src/recipes/image-zoom/image-zoom.css +7 -0
  435. package/src/recipes/joined-cta/README.md +13 -0
  436. package/src/recipes/joined-cta/create-joined-cta.ts +22 -0
  437. package/src/recipes/joined-cta/joined-cta.css +17 -0
  438. package/src/recipes/local-clock/README.md +31 -0
  439. package/src/recipes/local-clock/create-local-clock.ts +61 -0
  440. package/src/recipes/scroll-dock/README.md +46 -0
  441. package/src/recipes/scroll-dock/create-scroll-dock.ts +72 -0
  442. package/src/recipes/scroll-dock/scroll-dock.css +70 -0
  443. package/src/recipes/sticker/README.md +59 -0
  444. package/src/recipes/sticker/create-sticker.ts +490 -0
  445. package/src/recipes/sticker/sticker.css +112 -0
  446. package/src/recipes/thumbnail-nav/README.md +16 -0
  447. package/src/recipes/thumbnail-nav/create-thumbnail-nav.ts +165 -0
  448. package/src/recipes/thumbnail-nav/thumbnail-nav.css +108 -0
  449. package/src/recipes/weight-hover/README.md +37 -0
  450. package/src/recipes/weight-hover/create-weight-hover.ts +136 -0
  451. package/src/recipes/weight-hover/weight-hover.css +7 -0
  452. package/dist/chunk-BFC27F2G.js.map +0 -1
  453. package/dist/chunk-J6OCFNGU.js +0 -177
  454. package/dist/chunk-J6OCFNGU.js.map +0 -1
  455. package/dist/chunk-JQMJQSXD.js +0 -2374
  456. package/dist/chunk-JQMJQSXD.js.map +0 -1
  457. package/dist/chunk-XDWBVFHQ.js.map +0 -1
  458. package/dist/reveal-C0hCDcB_.d.ts +0 -415
@@ -0,0 +1,569 @@
1
+ import { gsap, EASE, registerMotion } from '../../core/gsap';
2
+ import { requestFrame } from '../../core/frame';
3
+ import { reduced } from '../../core/reduced';
4
+ import { createScope } from '../../core/scope';
5
+ import { inert, type Instance } from '../../core/types';
6
+ import { fromJson } from '../../core/dates';
7
+ import { checkStates, findNode, isBranch, searchTree, toggleCheck, walk, type CheckState, type TreeNode } from '../../core/tree';
8
+ import { foldText, markText } from '../../core/mark';
9
+
10
+ /*
11
+ * Tree view (WAI-ARIA tree): nodes that open and close, three-state
12
+ * checkboxes and a search. A branch opens by its height (.32 s, signature)
13
+ * while its children slide in out of a blur (25 ms apart) and its chevron
14
+ * turns (.3 s, swing); it closes the same way back. The selected row sits on a
15
+ * pill that glides to each new choice (.3 s, swing). A check draws itself and
16
+ * its box pops; a branch with some children checked shows a dash. The search
17
+ * marks the letters that match, opens the branches that hold them and hides
18
+ * the rest; clearing it puts back what was open. loadChildren() fills a branch
19
+ * the first time it opens (a spinner row meanwhile). Keyboard: ↑ ↓ move, → opens
20
+ * or enters, ← closes or goes up, Home, End, * opens the siblings, Enter
21
+ * selects, Space checks (or selects), and typing jumps to a label.
22
+ */
23
+
24
+ export type { TreeNode } from '../../core/tree';
25
+
26
+ export interface TreeViewOptions {
27
+ nodes?: TreeNode[];
28
+ /** Three-state checkboxes (default false). */
29
+ checkable?: boolean;
30
+ checked?: string[];
31
+ /** Open at the start: ids, or true for every branch. */
32
+ expanded?: string[] | boolean;
33
+ selected?: string | null;
34
+ /** A search field above the tree: true, or its placeholder. */
35
+ search?: boolean | string;
36
+ /** Accessible name (default 'Árbol'). */
37
+ label?: string;
38
+ /** Children of a branch with hasChildren, the first time it opens. */
39
+ loadChildren?: (node: TreeNode) => Promise<TreeNode[]> | TreeNode[];
40
+ /** Draw a label yourself (json-viewer, file-explorer); `query` is the search, for marking. */
41
+ renderLabel?: (node: TreeNode, element: HTMLElement, query: string) => void;
42
+ /** The text the search looks in (default: the label). */
43
+ searchText?: (node: TreeNode) => string;
44
+ onSelect?: (node: TreeNode) => void;
45
+ onCheck?: (checked: string[]) => void;
46
+ onExpand?: (node: TreeNode, expanded: boolean) => void;
47
+ }
48
+
49
+ export interface TreeViewInstance extends Instance {
50
+ expand(id: string, animate?: boolean): void;
51
+ collapse(id: string, animate?: boolean): void;
52
+ toggle(id: string): void;
53
+ expandAll(): void;
54
+ collapseAll(): void;
55
+ /** Open the branches above it and bring it into view. */
56
+ reveal(id: string): void;
57
+ select(id: string | null, notify?: boolean): void;
58
+ setChecked(ids: string[]): void;
59
+ filter(query: string): void;
60
+ setNodes(nodes: TreeNode[]): void;
61
+ focusItem(id: string): void;
62
+ readonly selected: string | null;
63
+ readonly checked: string[];
64
+ readonly expanded: string[];
65
+ /** The tree element (role="tree"). */
66
+ readonly element: HTMLElement;
67
+ }
68
+
69
+ const CHEVRON = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 6l6 6-6 6"/></svg>';
70
+ const CHECK = '<svg aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path data-mk-tree-tick d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"/><path data-mk-tree-dash d="M6 12h12"/></svg>';
71
+ let sequence = 0;
72
+
73
+ const fold = foldText;
74
+
75
+ // Kept here too for code that imported it from the tree.
76
+ export { markText };
77
+
78
+ export function createTreeView(root: HTMLElement | null, options: TreeViewOptions = {}): TreeViewInstance {
79
+ if (!root) return inert({ expand() {}, collapse() {}, toggle() {}, expandAll() {}, collapseAll() {}, reveal() {}, select() {}, setChecked() {}, filter() {}, setNodes() {}, focusItem() {}, selected: null, checked: [], expanded: [], element: null as unknown as HTMLElement });
80
+ registerMotion();
81
+ const scope = createScope();
82
+ const uid = `mk-tree-${++sequence}`;
83
+ // JSON text too (data-mk-nodes through autoInit).
84
+ let nodes: TreeNode[] = fromJson(options.nodes) ?? [];
85
+ const expanded = new Set<string>();
86
+ if (options.expanded === true) walk(nodes, (node) => isBranch(node) && expanded.add(node.id));
87
+ else for (const id of options.expanded || []) expanded.add(id);
88
+ /** Every given id checked with what lies under it; branches follow their children. */
89
+ const normalised = (ids: string[]) => ids.reduce((set, id) => toggleCheck(nodes, set, id, true), new Set<string>());
90
+ let checked = normalised(fromJson(options.checked) ?? []);
91
+ let selected: string | null = options.selected ?? null;
92
+ let focused: string | null = selected;
93
+ let query = '';
94
+ let beforeSearch: Set<string> | null = null;
95
+ const loading = new Set<string>();
96
+ const effects = new Set<gsap.core.Animation>();
97
+ const keep = (animation: gsap.core.Animation | null) => {
98
+ if (!animation) return;
99
+ effects.add(animation);
100
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
101
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
102
+ };
103
+
104
+ let searchInput: HTMLInputElement | null = null;
105
+ const status = document.createElement('span');
106
+ status.className = 'mk-sr-only';
107
+ status.setAttribute('role', 'status');
108
+ if (options.search) {
109
+ searchInput = document.createElement('input');
110
+ searchInput.type = 'search';
111
+ searchInput.dataset.mkTreeSearch = '';
112
+ searchInput.placeholder = typeof options.search === 'string' ? options.search : 'Buscar…';
113
+ searchInput.setAttribute('aria-label', 'Buscar en el árbol');
114
+ searchInput.setAttribute('aria-controls', uid);
115
+ }
116
+ const box = document.createElement('div');
117
+ box.dataset.mkTreeBox = '';
118
+ const pill = document.createElement('span');
119
+ pill.dataset.mkTreePill = '';
120
+ pill.setAttribute('aria-hidden', 'true');
121
+ const tree = document.createElement('ul');
122
+ tree.id = uid;
123
+ tree.dataset.mkTree = '';
124
+ tree.setAttribute('role', 'tree');
125
+ tree.setAttribute('aria-label', options.label ?? 'Árbol');
126
+ if (options.checkable) tree.setAttribute('aria-multiselectable', 'true');
127
+ box.append(pill, tree);
128
+ const empty = document.createElement('p');
129
+ empty.dataset.mkTreeEmpty = '';
130
+ empty.textContent = 'Sin coincidencias';
131
+ empty.hidden = true;
132
+ if (searchInput) root.append(searchInput);
133
+ root.append(box, empty, status);
134
+ scope.add(() => {
135
+ for (const effect of effects) effect.kill();
136
+ effects.clear();
137
+ gsap.killTweensOf(pill);
138
+ searchInput?.remove();
139
+ box.remove();
140
+ empty.remove();
141
+ status.remove();
142
+ });
143
+ scope.attr(root, 'data-mk-ready', '');
144
+
145
+ const itemOf = (id: string) => [...tree.querySelectorAll<HTMLElement>('[role="treeitem"]')].find((item) => item.dataset.id === id) ?? null;
146
+ const nodeOf = (id: string) => findNode(nodes, id)?.node ?? null;
147
+ /** Items a reader can reach now: not inside a closed branch, not hidden by the search. */
148
+ const visibleItems = () =>
149
+ [...tree.querySelectorAll<HTMLElement>('[role="treeitem"]')].filter((item) => !item.hidden && !item.closest('[role="group"][hidden], [role="treeitem"][hidden]'));
150
+
151
+ const paintChecks = (animate: boolean) => {
152
+ if (!options.checkable) return;
153
+ const states = checkStates(nodes, checked);
154
+ for (const item of tree.querySelectorAll<HTMLElement>('[role="treeitem"]')) {
155
+ const state = states.get(item.dataset.id!) ?? 'false';
156
+ const before = item.getAttribute('aria-checked') as CheckState | null;
157
+ item.setAttribute('aria-checked', state);
158
+ if (!animate || before === state || reduced()) continue;
159
+ const box = item.querySelector<HTMLElement>(':scope > [data-mk-tree-row] [data-mk-tree-check]');
160
+ if (!box) continue;
161
+ keep(gsap.fromTo(box, { scale: 0.8 }, { scale: 1, duration: 0.35, ease: EASE.click, clearProps: 'transform' }));
162
+ const path = box.querySelector<SVGPathElement>(state === 'true' ? '[data-mk-tree-tick]' : '[data-mk-tree-dash]');
163
+ if (path && state !== 'false') keep(gsap.fromTo(path, { strokeDasharray: state === 'true' ? 1 : 12, strokeDashoffset: state === 'true' ? 1 : 12 }, { strokeDashoffset: 0, duration: 0.3, ease: EASE.wipe, clearProps: 'strokeDasharray,strokeDashoffset' }));
164
+ }
165
+ };
166
+
167
+ const fillLabel = (node: TreeNode, label: HTMLElement) => {
168
+ label.replaceChildren();
169
+ if (options.renderLabel) options.renderLabel(node, label, query);
170
+ else label.append(markText(node.label, query));
171
+ };
172
+
173
+ const row = (node: TreeNode, depth: number, index: number, count: number): HTMLElement => {
174
+ const item = document.createElement('li');
175
+ item.dataset.id = node.id;
176
+ item.setAttribute('role', 'treeitem');
177
+ item.setAttribute('aria-level', String(depth + 1));
178
+ item.setAttribute('aria-setsize', String(count));
179
+ item.setAttribute('aria-posinset', String(index + 1));
180
+ item.tabIndex = -1;
181
+ if (node.disabled) item.setAttribute('aria-disabled', 'true');
182
+ const branch = isBranch(node);
183
+ if (branch) item.setAttribute('aria-expanded', String(expanded.has(node.id)));
184
+ if (!options.checkable && selected === node.id) item.setAttribute('aria-selected', 'true');
185
+ const line = document.createElement('div');
186
+ line.dataset.mkTreeRow = '';
187
+ line.style.setProperty('--mk-tree-depth', String(depth));
188
+ const chevron = document.createElement('span');
189
+ chevron.dataset.mkTreeChevron = '';
190
+ if (branch) chevron.innerHTML = CHEVRON;
191
+ line.append(chevron);
192
+ if (options.checkable) {
193
+ const check = document.createElement('span');
194
+ check.dataset.mkTreeCheck = '';
195
+ check.innerHTML = CHECK;
196
+ line.append(check);
197
+ }
198
+ if (node.icon) {
199
+ const icon = document.createElement('span');
200
+ icon.dataset.mkTreeIcon = '';
201
+ icon.setAttribute('aria-hidden', 'true');
202
+ icon.textContent = node.icon;
203
+ line.append(icon);
204
+ }
205
+ const label = document.createElement('span');
206
+ label.dataset.mkTreeLabel = '';
207
+ fillLabel(node, label);
208
+ line.append(label);
209
+ item.append(line);
210
+ if (branch) {
211
+ const group = document.createElement('ul');
212
+ group.setAttribute('role', 'group');
213
+ group.hidden = !expanded.has(node.id);
214
+ (node.children ?? []).forEach((child, i, all) => group.append(row(child, depth + 1, i, all.length)));
215
+ item.append(group);
216
+ }
217
+ return item;
218
+ };
219
+
220
+ /** The pill under the selected row (or under nothing). */
221
+ const placePill = (animate: boolean) => {
222
+ const item = selected ? itemOf(selected) : null;
223
+ const line = item && visibleItems().includes(item) ? item.querySelector<HTMLElement>(':scope > [data-mk-tree-row]') : null;
224
+ if (!line) {
225
+ gsap.killTweensOf(pill);
226
+ pill.style.opacity = '0';
227
+ return;
228
+ }
229
+ const base = box.getBoundingClientRect();
230
+ const rect = line.getBoundingClientRect();
231
+ const target = { y: rect.top - base.top + box.scrollTop, height: rect.height };
232
+ if (!animate || reduced() || pill.style.opacity !== '1') {
233
+ gsap.killTweensOf(pill);
234
+ gsap.set(pill, target);
235
+ pill.style.opacity = '1';
236
+ return;
237
+ }
238
+ keep(gsap.to(pill, { ...target, duration: 0.3, ease: EASE.swatch, overwrite: 'auto' }));
239
+ };
240
+
241
+ const roving = () => {
242
+ const items = visibleItems();
243
+ const target = (focused && items.find((item) => item.dataset.id === focused)) || (selected && items.find((item) => item.dataset.id === selected)) || items[0];
244
+ for (const item of tree.querySelectorAll<HTMLElement>('[role="treeitem"]')) item.tabIndex = item === target ? 0 : -1;
245
+ };
246
+
247
+ const render = () => {
248
+ tree.replaceChildren(...nodes.map((node, i) => row(node, 0, i, nodes.length)));
249
+ paintChecks(false);
250
+ applySearch(false);
251
+ roving();
252
+ placePill(false);
253
+ };
254
+
255
+ // ── Opening and closing ──────────────────────────────────────────────────
256
+ const animateGroup = (group: HTMLElement, open: boolean) => {
257
+ if (reduced()) {
258
+ group.hidden = !open;
259
+ return placePill(false);
260
+ }
261
+ gsap.killTweensOf(group);
262
+ if (open) {
263
+ group.hidden = false;
264
+ const height = group.scrollHeight;
265
+ keep(gsap.fromTo(group, { height: 0, opacity: 0.5, overflow: 'hidden' }, { height, opacity: 1, duration: 0.32, ease: EASE.signature, clearProps: 'height,opacity,overflow', onUpdate: () => placePill(false) }));
266
+ const rows = [...group.children].slice(0, 14);
267
+ keep(gsap.fromTo(rows, { opacity: 0, x: -6, filter: 'blur(4px)' }, { opacity: 1, x: 0, filter: 'blur(0px)', duration: 0.3, stagger: 0.025, ease: 'power2.out', clearProps: 'opacity,transform,filter' }));
268
+ } else {
269
+ keep(
270
+ gsap.to(group, {
271
+ height: 0,
272
+ opacity: 0,
273
+ overflow: 'hidden',
274
+ duration: 0.24,
275
+ ease: 'power2.in',
276
+ onUpdate: () => placePill(false),
277
+ onComplete: () => {
278
+ group.hidden = true;
279
+ gsap.set(group, { clearProps: 'height,opacity,overflow' });
280
+ placePill(false);
281
+ },
282
+ }),
283
+ );
284
+ }
285
+ };
286
+
287
+ const setOpen = async (id: string, open: boolean, animate = true) => {
288
+ const node = nodeOf(id);
289
+ const item = itemOf(id);
290
+ if (!node || !item || !isBranch(node) || expanded.has(id) === open) return;
291
+ if (open) expanded.add(id);
292
+ else expanded.delete(id);
293
+ item.setAttribute('aria-expanded', String(open));
294
+ const group = item.querySelector<HTMLElement>(':scope > [role="group"]')!;
295
+ // A branch whose children come later: a spinner row, then them.
296
+ if (open && node.hasChildren && !node.children?.length && options.loadChildren && !loading.has(id)) {
297
+ loading.add(id);
298
+ const spinner = document.createElement('li');
299
+ spinner.dataset.mkTreeLoading = '';
300
+ spinner.setAttribute('role', 'none');
301
+ spinner.textContent = 'Cargando…';
302
+ spinner.style.setProperty('--mk-tree-depth', String(Number(item.getAttribute('aria-level'))));
303
+ group.replaceChildren(spinner);
304
+ group.hidden = false;
305
+ item.setAttribute('aria-busy', 'true');
306
+ try {
307
+ node.children = await options.loadChildren(node);
308
+ } catch {
309
+ node.children = [];
310
+ }
311
+ loading.delete(id);
312
+ if (scope.disposed) return;
313
+ item.removeAttribute('aria-busy');
314
+ const depth = Number(item.getAttribute('aria-level'));
315
+ group.replaceChildren(...(node.children ?? []).map((child, i, all) => row(child, depth, i, all.length)));
316
+ paintChecks(false);
317
+ if (!node.children?.length) {
318
+ item.removeAttribute('aria-expanded');
319
+ group.remove();
320
+ return;
321
+ }
322
+ if (animate) animateGroup(group, true);
323
+ } else if (animate) animateGroup(group, open);
324
+ else {
325
+ group.hidden = !open;
326
+ placePill(false);
327
+ }
328
+ roving();
329
+ options.onExpand?.(node, open);
330
+ root.dispatchEvent(new CustomEvent('mk:expand', { bubbles: true, detail: { id, expanded: open } }));
331
+ };
332
+
333
+ // ── Selecting and checking ───────────────────────────────────────────────
334
+ const select = (id: string | null, notify: boolean) => {
335
+ if (id && (nodeOf(id)?.disabled || !itemOf(id))) return;
336
+ const changed = id !== selected;
337
+ selected = id;
338
+ if (!options.checkable) for (const item of tree.querySelectorAll<HTMLElement>('[role="treeitem"]')) {
339
+ if (item.dataset.id === id) item.setAttribute('aria-selected', 'true');
340
+ else item.removeAttribute('aria-selected');
341
+ }
342
+ if (id) focused = id;
343
+ roving();
344
+ placePill(true);
345
+ if (!changed || !notify || !id) return;
346
+ const node = nodeOf(id)!;
347
+ options.onSelect?.(node);
348
+ root.dispatchEvent(new CustomEvent('mk:select', { bubbles: true, detail: { id, node } }));
349
+ };
350
+
351
+ const check = (id: string) => {
352
+ if (!options.checkable || nodeOf(id)?.disabled) return;
353
+ checked = toggleCheck(nodes, checked, id);
354
+ paintChecks(true);
355
+ const list = [...checked];
356
+ const state = checkStates(nodes, checked).get(id);
357
+ status.textContent = `${nodeOf(id)?.label ?? ''} ${state === 'true' ? 'marcado' : 'desmarcado'}`;
358
+ options.onCheck?.(list);
359
+ root.dispatchEvent(new CustomEvent('mk:check', { bubbles: true, detail: { checked: list } }));
360
+ };
361
+
362
+ // ── Search ───────────────────────────────────────────────────────────────
363
+ const applySearch = (announce: boolean) => {
364
+ const result = searchTree(nodes, query, options.searchText);
365
+ if (result.active) {
366
+ beforeSearch ??= new Set(expanded);
367
+ for (const id of result.open) expanded.add(id);
368
+ } else if (beforeSearch) {
369
+ expanded.clear();
370
+ for (const id of beforeSearch) expanded.add(id);
371
+ beforeSearch = null;
372
+ }
373
+ for (const item of tree.querySelectorAll<HTMLElement>('[role="treeitem"]')) {
374
+ const id = item.dataset.id!;
375
+ item.hidden = result.active && !result.visible.has(id);
376
+ if (item.hasAttribute('aria-expanded')) {
377
+ const open = expanded.has(id);
378
+ item.setAttribute('aria-expanded', String(open));
379
+ const group = item.querySelector<HTMLElement>(':scope > [role="group"]');
380
+ if (group) group.hidden = !open;
381
+ }
382
+ const node = nodeOf(id);
383
+ const label = item.querySelector<HTMLElement>(':scope > [data-mk-tree-row] [data-mk-tree-label]');
384
+ if (node && label) fillLabel(node, label);
385
+ }
386
+ empty.hidden = !result.active || result.matches.size > 0;
387
+ if (announce) status.textContent = !result.active ? '' : result.matches.size ? `${result.matches.size} ${result.matches.size === 1 ? 'coincidencia' : 'coincidencias'}` : 'Sin coincidencias';
388
+ roving();
389
+ placePill(false);
390
+ };
391
+ if (searchInput) {
392
+ scope.on(searchInput, 'input', () => {
393
+ query = searchInput!.value;
394
+ applySearch(true);
395
+ });
396
+ scope.on(searchInput, 'keydown', (event) => {
397
+ if ((event as KeyboardEvent).key === 'ArrowDown') {
398
+ event.preventDefault();
399
+ visibleItems()[0]?.focus();
400
+ }
401
+ });
402
+ }
403
+
404
+ // ── Pointer and keyboard ─────────────────────────────────────────────────
405
+ scope.on(tree, 'click', (event) => {
406
+ const target = event.target as Element | null;
407
+ const item = target?.closest<HTMLElement>('[role="treeitem"]');
408
+ if (!item || !tree.contains(item) || item.getAttribute('aria-disabled') === 'true') return;
409
+ const id = item.dataset.id!;
410
+ focused = id;
411
+ if (target?.closest('[data-mk-tree-chevron]') && item.hasAttribute('aria-expanded')) {
412
+ void setOpen(id, item.getAttribute('aria-expanded') !== 'true');
413
+ item.focus();
414
+ return;
415
+ }
416
+ if (options.checkable && target?.closest('[data-mk-tree-check]')) check(id);
417
+ else if (options.checkable) check(id);
418
+ else select(id, true);
419
+ item.focus();
420
+ });
421
+ scope.on(tree, 'dblclick', (event) => {
422
+ const item = (event.target as Element | null)?.closest<HTMLElement>('[role="treeitem"]');
423
+ if (item?.hasAttribute('aria-expanded') && !(event.target as Element).closest('[data-mk-tree-chevron]')) void setOpen(item.dataset.id!, item.getAttribute('aria-expanded') !== 'true');
424
+ });
425
+ scope.on(tree, 'focusin', (event) => {
426
+ const item = (event.target as Element | null)?.closest<HTMLElement>('[role="treeitem"]');
427
+ if (!item) return;
428
+ focused = item.dataset.id!;
429
+ roving();
430
+ });
431
+ let typed = '';
432
+ let typedAt = 0;
433
+ scope.on(tree, 'keydown', (event) => {
434
+ const key = event as KeyboardEvent;
435
+ const item = (key.target as Element | null)?.closest<HTMLElement>('[role="treeitem"]');
436
+ if (!item || key.ctrlKey || key.metaKey || key.altKey) return;
437
+ const items = visibleItems();
438
+ const index = items.indexOf(item);
439
+ const id = item.dataset.id!;
440
+ const go = (target: HTMLElement | undefined) => {
441
+ if (!target) return;
442
+ key.preventDefault();
443
+ target.focus();
444
+ target.scrollIntoView?.({ block: 'nearest' });
445
+ };
446
+ switch (key.key) {
447
+ case 'ArrowDown':
448
+ return go(items[index + 1]);
449
+ case 'ArrowUp':
450
+ return go(items[index - 1]);
451
+ case 'Home':
452
+ return go(items[0]);
453
+ case 'End':
454
+ return go(items[items.length - 1]);
455
+ case 'ArrowRight':
456
+ key.preventDefault();
457
+ if (item.getAttribute('aria-expanded') === 'false') void setOpen(id, true);
458
+ else if (item.getAttribute('aria-expanded') === 'true') go(item.querySelector<HTMLElement>(':scope > [role="group"] > [role="treeitem"]:not([hidden])') ?? undefined);
459
+ return;
460
+ case 'ArrowLeft':
461
+ key.preventDefault();
462
+ if (item.getAttribute('aria-expanded') === 'true') void setOpen(id, false);
463
+ else go(item.parentElement?.closest<HTMLElement>('[role="treeitem"]') ?? undefined);
464
+ return;
465
+ case 'Enter':
466
+ key.preventDefault();
467
+ if (options.checkable) check(id);
468
+ else select(id, true);
469
+ return;
470
+ case ' ':
471
+ key.preventDefault();
472
+ if (options.checkable) check(id);
473
+ else select(id, true);
474
+ return;
475
+ case '*': {
476
+ key.preventDefault();
477
+ const siblings = [...(item.parentElement?.children ?? [])].filter((sibling) => sibling.getAttribute('aria-expanded') === 'false') as HTMLElement[];
478
+ for (const sibling of siblings) void setOpen(sibling.dataset.id!, true);
479
+ return;
480
+ }
481
+ }
482
+ // Typing jumps to the next label that starts with what was typed.
483
+ if (key.key.length === 1 && /\S/.test(key.key)) {
484
+ const now = Date.now();
485
+ typed = now - typedAt > 600 ? key.key : typed + key.key;
486
+ typedAt = now;
487
+ const needle = fold(typed);
488
+ const order = [...items.slice(index + (typed.length === 1 ? 1 : 0)), ...items.slice(0, index + (typed.length === 1 ? 1 : 0))];
489
+ const match = order.find((candidate) => fold(nodeOf(candidate.dataset.id!)?.label ?? '').startsWith(needle));
490
+ if (match) go(match);
491
+ }
492
+ });
493
+
494
+ if (typeof ResizeObserver === 'function') {
495
+ let pending: (() => void) | null = null;
496
+ scope.observe(
497
+ new ResizeObserver(() => {
498
+ if (pending) return;
499
+ pending = requestFrame(() => {
500
+ pending = null;
501
+ placePill(false);
502
+ });
503
+ }),
504
+ ).observe(box);
505
+ scope.add(() => pending?.());
506
+ }
507
+
508
+ render();
509
+
510
+ return {
511
+ expand: (id, animate = true) => void setOpen(id, true, animate),
512
+ collapse: (id, animate = true) => void setOpen(id, false, animate),
513
+ toggle: (id) => void setOpen(id, !expanded.has(id)),
514
+ expandAll() {
515
+ walk(nodes, (node) => {
516
+ if (node.children?.length) expanded.add(node.id);
517
+ });
518
+ render();
519
+ },
520
+ collapseAll() {
521
+ expanded.clear();
522
+ render();
523
+ },
524
+ reveal(id) {
525
+ const found = findNode(nodes, id);
526
+ if (!found) return;
527
+ for (const ancestor of found.path.slice(0, -1)) expanded.add(ancestor.id);
528
+ for (const ancestor of found.path.slice(0, -1)) {
529
+ const item = itemOf(ancestor.id);
530
+ if (!item) continue;
531
+ item.setAttribute('aria-expanded', 'true');
532
+ const group = item.querySelector<HTMLElement>(':scope > [role="group"]');
533
+ if (group) group.hidden = false;
534
+ }
535
+ roving();
536
+ placePill(false);
537
+ itemOf(id)?.scrollIntoView?.({ block: 'nearest' });
538
+ },
539
+ select: (id, notify = false) => select(id, notify),
540
+ setChecked(ids) {
541
+ checked = normalised(ids);
542
+ paintChecks(false);
543
+ },
544
+ filter(next) {
545
+ query = next;
546
+ if (searchInput) searchInput.value = next;
547
+ applySearch(true);
548
+ },
549
+ setNodes(next) {
550
+ if (scope.disposed) return;
551
+ nodes = next;
552
+ render();
553
+ },
554
+ focusItem(id) {
555
+ itemOf(id)?.focus();
556
+ },
557
+ get selected() {
558
+ return selected;
559
+ },
560
+ get checked() {
561
+ return [...checked];
562
+ },
563
+ get expanded() {
564
+ return [...expanded];
565
+ },
566
+ element: tree,
567
+ destroy: () => scope.dispose(),
568
+ };
569
+ }
@@ -0,0 +1,78 @@
1
+ 'use client';
2
+ import { forwardRef, useImperativeHandle, useRef, type HTMLAttributes } from 'react';
3
+ import { useIsomorphicLayoutEffect, useLatest } from '../../react/use-recipe';
4
+ import { createTreeView, type TreeNode, type TreeViewInstance, type TreeViewOptions } from './engine';
5
+
6
+ export interface TreeViewProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onSelect' | 'children'>, Omit<TreeViewOptions, 'nodes'> {
7
+ nodes: TreeNode[];
8
+ }
9
+
10
+ export type TreeViewHandle = Pick<TreeViewInstance, 'expand' | 'collapse' | 'expandAll' | 'collapseAll' | 'reveal' | 'filter'>;
11
+
12
+ /** <TreeView nodes={tree} checkable checked={ids} onCheck={setIds} search />. */
13
+ export const TreeView = forwardRef<TreeViewHandle, TreeViewProps>(function TreeView(
14
+ { nodes, checkable, checked, expanded, selected, search, label, loadChildren, renderLabel, searchText, onSelect, onCheck, onExpand, className, ...props },
15
+ handle,
16
+ ) {
17
+ const ref = useRef<HTMLDivElement>(null);
18
+ const instance = useRef<TreeViewInstance | null>(null);
19
+ const select = useLatest(onSelect);
20
+ const check = useLatest(onCheck);
21
+ const expand = useLatest(onExpand);
22
+ const load = useLatest(loadChildren);
23
+ const draw = useLatest(renderLabel);
24
+ const text = useLatest(searchText);
25
+ useImperativeHandle(
26
+ handle,
27
+ () => ({
28
+ expand: (id, animate) => instance.current?.expand(id, animate),
29
+ collapse: (id, animate) => instance.current?.collapse(id, animate),
30
+ expandAll: () => instance.current?.expandAll(),
31
+ collapseAll: () => instance.current?.collapseAll(),
32
+ reveal: (id) => instance.current?.reveal(id),
33
+ filter: (query) => instance.current?.filter(query),
34
+ }),
35
+ [],
36
+ );
37
+ const nodesKey = JSON.stringify(nodes);
38
+ useIsomorphicLayoutEffect(() => {
39
+ const created = createTreeView(ref.current, {
40
+ nodes: JSON.parse(nodesKey) as TreeNode[],
41
+ checkable,
42
+ checked,
43
+ expanded,
44
+ selected,
45
+ search,
46
+ label,
47
+ loadChildren: loadChildren ? (node) => load.current?.(node) ?? [] : undefined,
48
+ renderLabel: renderLabel ? (node, element, query) => draw.current?.(node, element, query) : undefined,
49
+ searchText: searchText ? (node) => text.current?.(node) ?? node.label : undefined,
50
+ onSelect: (node) => select.current?.(node),
51
+ onCheck: (ids) => check.current?.(ids),
52
+ onExpand: (node, open) => expand.current?.(node, open),
53
+ });
54
+ instance.current = created;
55
+ return () => {
56
+ created.destroy();
57
+ instance.current = null;
58
+ };
59
+ // eslint-disable-next-line react-hooks/exhaustive-deps
60
+ }, [checkable, String(search), label, Boolean(loadChildren), Boolean(renderLabel), Boolean(searchText)]);
61
+ const first = useRef(true);
62
+ useIsomorphicLayoutEffect(() => {
63
+ if (first.current) {
64
+ first.current = false;
65
+ return;
66
+ }
67
+ instance.current?.setNodes(JSON.parse(nodesKey) as TreeNode[]);
68
+ }, [nodesKey]);
69
+ const checkedKey = JSON.stringify(checked ?? null);
70
+ useIsomorphicLayoutEffect(() => {
71
+ if (checked && instance.current && JSON.stringify([...instance.current.checked].sort()) !== JSON.stringify([...checked].sort())) instance.current.setChecked(checked);
72
+ // eslint-disable-next-line react-hooks/exhaustive-deps
73
+ }, [checkedKey]);
74
+ useIsomorphicLayoutEffect(() => {
75
+ if (selected !== undefined && instance.current && instance.current.selected !== selected) instance.current.select(selected);
76
+ }, [selected]);
77
+ return <div {...props} ref={ref} className={['mk-tree-view', className].filter(Boolean).join(' ')} />;
78
+ });