@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,304 @@
1
+ /*
2
+ * Colour model for the colour components. HSVA is the working space (h 0–360,
3
+ * s and v 0–100, a 0–1): the saturation × brightness area and the hue slider
4
+ * map onto it directly. Parses #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba()
5
+ * and hsl()/hsla() (comma or space syntax, optional "/ alpha"); formats hex,
6
+ * rgb or hsl (alpha only when below 1). A grey keeps the hue it is given, so
7
+ * dragging through grey never resets the hue.
8
+ */
9
+
10
+ export interface Hsva {
11
+ /** 0–360. */
12
+ h: number;
13
+ /** 0–100. */
14
+ s: number;
15
+ /** 0–100. */
16
+ v: number;
17
+ /** 0–1. */
18
+ a: number;
19
+ }
20
+
21
+ export interface Rgba {
22
+ /** 0–255. */
23
+ r: number;
24
+ g: number;
25
+ b: number;
26
+ a: number;
27
+ }
28
+
29
+ export interface Hsla {
30
+ h: number;
31
+ /** 0–100. */
32
+ s: number;
33
+ l: number;
34
+ a: number;
35
+ }
36
+
37
+ export type ColorFormat = 'hex' | 'rgb' | 'hsl';
38
+ export type ColorChannel = 'hue' | 'saturation' | 'brightness' | 'lightness' | 'red' | 'green' | 'blue' | 'alpha';
39
+
40
+ export const CHANNEL_LABELS: Record<ColorChannel, string> = {
41
+ hue: 'Tono',
42
+ saturation: 'Saturación',
43
+ brightness: 'Brillo',
44
+ lightness: 'Luminosidad',
45
+ red: 'Rojo',
46
+ green: 'Verde',
47
+ blue: 'Azul',
48
+ alpha: 'Opacidad',
49
+ };
50
+
51
+ export const CHANNEL_RANGE: Record<ColorChannel, [number, number]> = {
52
+ hue: [0, 360],
53
+ saturation: [0, 100],
54
+ brightness: [0, 100],
55
+ lightness: [0, 100],
56
+ red: [0, 255],
57
+ green: [0, 255],
58
+ blue: [0, 255],
59
+ alpha: [0, 1],
60
+ };
61
+
62
+ const clamp = (n: number, min: number, max: number) => Math.min(max, Math.max(min, Number.isFinite(n) ? n : min));
63
+ /** Two decimals: enough for a lossless 8-bit round trip, without float noise (31.999…). */
64
+ const tidy = (n: number) => Math.round(n * 100) / 100;
65
+ const wrapHue = (h: number) => {
66
+ const hue = ((h % 360) + 360) % 360;
67
+ return hue === 0 && h > 0 ? 360 : hue;
68
+ };
69
+
70
+ const hsva = (h: number, s: number, v: number, a: number): Hsva => ({
71
+ h: tidy(clamp(h, 0, 360)),
72
+ s: tidy(clamp(s, 0, 100)),
73
+ v: tidy(clamp(v, 0, 100)),
74
+ a: Math.round(clamp(a, 0, 1) * 1000) / 1000,
75
+ });
76
+
77
+ /** HSVA → RGBA (channels 0–255, unrounded). */
78
+ export function hsvToRgb({ h, s, v, a }: Hsva): Rgba {
79
+ const sat = s / 100;
80
+ const val = v / 100;
81
+ const f = (n: number) => {
82
+ const k = (n + (h % 360) / 60) % 6;
83
+ return val - val * sat * Math.max(0, Math.min(k, 4 - k, 1));
84
+ };
85
+ return { r: f(5) * 255, g: f(3) * 255, b: f(1) * 255, a };
86
+ }
87
+
88
+ /** RGBA → HSVA; a grey keeps `hue` (and black keeps `saturation`) when given. */
89
+ export function rgbToHsv({ r, g, b, a }: Rgba, hue?: number, saturation?: number): Hsva {
90
+ const [red, green, blue] = [r / 255, g / 255, b / 255];
91
+ const max = Math.max(red, green, blue);
92
+ const min = Math.min(red, green, blue);
93
+ const delta = max - min;
94
+ let h = hue ?? 0;
95
+ if (delta > 0) {
96
+ if (max === red) h = 60 * (((green - blue) / delta) % 6);
97
+ else if (max === green) h = 60 * ((blue - red) / delta + 2);
98
+ else h = 60 * ((red - green) / delta + 4);
99
+ if (h < 0) h += 360;
100
+ }
101
+ const s = max === 0 ? (saturation ?? 0) : (delta / max) * 100;
102
+ return hsva(h, s, max * 100, a);
103
+ }
104
+
105
+ export function hsvToHsl({ h, s, v, a }: Hsva): Hsla {
106
+ const sat = s / 100;
107
+ const val = v / 100;
108
+ const l = val * (1 - sat / 2);
109
+ const sl = l === 0 || l === 1 ? 0 : (val - l) / Math.min(l, 1 - l);
110
+ return { h, s: sl * 100, l: l * 100, a };
111
+ }
112
+
113
+ export function hslToHsv({ h, s, l, a }: Hsla, saturation?: number): Hsva {
114
+ const sat = s / 100;
115
+ const light = l / 100;
116
+ const v = light + sat * Math.min(light, 1 - light);
117
+ const sv = v === 0 ? (saturation ?? 0) / 100 : 2 * (1 - light / v);
118
+ return hsva(h, sv * 100, v * 100, a);
119
+ }
120
+
121
+ const number = (text: string, scale: number) => {
122
+ const value = Number.parseFloat(text);
123
+ if (!Number.isFinite(value)) return Number.NaN;
124
+ return text.trim().endsWith('%') ? (value / 100) * scale : value;
125
+ };
126
+
127
+ const alphaOf = (text: string | undefined) => (text === undefined ? 1 : clamp(number(text, 1), 0, 1));
128
+
129
+ const angle = (text: string) => {
130
+ const value = Number.parseFloat(text);
131
+ if (!Number.isFinite(value)) return Number.NaN;
132
+ if (/turn$/i.test(text)) return value * 360;
133
+ if (/grad$/i.test(text)) return value * 0.9;
134
+ if (/rad$/i.test(text)) return (value * 180) / Math.PI;
135
+ return value;
136
+ };
137
+
138
+ /** "rgb(1, 2, 3)", "rgb(1 2 3 / 50%)" → its parts, or null. */
139
+ function args(text: string, name: string): string[] | null {
140
+ const match = new RegExp(`^${name}a?\\(\\s*([^)]*)\\)$`, 'i').exec(text);
141
+ if (!match) return null;
142
+ const body = match[1]!.trim();
143
+ const [main, slash] = body.split('/');
144
+ const parts = main!.includes(',') ? main!.split(',').map((part) => part.trim()) : main!.trim().split(/\s+/);
145
+ if (slash !== undefined) {
146
+ if (main!.includes(',') || parts.length !== 3) return null;
147
+ parts.push(slash.trim());
148
+ }
149
+ if (parts.length < 3 || parts.length > 4 || parts.some((part) => !part)) return null;
150
+ return parts;
151
+ }
152
+
153
+ /**
154
+ * Any colour the components accept, as HSVA; null when the text is not one.
155
+ * `hue` is kept for greys (no hue of their own).
156
+ */
157
+ export function parseColor(input: string | null | undefined, hue?: number): Hsva | null {
158
+ const text = (input ?? '').trim().toLowerCase();
159
+ if (!text) return null;
160
+ const hex = /^#?([0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/.exec(text);
161
+ if (hex) {
162
+ let digits = hex[1]!;
163
+ if (digits.length <= 4) digits = [...digits].map((c) => c + c).join('');
164
+ const channel = (i: number) => Number.parseInt(digits.slice(i, i + 2), 16);
165
+ return rgbToHsv({ r: channel(0), g: channel(2), b: channel(4), a: digits.length === 8 ? channel(6) / 255 : 1 }, hue);
166
+ }
167
+ const rgb = args(text, 'rgb');
168
+ if (rgb) {
169
+ const [r, g, b] = rgb.slice(0, 3).map((part) => number(part, 255));
170
+ const a = alphaOf(rgb[3]);
171
+ if (![r, g, b, a].every(Number.isFinite)) return null;
172
+ return rgbToHsv({ r: clamp(r!, 0, 255), g: clamp(g!, 0, 255), b: clamp(b!, 0, 255), a }, hue);
173
+ }
174
+ const hsl = args(text, 'hsl');
175
+ if (hsl) {
176
+ const h = angle(hsl[0]!);
177
+ const s = Number.parseFloat(hsl[1]!);
178
+ const l = Number.parseFloat(hsl[2]!);
179
+ const a = alphaOf(hsl[3]);
180
+ if (![h, s, l, a].every(Number.isFinite)) return null;
181
+ const color = hslToHsv({ h: wrapHue(h), s: clamp(s, 0, 100), l: clamp(l, 0, 100), a });
182
+ return color.s === 0 && hue !== undefined ? { ...color, h: hue } : color;
183
+ }
184
+ return null;
185
+ }
186
+
187
+ const DEFAULT: Hsva = { h: 0, s: 100, v: 100, a: 1 };
188
+
189
+ /** A colour option (text or HSVA) as HSVA; `fallback` (default red) when it cannot be read. */
190
+ export function toHsva(input: string | Hsva | null | undefined, fallback: Hsva = DEFAULT): Hsva {
191
+ if (input && typeof input === 'object') return hsva(input.h, input.s, input.v, input.a ?? 1);
192
+ return parseColor(input, fallback.h) ?? { ...fallback };
193
+ }
194
+
195
+ const byte = (n: number) => Math.round(clamp(n, 0, 255));
196
+ const hex2 = (n: number) => byte(n).toString(16).padStart(2, '0');
197
+ const short = (n: number) => String(Math.round(n * 100) / 100);
198
+
199
+ /** #rrggbb, or #rrggbbaa when translucent. */
200
+ export function toHex(color: Hsva): string {
201
+ const { r, g, b, a } = hsvToRgb(color);
202
+ const alpha = byte(a * 255);
203
+ return `#${hex2(r)}${hex2(g)}${hex2(b)}${alpha < 255 ? hex2(alpha) : ''}`;
204
+ }
205
+
206
+ /** The colour as CSS text: hex, "rgb(255 136 0 / 0.5)" or "hsl(32 100% 50%)". */
207
+ export function formatColor(color: Hsva, format: ColorFormat = 'hex'): string {
208
+ if (format === 'rgb') {
209
+ const { r, g, b, a } = hsvToRgb(color);
210
+ return `rgb(${byte(r)} ${byte(g)} ${byte(b)}${a < 1 ? ` / ${short(a)}` : ''})`;
211
+ }
212
+ if (format === 'hsl') {
213
+ const { h, s, l, a } = hsvToHsl(color);
214
+ return `hsl(${Math.round(h) % 360} ${Math.round(s)}% ${Math.round(l)}%${a < 1 ? ` / ${short(a)}` : ''})`;
215
+ }
216
+ return toHex(color);
217
+ }
218
+
219
+ /** Same colour on screen (same 8-bit hex). */
220
+ export const sameColor = (a: Hsva, b: Hsva) => toHex(a) === toHex(b);
221
+
222
+ /** Relative luminance (WCAG), 0–1, ignoring alpha. */
223
+ export function luminance(color: Hsva): number {
224
+ const { r, g, b } = hsvToRgb(color);
225
+ const linear = (n: number) => {
226
+ const c = n / 255;
227
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
228
+ };
229
+ return 0.2126 * linear(r) + 0.7152 * linear(g) + 0.0722 * linear(b);
230
+ }
231
+
232
+ /** One channel's value in its own range (CHANNEL_RANGE). */
233
+ export function getChannel(color: Hsva, channel: ColorChannel): number {
234
+ switch (channel) {
235
+ case 'hue':
236
+ return color.h;
237
+ case 'saturation':
238
+ return color.s;
239
+ case 'brightness':
240
+ return color.v;
241
+ case 'alpha':
242
+ return color.a;
243
+ case 'lightness':
244
+ return tidy(hsvToHsl(color).l);
245
+ default: {
246
+ const rgb = hsvToRgb(color);
247
+ return byte(channel === 'red' ? rgb.r : channel === 'green' ? rgb.g : rgb.b);
248
+ }
249
+ }
250
+ }
251
+
252
+ /** A copy with one channel changed (clamped to its range); the hue survives greys. */
253
+ export function setChannel(color: Hsva, channel: ColorChannel, value: number): Hsva {
254
+ const [min, max] = CHANNEL_RANGE[channel];
255
+ const next = clamp(value, min, max);
256
+ switch (channel) {
257
+ case 'hue':
258
+ return hsva(next, color.s, color.v, color.a);
259
+ case 'saturation':
260
+ return hsva(color.h, next, color.v, color.a);
261
+ case 'brightness':
262
+ return hsva(color.h, color.s, next, color.a);
263
+ case 'alpha':
264
+ return hsva(color.h, color.s, color.v, next);
265
+ case 'lightness': {
266
+ const hsl = hsvToHsl(color);
267
+ return { ...hslToHsv({ ...hsl, l: next }, color.s), h: color.h };
268
+ }
269
+ default: {
270
+ const rgb = hsvToRgb(color);
271
+ const key = channel === 'red' ? 'r' : channel === 'green' ? 'g' : 'b';
272
+ return rgbToHsv({ ...rgb, [key]: next }, color.h, color.s);
273
+ }
274
+ }
275
+ }
276
+
277
+ /** What a channel's value reads as: "210°", "64 %", "136". */
278
+ export function channelText(channel: ColorChannel, value: number, format?: Intl.NumberFormat): string {
279
+ const shown = (n: number) => (format ? format.format(n) : String(Math.round(n)));
280
+ if (channel === 'hue') return `${shown(value)}°`;
281
+ if (channel === 'alpha') return `${shown(value * 100)} %`;
282
+ if (channel === 'red' || channel === 'green' || channel === 'blue') return shown(value);
283
+ return `${shown(value)} %`;
284
+ }
285
+
286
+ const rgbText = (color: Hsva) => formatColor(color, 'rgb');
287
+
288
+ /** A slider track for `channel` at `color`: the channel's whole range, the other channels fixed. */
289
+ export function channelGradient(color: Hsva, channel: ColorChannel, direction = 'to right'): string {
290
+ const opaque = { ...color, a: 1 };
291
+ let stops: string[];
292
+ if (channel === 'hue') stops = [0, 60, 120, 180, 240, 300, 360].map((h) => rgbText({ h, s: 100, v: 100, a: 1 }));
293
+ else if (channel === 'alpha') stops = [rgbText({ ...opaque, a: 0 }), rgbText(opaque)];
294
+ else if (channel === 'lightness') stops = [0, 50, 100].map((l) => rgbText(setChannel(opaque, 'lightness', l)));
295
+ else {
296
+ const [min, max] = CHANNEL_RANGE[channel];
297
+ stops = [setChannel(opaque, channel, min), setChannel(opaque, channel, max)].map(rgbText);
298
+ }
299
+ return `linear-gradient(${direction}, ${stops.join(', ')})`;
300
+ }
301
+
302
+ /** The saturation (x) × brightness (y) square at hue `h`. */
303
+ export const areaBackground = (h: number) =>
304
+ `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${Math.round(h) % 360} 100% 50%))`;
@@ -0,0 +1,67 @@
1
+ import { addDays, dayKey, sameDay, toDate, type DateInput } from './dates';
2
+
3
+ /*
4
+ * Date ranges on local days (range-calendar, date-range-picker): reading a
5
+ * {start, end} option, the day rules (min, max, unavailable days) and the
6
+ * two-click pick — the first click fixes an anchor, the second closes the
7
+ * range in either direction; crossing an unavailable day restarts at the
8
+ * clicked day.
9
+ */
10
+
11
+ export interface DateRange {
12
+ start: Date;
13
+ end: Date;
14
+ }
15
+
16
+ export interface DayRules {
17
+ min?: Date | null;
18
+ max?: Date | null;
19
+ isUnavailable?: (day: Date) => boolean;
20
+ }
21
+
22
+ const valid = (input: DateInput | null | undefined) => {
23
+ if (input === null || input === undefined || input === '') return null;
24
+ const date = toDate(input, new Date(Number.NaN));
25
+ return Number.isNaN(date.getTime()) ? null : date;
26
+ };
27
+
28
+ /** An ordered range from {start, end} (Dates or 'YYYY-MM-DD'); null when either end is missing or unreadable. */
29
+ export function toRange(value: { start?: DateInput | null; end?: DateInput | null } | null | undefined): DateRange | null {
30
+ if (!value) return null;
31
+ const start = valid(value.start);
32
+ const end = valid(value.end);
33
+ if (!start || !end) return null;
34
+ return start <= end ? { start, end } : { start: end, end: start };
35
+ }
36
+
37
+ export const rangeKeys = (range: DateRange) => ({ start: dayKey(range.start), end: dayKey(range.end) });
38
+
39
+ export const inRange = (day: Date, range: DateRange | null | undefined) => !!range && day >= range.start && day <= range.end;
40
+
41
+ /** Days from start to end, both included. */
42
+ export const rangeLength = (range: DateRange) =>
43
+ Math.round((Date.UTC(range.end.getFullYear(), range.end.getMonth(), range.end.getDate()) - Date.UTC(range.start.getFullYear(), range.start.getMonth(), range.start.getDate())) / 86400000) + 1;
44
+
45
+ export function isPickable(day: Date, rules: DayRules = {}): boolean {
46
+ if (rules.min && day < rules.min) return false;
47
+ if (rules.max && day > rules.max) return false;
48
+ return !rules.isUnavailable?.(day);
49
+ }
50
+
51
+ /** Every day of the range can be picked. */
52
+ export function rangeIsPickable(range: DateRange, rules: DayRules = {}): boolean {
53
+ for (let day = range.start; day <= range.end; day = addDays(day, 1)) if (!isPickable(day, rules)) return false;
54
+ return true;
55
+ }
56
+
57
+ /**
58
+ * One click. Without an anchor, `day` becomes it. With one, the range closes
59
+ * (ordered); if it would cross a day that cannot be picked, `day` becomes the
60
+ * new anchor instead. Clicking the anchor again makes a one-day range.
61
+ */
62
+ export function pickDay(anchor: Date | null, day: Date, rules: DayRules = {}): { anchor: Date | null; range: DateRange | null } {
63
+ if (!anchor) return { anchor: day, range: null };
64
+ const range = anchor <= day ? { start: anchor, end: day } : { start: day, end: anchor };
65
+ if (!sameDay(anchor, day) && !rangeIsPickable(range, rules)) return { anchor: day, range: null };
66
+ return { anchor: null, range };
67
+ }
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Calendar date helpers on local dates (no time zone maths): the calendar and
3
+ * slot-picker components share them. Days are compared by their YYYY-MM-DD key.
4
+ */
5
+
6
+ export type DateInput = Date | string | number;
7
+
8
+ /** Local midnight of a date; "2026-10-23" is read as a local day, not UTC. */
9
+ export function toDate(input: DateInput | null | undefined, fallback = new Date()): Date {
10
+ if (input instanceof Date) return Number.isNaN(input.getTime()) ? startOfDay(fallback) : startOfDay(input);
11
+ if (typeof input === 'string') {
12
+ const day = /^(\d{4})-(\d{2})-(\d{2})$/.exec(input.trim());
13
+ if (day) return new Date(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
14
+ }
15
+ if (typeof input === 'string' || typeof input === 'number') {
16
+ const parsed = new Date(input);
17
+ if (!Number.isNaN(parsed.getTime())) return startOfDay(parsed);
18
+ }
19
+ return startOfDay(fallback);
20
+ }
21
+
22
+ /** A full date-time (events keep their hour). */
23
+ export function toDateTime(input: DateInput): Date | null {
24
+ if (input instanceof Date) return Number.isNaN(input.getTime()) ? null : input;
25
+ if (typeof input === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(input.trim())) return toDate(input);
26
+ const parsed = new Date(input);
27
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
28
+ }
29
+
30
+ export const startOfDay = (date: Date) => new Date(date.getFullYear(), date.getMonth(), date.getDate());
31
+
32
+ export const addDays = (date: Date, days: number) => new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
33
+
34
+ /** Same day number in another month, clamped to that month's length (Jan 31 + 1 → Feb 28/29). */
35
+ export function addMonths(date: Date, months: number): Date {
36
+ const target = new Date(date.getFullYear(), date.getMonth() + months, 1);
37
+ const last = new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate();
38
+ return new Date(target.getFullYear(), target.getMonth(), Math.min(date.getDate(), last));
39
+ }
40
+
41
+ export const startOfMonth = (date: Date) => new Date(date.getFullYear(), date.getMonth(), 1);
42
+
43
+ export function startOfWeek(date: Date, firstDay = 0): Date {
44
+ const shift = (date.getDay() - firstDay + 7) % 7;
45
+ return addDays(startOfDay(date), -shift);
46
+ }
47
+
48
+ export const sameDay = (a: Date | null | undefined, b: Date | null | undefined) =>
49
+ !!a && !!b && a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
50
+
51
+ export const sameMonth = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
52
+
53
+ const pad = (n: number) => String(n).padStart(2, '0');
54
+
55
+ /** YYYY-MM-DD of the local day. */
56
+ export const dayKey = (date: Date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
57
+
58
+ /** The weeks shown for a month: from the week of the 1st to the week of the last day (4–6 rows). */
59
+ export function monthWeeks(date: Date, firstDay = 0): Date[][] {
60
+ const first = startOfMonth(date);
61
+ const last = new Date(date.getFullYear(), date.getMonth() + 1, 0);
62
+ const weeks: Date[][] = [];
63
+ for (let start = startOfWeek(first, firstDay); start <= last; start = addDays(start, 7)) {
64
+ weeks.push(Array.from({ length: 7 }, (_, i) => addDays(start, i)));
65
+ }
66
+ return weeks;
67
+ }
68
+
69
+ export const weekDays = (date: Date, firstDay = 0) => Array.from({ length: 7 }, (_, i) => addDays(startOfWeek(date, firstDay), i));
70
+
71
+ /** Weekday names in display order, e.g. ["dom", "lun", …] for es with firstDay 0. */
72
+ export function weekdayNames(locale: string, firstDay = 0, width: 'long' | 'short' | 'narrow' = 'short'): string[] {
73
+ const format = safeFormat(locale, { weekday: width });
74
+ // 2024-09-01 was a Sunday.
75
+ return Array.from({ length: 7 }, (_, i) => format.format(new Date(2024, 8, 1 + ((firstDay + i) % 7))));
76
+ }
77
+
78
+ export function safeFormat(locale: string | undefined, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
79
+ try {
80
+ return new Intl.DateTimeFormat(locale, options);
81
+ } catch {
82
+ return new Intl.DateTimeFormat(undefined, options);
83
+ }
84
+ }
85
+
86
+ /** Numbers in the language of the page around `element`: "4,6" and "1204" on a Spanish page, "4.6" and "1,204" in English. */
87
+ export function numberFormat(element: Element, options: Intl.NumberFormatOptions = {}): Intl.NumberFormat {
88
+ const locale = element.closest('[lang]')?.getAttribute('lang') || undefined;
89
+ try {
90
+ return new Intl.NumberFormat(locale, options);
91
+ } catch {
92
+ return new Intl.NumberFormat(undefined, options);
93
+ }
94
+ }
95
+
96
+ export const capitalize =(text: string) => text.charAt(0).toLocaleUpperCase() + text.slice(1);
97
+
98
+ /** Options given as JSON text (data-mk-* attributes through autoInit) become values. */
99
+ export function fromJson<T>(value: T | string | undefined): T | undefined {
100
+ if (typeof value !== 'string') return value;
101
+ try {
102
+ return JSON.parse(value) as T;
103
+ } catch {
104
+ return undefined;
105
+ }
106
+ }
107
+
108
+ export const clampFirstDay =(n: unknown) => (Number.isInteger(n) && (n as number) >= 0 && (n as number) <= 6 ? (n as number) : 0);
@@ -0,0 +1,107 @@
1
+ import { toTime, type TimeInput } from './relative-time';
2
+
3
+ /*
4
+ * Files (file-explorer), the pure part: what is a folder, the family an
5
+ * extension belongs to (its colour), sizes in KB and MB, a free name in a
6
+ * folder ("Nueva carpeta (2)") and the order of a folder's content (folders
7
+ * first, then by name, size or date; numbers in names in their natural order).
8
+ */
9
+
10
+ export interface FileEntry {
11
+ id: string;
12
+ name: string;
13
+ /** 'folder' or 'file'; without it, an entry with children is a folder. */
14
+ kind?: 'folder' | 'file';
15
+ children?: FileEntry[];
16
+ /** Bytes. */
17
+ size?: number;
18
+ modified?: TimeInput;
19
+ }
20
+
21
+ export type FileFamily = 'folder' | 'image' | 'pdf' | 'doc' | 'sheet' | 'slides' | 'code' | 'archive' | 'media' | 'other';
22
+ export type FileSort = 'name' | 'size' | 'modified';
23
+
24
+ export const isFolder = (entry: FileEntry) => (entry.kind ? entry.kind === 'folder' : Array.isArray(entry.children));
25
+
26
+ /** "pdf" for "Informe.PDF"; '' without one (".gitignore" has none). */
27
+ export function extensionOf(name: string): string {
28
+ const dot = name.lastIndexOf('.');
29
+ return dot > 0 && dot < name.length - 1 ? name.slice(dot + 1).toLowerCase() : '';
30
+ }
31
+
32
+ const FAMILIES: Record<Exclude<FileFamily, 'folder' | 'other'>, string[]> = {
33
+ image: ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'avif', 'heic', 'bmp', 'ico', 'fig', 'psd', 'ai', 'sketch'],
34
+ pdf: ['pdf'],
35
+ doc: ['doc', 'docx', 'txt', 'md', 'rtf', 'odt', 'pages'],
36
+ sheet: ['xls', 'xlsx', 'csv', 'numbers', 'ods'],
37
+ slides: ['ppt', 'pptx', 'key', 'odp'],
38
+ code: ['js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'json', 'css', 'scss', 'html', 'astro', 'vue', 'svelte', 'py', 'rb', 'go', 'rs', 'java', 'c', 'cpp', 'h', 'php', 'sh', 'sql', 'xml', 'yml', 'yaml', 'toml'],
39
+ archive: ['zip', 'rar', '7z', 'tar', 'gz', 'tgz', 'bz2'],
40
+ media: ['mp3', 'wav', 'ogg', 'flac', 'm4a', 'mp4', 'mov', 'avi', 'mkv', 'webm'],
41
+ };
42
+
43
+ export function fileFamily(entry: FileEntry): FileFamily {
44
+ if (isFolder(entry)) return 'folder';
45
+ const extension = extensionOf(entry.name);
46
+ for (const [family, extensions] of Object.entries(FAMILIES)) if (extensions.includes(extension)) return family as FileFamily;
47
+ return 'other';
48
+ }
49
+
50
+ /** "820 B", "12 KB", "3,4 MB" (in the given language). */
51
+ export function formatSize(bytes: number, locale = 'es'): string {
52
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
53
+ let value = Math.max(0, bytes);
54
+ let unit = 0;
55
+ while (value >= 1024 && unit < units.length - 1) {
56
+ value /= 1024;
57
+ unit++;
58
+ }
59
+ let format: Intl.NumberFormat;
60
+ try {
61
+ format = new Intl.NumberFormat(locale, { maximumFractionDigits: unit && value < 10 ? 1 : 0 });
62
+ } catch {
63
+ format = new Intl.NumberFormat(undefined, { maximumFractionDigits: unit && value < 10 ? 1 : 0 });
64
+ }
65
+ return `${format.format(value)} ${units[unit]}`;
66
+ }
67
+
68
+ const same = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0;
69
+
70
+ /** Does a sibling (other than `except`) already have this name? Case does not count ("foto" is "Foto"); accents do. */
71
+ export const nameTaken = (name: string, siblings: FileEntry[], except?: string) => siblings.some((sibling) => sibling.id !== except && same(sibling.name, name));
72
+
73
+ /** `name`, or "name (2)", "name (3)"… (before the extension for files) when a sibling has it; `except` is the entry being renamed. */
74
+ export function uniqueName(name: string, siblings: FileEntry[], except?: string): string {
75
+ if (!nameTaken(name, siblings, except)) return name;
76
+ const extension = extensionOf(name);
77
+ const base = extension ? name.slice(0, -extension.length - 1) : name;
78
+ for (let n = 2; ; n++) {
79
+ const candidate = extension ? `${base} (${n}).${extension}` : `${base} (${n})`;
80
+ if (!nameTaken(candidate, siblings, except)) return candidate;
81
+ }
82
+ }
83
+
84
+ /** Folders first; then by `key` (names in natural order: "Foto 2" before "Foto 10"). */
85
+ export function sortEntries(entries: FileEntry[], key: FileSort = 'name', direction: 1 | -1 = 1, locale = 'es'): FileEntry[] {
86
+ let collator: Intl.Collator;
87
+ try {
88
+ collator = new Intl.Collator(locale, { numeric: true, sensitivity: 'base' });
89
+ } catch {
90
+ collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
91
+ }
92
+ const time = (entry: FileEntry) => toTime(entry.modified)?.getTime() ?? 0;
93
+ return [...entries].sort((a, b) => {
94
+ const folderA = isFolder(a);
95
+ if (folderA !== isFolder(b)) return folderA ? -1 : 1;
96
+ let diff = 0;
97
+ // Folders have no size of their own: by name, as file managers do.
98
+ if (key === 'size' && !folderA) diff = (a.size ?? 0) - (b.size ?? 0);
99
+ else if (key === 'modified') diff = time(a) - time(b);
100
+ if (!diff) diff = collator.compare(a.name, b.name);
101
+ return diff * direction;
102
+ });
103
+ }
104
+
105
+ /** A deep copy of entries (dates and other values are kept as they are). */
106
+ export const cloneEntries = (entries: FileEntry[]): FileEntry[] =>
107
+ entries.map((entry) => (entry.children ? { ...entry, children: cloneEntries(entry.children) } : { ...entry }));