@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,332 @@
1
+ import { gsap, EASE, registerMotion } from './gsap';
2
+ import { requestFrame } from './frame';
3
+ import { reduced } from './reduced';
4
+ import { createScope } from './scope';
5
+ import { place } from './position';
6
+ import { matchScore } from '../components/command-search/match';
7
+
8
+ /*
9
+ * Listbox (multi-select, tag-input): the options under a combobox field, in
10
+ * the top layer (popover API) so no overflow clips them, as wide as the field,
11
+ * below it or above when there is no room. It opens out of the field (from 96 %
12
+ * and a 6 px blur, .22 s, pop curve) while its first options flow in (20 ms
13
+ * apart); a pill glides behind the active option (.3 s, swing) as the arrows or
14
+ * the pointer move it; the letters that match the query are marked (as text);
15
+ * chosen options show a check that draws itself. The component keeps the
16
+ * keyboard: it calls move(), pick() and hide().
17
+ */
18
+
19
+ export interface ListboxOption {
20
+ value: string;
21
+ label: string;
22
+ group?: string;
23
+ hint?: string;
24
+ disabled?: boolean;
25
+ }
26
+
27
+ export interface ListboxOptions {
28
+ /** The combobox input: it gets aria-controls and aria-activedescendant. */
29
+ input: HTMLInputElement;
30
+ /** The box the list hangs from (the field). */
31
+ anchor: HTMLElement;
32
+ /** Where the list element lives in the DOM (it is shown in the top layer). */
33
+ host: HTMLElement;
34
+ multiselectable?: boolean;
35
+ label?: string;
36
+ emptyLabel?: string;
37
+ /** Chosen options (a check and aria-selected). */
38
+ isSelected?: (value: string) => boolean;
39
+ onPick: (option: ListboxOption) => void;
40
+ }
41
+
42
+ export interface Listbox {
43
+ readonly element: HTMLElement;
44
+ /** Show these options with `query`'s letters marked; opens it. */
45
+ show(options: ListboxOption[], query?: string): void;
46
+ hide(): void;
47
+ readonly isOpen: boolean;
48
+ /** Move the active option (wraps); 'first' / 'last' jump. */
49
+ move(delta: number | 'first' | 'last'): void;
50
+ /** Choose the active option; false when there is none. */
51
+ pick(): boolean;
52
+ readonly active: ListboxOption | null;
53
+ /** Re-mark the chosen options (after the value changed). */
54
+ refresh(): void;
55
+ destroy(): void;
56
+ }
57
+
58
+ const CHECK = '<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"/></svg>';
59
+ let sequence = 0;
60
+
61
+ /** `text` with the matched ranges in <mark>, built as text nodes. */
62
+ function marked(text: string, query: string) {
63
+ const fragment = document.createDocumentFragment();
64
+ const match = query.trim() ? matchScore(query, text) : null;
65
+ let at = 0;
66
+ for (const [start, end] of match?.ranges ?? []) {
67
+ if (start > at) fragment.append(text.slice(at, start));
68
+ const mark = document.createElement('mark');
69
+ mark.textContent = text.slice(start, end);
70
+ fragment.append(mark);
71
+ at = end;
72
+ }
73
+ if (at < text.length) fragment.append(text.slice(at));
74
+ return fragment;
75
+ }
76
+
77
+ /** Options that match `query` (fuzzy, accents ignored), best first; all of them, in order, without a query. */
78
+ export function filterOptions<T extends ListboxOption>(options: T[], query: string): T[] {
79
+ if (!query.trim()) return [...options];
80
+ return options
81
+ .map((option, order) => ({ option, order, match: matchScore(query, option.label) ?? (option.hint ? matchScore(query, option.hint) : null) }))
82
+ .filter((entry) => entry.match)
83
+ .sort((a, b) => b.match!.score - a.match!.score || a.order - b.order)
84
+ .map((entry) => entry.option);
85
+ }
86
+
87
+ export function createListbox(options: ListboxOptions): Listbox {
88
+ registerMotion();
89
+ const scope = createScope();
90
+ const uid = `mk-listbox-${++sequence}`;
91
+ const list = document.createElement('div');
92
+ list.id = uid;
93
+ list.dataset.mkListbox = '';
94
+ list.setAttribute('role', 'listbox');
95
+ if (options.multiselectable) list.setAttribute('aria-multiselectable', 'true');
96
+ if (options.label) list.setAttribute('aria-label', options.label);
97
+ const pill = document.createElement('span');
98
+ pill.dataset.mkListboxPill = '';
99
+ pill.setAttribute('aria-hidden', 'true');
100
+ const items = document.createElement('div');
101
+ items.dataset.mkListboxItems = '';
102
+ list.append(pill, items);
103
+ const topLayer = typeof list.showPopover === 'function';
104
+ if (topLayer) list.setAttribute('popover', 'manual');
105
+ else list.hidden = true;
106
+ options.host.append(list);
107
+ scope.attr(options.input, 'aria-controls', uid);
108
+ scope.attr(options.input, 'aria-expanded', 'false');
109
+ scope.attr(options.input, 'aria-activedescendant', null);
110
+ const effects = new Set<gsap.core.Animation>();
111
+ const keep = (animation: gsap.core.Animation | null) => {
112
+ if (!animation) return;
113
+ effects.add(animation);
114
+ // Finished ones leave as new ones arrive: the animation's own onComplete stays untouched.
115
+ for (const effect of effects) if (effect.totalProgress() === 1) effects.delete(effect);
116
+ };
117
+ scope.add(() => {
118
+ for (const effect of effects) effect.kill();
119
+ effects.clear();
120
+ gsap.killTweensOf([list, pill]);
121
+ list.remove();
122
+ });
123
+
124
+ let isOpen = false;
125
+ let shown: ListboxOption[] = [];
126
+ let active = -1;
127
+
128
+ const optionEls = () => [...items.querySelectorAll<HTMLElement>('[role="option"]')];
129
+ const shownNow = () => (topLayer ? list.matches(':popover-open') : !list.hidden);
130
+
131
+ const position = () => {
132
+ const anchor = options.anchor.getBoundingClientRect();
133
+ list.style.width = `${Math.round(anchor.width)}px`;
134
+ const box = list.getBoundingClientRect();
135
+ const placed = place(anchor, { width: anchor.width, height: box.height }, { placement: 'bottom', align: 'start', gap: 6 });
136
+ list.style.left = `${placed.x}px`;
137
+ list.style.top = `${placed.y}px`;
138
+ list.dataset.mkSide = placed.side;
139
+ return placed;
140
+ };
141
+ let pending: (() => void) | null = null;
142
+ const follow = () => {
143
+ if (!isOpen || pending) return;
144
+ pending = requestFrame(() => {
145
+ pending = null;
146
+ if (isOpen) position();
147
+ });
148
+ };
149
+ scope.add(() => pending?.());
150
+ scope.on(window, 'resize', follow);
151
+ scope.on(window, 'scroll', follow, { capture: true, passive: true });
152
+
153
+ const movePill = (animate: boolean) => {
154
+ const option = optionEls()[active];
155
+ if (!option) {
156
+ pill.style.opacity = '0';
157
+ return;
158
+ }
159
+ const box = { y: option.offsetTop, height: option.offsetHeight };
160
+ if (!animate || reduced() || pill.style.opacity === '0') {
161
+ gsap.set(pill, box);
162
+ pill.style.opacity = '1';
163
+ return;
164
+ }
165
+ keep(gsap.to(pill, { ...box, duration: 0.3, ease: EASE.swatch, overwrite: 'auto' }));
166
+ };
167
+
168
+ const setActive = (index: number, animate = true) => {
169
+ const all = optionEls();
170
+ const enabled = all.map((option, i) => (option.getAttribute('aria-disabled') === 'true' ? -1 : i)).filter((i) => i >= 0);
171
+ if (!enabled.length) {
172
+ active = -1;
173
+ options.input.removeAttribute('aria-activedescendant');
174
+ movePill(false);
175
+ return;
176
+ }
177
+ active = index;
178
+ all.forEach((option, i) => option.toggleAttribute('data-mk-active', i === active));
179
+ const option = all[active];
180
+ if (option) {
181
+ options.input.setAttribute('aria-activedescendant', option.id);
182
+ option.scrollIntoView?.({ block: 'nearest' });
183
+ } else options.input.removeAttribute('aria-activedescendant');
184
+ movePill(animate);
185
+ };
186
+
187
+ const mark = () => {
188
+ for (const option of optionEls()) {
189
+ const selected = Boolean(options.isSelected?.(option.dataset.value!));
190
+ const was = option.getAttribute('aria-selected') === 'true';
191
+ option.setAttribute('aria-selected', String(selected));
192
+ if (selected && !was && !reduced()) {
193
+ const path = option.querySelector<SVGPathElement>('[data-mk-option-check] path');
194
+ if (path) keep(gsap.fromTo(path, { strokeDasharray: 1, strokeDashoffset: 1 }, { strokeDashoffset: 0, duration: 0.3, ease: EASE.wipe, clearProps: 'strokeDasharray,strokeDashoffset' }));
195
+ }
196
+ }
197
+ };
198
+
199
+ const render = (query: string) => {
200
+ items.replaceChildren();
201
+ let group: string | undefined;
202
+ shown.forEach((option, i) => {
203
+ if (option.group && option.group !== group) {
204
+ group = option.group;
205
+ const heading = document.createElement('div');
206
+ heading.dataset.mkListboxGroup = '';
207
+ heading.setAttribute('role', 'presentation');
208
+ heading.textContent = option.group;
209
+ items.append(heading);
210
+ }
211
+ const element = document.createElement('div');
212
+ element.id = `${uid}-${i}`;
213
+ element.dataset.mkOption = '';
214
+ element.dataset.value = option.value;
215
+ element.dataset.mkIndex = String(i);
216
+ element.setAttribute('role', 'option');
217
+ element.setAttribute('aria-selected', 'false');
218
+ if (option.disabled) element.setAttribute('aria-disabled', 'true');
219
+ if (options.multiselectable) {
220
+ const check = document.createElement('span');
221
+ check.dataset.mkOptionCheck = '';
222
+ check.innerHTML = CHECK;
223
+ element.append(check);
224
+ }
225
+ const label = document.createElement('span');
226
+ label.dataset.mkOptionLabel = '';
227
+ label.append(marked(option.label, query));
228
+ element.append(label);
229
+ if (option.hint) {
230
+ const hint = document.createElement('span');
231
+ hint.dataset.mkOptionHint = '';
232
+ hint.textContent = option.hint;
233
+ element.append(hint);
234
+ }
235
+ items.append(element);
236
+ });
237
+ if (!shown.length) {
238
+ const empty = document.createElement('div');
239
+ empty.dataset.mkListboxEmpty = '';
240
+ empty.textContent = options.emptyLabel ?? 'Sin coincidencias';
241
+ items.append(empty);
242
+ }
243
+ // Without the transition into the new marks: only real changes draw their check.
244
+ for (const option of optionEls()) option.setAttribute('aria-selected', String(Boolean(options.isSelected?.(option.dataset.value!))));
245
+ };
246
+
247
+ const show = (next: ListboxOption[], query = '') => {
248
+ const opening = !isOpen;
249
+ shown = next;
250
+ render(query);
251
+ if (opening) {
252
+ isOpen = true;
253
+ if (topLayer) {
254
+ if (!shownNow()) list.showPopover();
255
+ } else list.hidden = false;
256
+ options.input.setAttribute('aria-expanded', 'true');
257
+ }
258
+ const placed = position();
259
+ const first = shown.findIndex((option) => !option.disabled);
260
+ pill.style.opacity = '0';
261
+ setActive(query.trim() ? first : -1, false);
262
+ if (opening && !reduced()) {
263
+ gsap.killTweensOf(list);
264
+ gsap.set(list, { transformOrigin: placed.side === 'top' ? '50% 100%' : '50% 0%' });
265
+ keep(gsap.fromTo(list, { opacity: 0, scaleY: 0.96, filter: 'blur(6px)' }, { opacity: 1, scaleY: 1, filter: 'blur(0px)', duration: 0.22, ease: EASE.pop, clearProps: 'opacity,transform,filter' }));
266
+ const firstOptions = optionEls().slice(0, 10);
267
+ if (firstOptions.length) keep(gsap.fromTo(firstOptions, { opacity: 0, y: -4 }, { opacity: 1, y: 0, duration: 0.25, stagger: 0.02, ease: 'power2.out', clearProps: 'opacity,transform' }));
268
+ }
269
+ };
270
+
271
+ const hide = () => {
272
+ if (!isOpen) return;
273
+ isOpen = false;
274
+ active = -1;
275
+ options.input.setAttribute('aria-expanded', 'false');
276
+ options.input.removeAttribute('aria-activedescendant');
277
+ const finish = () => {
278
+ if (topLayer) {
279
+ if (shownNow()) list.hidePopover();
280
+ } else list.hidden = true;
281
+ gsap.set(list, { clearProps: 'opacity,transform,filter' });
282
+ };
283
+ if (reduced()) return finish();
284
+ gsap.killTweensOf(list);
285
+ keep(gsap.to(list, { opacity: 0, scaleY: 0.97, duration: 0.12, ease: 'power2.in', onComplete: finish }));
286
+ };
287
+
288
+ scope.on(items, 'pointermove', (event) => {
289
+ const option = (event.target as Element | null)?.closest<HTMLElement>('[role="option"]');
290
+ if (!option || option.getAttribute('aria-disabled') === 'true') return;
291
+ const index = Number(option.dataset.mkIndex);
292
+ if (index !== active) setActive(index);
293
+ });
294
+ // The pointer must not take the focus from the field.
295
+ scope.on(list, 'pointerdown', (event) => event.preventDefault());
296
+ scope.on(items, 'click', (event) => {
297
+ const option = (event.target as Element | null)?.closest<HTMLElement>('[role="option"]');
298
+ if (!option || option.getAttribute('aria-disabled') === 'true') return;
299
+ options.onPick(shown[Number(option.dataset.mkIndex)]!);
300
+ });
301
+
302
+ return {
303
+ element: list,
304
+ show,
305
+ hide,
306
+ get isOpen() {
307
+ return isOpen;
308
+ },
309
+ move(delta) {
310
+ if (!isOpen) return;
311
+ const all = optionEls();
312
+ const enabled = all.map((option, i) => (option.getAttribute('aria-disabled') === 'true' ? -1 : i)).filter((i) => i >= 0);
313
+ if (!enabled.length) return;
314
+ if (delta === 'first') return setActive(enabled[0]!);
315
+ if (delta === 'last') return setActive(enabled[enabled.length - 1]!);
316
+ const at = enabled.indexOf(active);
317
+ const next = at < 0 ? (delta > 0 ? 0 : enabled.length - 1) : (at + delta + enabled.length) % enabled.length;
318
+ setActive(enabled[next]!);
319
+ },
320
+ pick() {
321
+ const option = shown[active];
322
+ if (!option || option.disabled) return false;
323
+ options.onPick(option);
324
+ return true;
325
+ },
326
+ get active() {
327
+ return shown[active] ?? null;
328
+ },
329
+ refresh: mark,
330
+ destroy: () => scope.dispose(),
331
+ };
332
+ }
@@ -0,0 +1,29 @@
1
+ /*
2
+ * Marking matches (tree-view, json-viewer, data-table): text folded for
3
+ * comparing (accents and case ignored), and text with the matches of a query
4
+ * wrapped in <mark>.
5
+ */
6
+
7
+ /** Lower case without accents ("Canción" → "cancion"); lengths stay the same for precomposed text. */
8
+ export const foldText = (text: string) => text.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
9
+
10
+ /** `text` with the folded matches of `query` in <mark> (accents and case ignored). */
11
+ export function markText(text: string, query: string): DocumentFragment {
12
+ const fragment = document.createDocumentFragment();
13
+ const needle = foldText(query.trim());
14
+ if (!needle) {
15
+ fragment.append(text);
16
+ return fragment;
17
+ }
18
+ const hay = foldText(text);
19
+ let at = 0;
20
+ for (let found = hay.indexOf(needle); found >= 0; found = hay.indexOf(needle, found + needle.length)) {
21
+ if (found > at) fragment.append(text.slice(at, found));
22
+ const mark = document.createElement('mark');
23
+ mark.textContent = text.slice(found, found + needle.length);
24
+ fragment.append(mark);
25
+ at = found + needle.length;
26
+ }
27
+ if (at < text.length) fragment.append(text.slice(at));
28
+ return fragment;
29
+ }
@@ -0,0 +1,89 @@
1
+ /*
2
+ * Floating placement (tooltip, chart-tooltip, listboxes): a box of `size` goes
3
+ * next to `anchor` on the preferred side, flips to the opposite side when its
4
+ * own has no room and the other has more ('auto' takes the roomiest side),
5
+ * slides along the edge to stay `edge` px inside the viewport, and reports
6
+ * where an arrow should point (the anchor's centre, kept off the corners).
7
+ * Pure: the callers measure and write the result.
8
+ */
9
+
10
+ export type Side = 'top' | 'bottom' | 'left' | 'right';
11
+ export type Placement = Side | 'auto';
12
+
13
+ export interface Box {
14
+ left: number;
15
+ top: number;
16
+ width: number;
17
+ height: number;
18
+ }
19
+
20
+ export interface PlaceOptions {
21
+ placement?: Placement;
22
+ /** Space between anchor and box (default 8). */
23
+ gap?: number;
24
+ /** Distance kept from the viewport edges (default 8). */
25
+ edge?: number;
26
+ /** Along the edge: centred on the anchor (default) or flush with its start or end. */
27
+ align?: 'center' | 'start' | 'end';
28
+ /** The arrow stays this far from the box's corners (default 12). */
29
+ arrowPad?: number;
30
+ viewport?: { width: number; height: number };
31
+ }
32
+
33
+ export interface Placed {
34
+ side: Side;
35
+ /** Viewport coordinates of the box's top-left corner. */
36
+ x: number;
37
+ y: number;
38
+ /** Arrow position along the box's edge facing the anchor, in px from its start. */
39
+ arrow: number;
40
+ }
41
+
42
+ export const OPPOSITE: Record<Side, Side> = { top: 'bottom', bottom: 'top', left: 'right', right: 'left' };
43
+
44
+ const clamp = (value: number, min: number, max: number) => Math.max(min, Math.min(value, max));
45
+ const round = (n: number) => Math.round(n * 100) / 100;
46
+
47
+ export function place(anchor: Box, size: { width: number; height: number }, options: PlaceOptions = {}): Placed {
48
+ const { placement = 'top', gap = 8, edge = 8, align = 'center', arrowPad = 12 } = options;
49
+ const view = options.viewport ?? { width: innerWidth, height: innerHeight };
50
+ const room: Record<Side, number> = {
51
+ top: anchor.top - gap - edge,
52
+ bottom: view.height - (anchor.top + anchor.height) - gap - edge,
53
+ left: anchor.left - gap - edge,
54
+ right: view.width - (anchor.left + anchor.width) - gap - edge,
55
+ };
56
+ const need = (side: Side) => (side === 'top' || side === 'bottom' ? size.height : size.width);
57
+ let side: Side;
58
+ if (placement === 'auto') {
59
+ const order: Side[] = ['top', 'bottom', 'right', 'left'];
60
+ side = order.find((candidate) => room[candidate] >= need(candidate)) ?? order.reduce((best, candidate) => (room[candidate] - need(candidate) > room[best] - need(best) ? candidate : best));
61
+ } else {
62
+ side = placement;
63
+ if (room[side] < need(side) && room[OPPOSITE[side]] > room[side]) side = OPPOSITE[side];
64
+ }
65
+ const vertical = side === 'top' || side === 'bottom';
66
+ let x: number;
67
+ let y: number;
68
+ if (vertical) {
69
+ y = side === 'top' ? anchor.top - gap - size.height : anchor.top + anchor.height + gap;
70
+ x = align === 'start' ? anchor.left : align === 'end' ? anchor.left + anchor.width - size.width : anchor.left + anchor.width / 2 - size.width / 2;
71
+ x = clamp(x, edge, view.width - edge - size.width);
72
+ } else {
73
+ x = side === 'left' ? anchor.left - gap - size.width : anchor.left + anchor.width + gap;
74
+ y = align === 'start' ? anchor.top : align === 'end' ? anchor.top + anchor.height - size.height : anchor.top + anchor.height / 2 - size.height / 2;
75
+ y = clamp(y, edge, view.height - edge - size.height);
76
+ }
77
+ const length = vertical ? size.width : size.height;
78
+ const centre = vertical ? anchor.left + anchor.width / 2 - x : anchor.top + anchor.height / 2 - y;
79
+ const pad = Math.min(arrowPad, length / 2);
80
+ return { side, x: round(x), y: round(y), arrow: round(clamp(centre, pad, length - pad)) };
81
+ }
82
+
83
+ /** The point the box should grow from: its edge facing the anchor, at the arrow. */
84
+ export function originOf({ side, arrow }: Placed): string {
85
+ if (side === 'top') return `${arrow}px 100%`;
86
+ if (side === 'bottom') return `${arrow}px 0%`;
87
+ if (side === 'left') return `100% ${arrow}px`;
88
+ return `0% ${arrow}px`;
89
+ }
@@ -0,0 +1,77 @@
1
+ import { capitalize, safeFormat, startOfDay } from './dates';
2
+
3
+ /*
4
+ * Relative times for timelines and feeds: "ahora", "hace 5 min", "hace 2 h",
5
+ * "ayer", "hace 3 días", then the date ("12 nov."; with the year when it is
6
+ * another one). dayLabel() heads groups of a day: "Hoy", "Ayer",
7
+ * "Lunes, 12 de noviembre". everyMinute() shares one timer between every
8
+ * component that refreshes its times.
9
+ */
10
+
11
+ export type TimeInput = Date | string | number;
12
+
13
+ /** A date from a Date, an ISO string or a timestamp; null when unreadable. */
14
+ export function toTime(input: TimeInput | null | undefined): Date | null {
15
+ if (input === null || input === undefined || input === '') return null;
16
+ if (input instanceof Date) return Number.isNaN(input.getTime()) ? null : input;
17
+ if (typeof input === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(input.trim())) {
18
+ const [y, m, d] = input.trim().split('-').map(Number);
19
+ return new Date(y!, m! - 1, d!);
20
+ }
21
+ const date = new Date(input);
22
+ return Number.isNaN(date.getTime()) ? null : date;
23
+ }
24
+
25
+ /** Short for minutes and hours ("hace 5 min", "hace 2 h"), long for days ("hace 3 días", "ayer"). */
26
+ const relative = (locale: string, style: 'short' | 'long' = 'short') => {
27
+ try {
28
+ return new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style });
29
+ } catch {
30
+ return new Intl.RelativeTimeFormat(undefined, { numeric: 'auto', style });
31
+ }
32
+ };
33
+
34
+ const daysBetween = (a: Date, b: Date) => Math.round((startOfDay(a).getTime() - startOfDay(b).getTime()) / 86_400_000);
35
+
36
+ /** "ahora", "hace 5 min", "hace 2 h", "ayer", "hace 3 días", then "12 nov." (with the year when it differs). */
37
+ export function relativeTime(date: Date, now: Date = new Date(), locale = 'es'): string {
38
+ const seconds = Math.round((date.getTime() - now.getTime()) / 1000);
39
+ const abs = Math.abs(seconds);
40
+ const format = relative(locale);
41
+ if (abs < 45) return locale.startsWith('es') ? 'ahora' : format.format(0, 'second');
42
+ if (abs < 3600) return format.format(Math.round(seconds / 60), 'minute');
43
+ const days = daysBetween(date, now);
44
+ if (days === 0) return format.format(Math.round(seconds / 3600), 'hour');
45
+ if (Math.abs(days) < 7) return relative(locale, 'long').format(days, 'day');
46
+ const sameYear = date.getFullYear() === now.getFullYear();
47
+ return safeFormat(locale, { day: 'numeric', month: 'short', ...(sameYear ? {} : { year: 'numeric' }) }).format(date);
48
+ }
49
+
50
+ /** The full date and time, for a title or a screen reader. */
51
+ export function fullTime(date: Date, locale = 'es'): string {
52
+ return safeFormat(locale, { dateStyle: 'full', timeStyle: 'short' }).format(date);
53
+ }
54
+
55
+ /** "Hoy", "Ayer", "Mañana", else "Lunes, 12 de noviembre" (with the year when it differs). */
56
+ export function dayLabel(date: Date, now: Date = new Date(), locale = 'es'): string {
57
+ const days = daysBetween(date, now);
58
+ if (Math.abs(days) <= 1) return capitalize(relative(locale, 'long').format(days, 'day'));
59
+ const sameYear = date.getFullYear() === now.getFullYear();
60
+ return capitalize(safeFormat(locale, { weekday: 'long', day: 'numeric', month: 'long', ...(sameYear ? {} : { year: 'numeric' }) }).format(date));
61
+ }
62
+
63
+ const listeners = new Set<() => void>();
64
+ let timer: ReturnType<typeof setInterval> | undefined;
65
+
66
+ /** Call `fn` every minute (one shared timer); returns the function that stops it. */
67
+ export function everyMinute(fn: () => void): () => void {
68
+ listeners.add(fn);
69
+ timer ??= setInterval(() => listeners.forEach((listener) => listener()), 60_000);
70
+ return () => {
71
+ listeners.delete(fn);
72
+ if (!listeners.size && timer !== undefined) {
73
+ clearInterval(timer);
74
+ timer = undefined;
75
+ }
76
+ };
77
+ }