@justanarthur/modo-ui 0.5.0 → 0.6.0-canary.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 (461) hide show
  1. package/DESIGN.md +608 -0
  2. package/README.md +61 -39
  3. package/blocks/sidebar-app/examples.mdx +33 -0
  4. package/blocks/sidebar-app/index.tsx +218 -0
  5. package/blocks/sidebar-app/inset-topbar.tsx +25 -0
  6. package/blocks/sidebar-app/menu-grid.ts +26 -0
  7. package/blocks/sidebar-app/nav-data.ts +36 -0
  8. package/blocks/sidebar-app/search-field.tsx +48 -0
  9. package/blocks/sidebar-app/user-footer.tsx +67 -0
  10. package/blocks/sidebar-app/workspace-header.tsx +129 -0
  11. package/components/AI/input-message/examples.mdx +124 -0
  12. package/components/AI/input-message/file-thumbnail.tsx +173 -0
  13. package/components/AI/input-message/index.tsx +1383 -0
  14. package/components/AI/thinking-indicator/examples.mdx +27 -0
  15. package/components/AI/thinking-indicator/index.tsx +210 -0
  16. package/components/accordion/examples.mdx +148 -0
  17. package/components/accordion/index.tsx +1034 -0
  18. package/components/avatar/examples.mdx +95 -0
  19. package/components/avatar/index.tsx +129 -0
  20. package/components/badge/change-demo.tsx +29 -0
  21. package/components/badge/examples.mdx +120 -0
  22. package/components/badge/index.tsx +225 -0
  23. package/components/badge/variants.ts +73 -0
  24. package/components/button/examples.mdx +164 -0
  25. package/components/button/index.tsx +416 -0
  26. package/components/button/loading-demo.tsx +25 -0
  27. package/components/button/variants.ts +166 -0
  28. package/components/card/examples.mdx +212 -0
  29. package/components/card/index.tsx +1037 -0
  30. package/components/copy/examples.mdx +87 -0
  31. package/components/copy/index.tsx +482 -0
  32. package/components/inputs/checkbox/examples.mdx +83 -0
  33. package/components/inputs/checkbox/index.tsx +611 -0
  34. package/components/inputs/checkbox/select-all-demo.tsx +48 -0
  35. package/components/inputs/color-picker/examples.mdx +72 -0
  36. package/components/inputs/color-picker/index.tsx +1955 -0
  37. package/components/inputs/color-picker/variants.ts +401 -0
  38. package/components/inputs/combobox/examples.mdx +273 -0
  39. package/components/inputs/combobox/index.tsx +1436 -0
  40. package/components/inputs/combobox/remote-demo.tsx +101 -0
  41. package/components/inputs/date-time/calendar/examples.mdx +34 -0
  42. package/components/inputs/date-time/calendar/index.tsx +351 -0
  43. package/components/inputs/date-time/calendar/use-month-window.ts +163 -0
  44. package/components/inputs/date-time/date-picker/examples.mdx +166 -0
  45. package/components/inputs/date-time/date-picker/index.tsx +385 -0
  46. package/components/inputs/date-time/date-picker/masked-date-input.tsx +336 -0
  47. package/components/inputs/date-time/date-range-picker/examples.mdx +29 -0
  48. package/components/inputs/date-time/date-range-picker/index.tsx +217 -0
  49. package/components/inputs/date-time/duration-picker/examples.mdx +16 -0
  50. package/components/inputs/date-time/duration-picker/index.tsx +189 -0
  51. package/components/inputs/date-time/month-picker/examples.mdx +16 -0
  52. package/components/inputs/date-time/month-picker/index.tsx +190 -0
  53. package/components/inputs/date-time/picker-shell.tsx +395 -0
  54. package/components/inputs/date-time/time-columns.tsx +91 -0
  55. package/components/inputs/date-time/time-picker/examples.mdx +26 -0
  56. package/components/inputs/date-time/time-picker/index.tsx +166 -0
  57. package/components/inputs/field/examples.mdx +113 -0
  58. package/components/inputs/field/index.tsx +267 -0
  59. package/components/inputs/file-dropzone/examples.mdx +73 -0
  60. package/components/inputs/file-dropzone/file-row.tsx +201 -0
  61. package/components/inputs/file-dropzone/index.tsx +296 -0
  62. package/components/inputs/file-dropzone/upload-demo.tsx +67 -0
  63. package/components/inputs/file-dropzone/variants.ts +94 -0
  64. package/components/inputs/input/examples.mdx +135 -0
  65. package/components/inputs/input/index.tsx +494 -0
  66. package/components/inputs/input/password-demo.tsx +30 -0
  67. package/components/inputs/label/examples.mdx +67 -0
  68. package/components/inputs/label/index.tsx +85 -0
  69. package/components/inputs/number-input/examples.mdx +56 -0
  70. package/components/inputs/number-input/index.tsx +214 -0
  71. package/components/inputs/radio/examples.mdx +77 -0
  72. package/components/inputs/radio/index.tsx +636 -0
  73. package/components/inputs/search/examples.mdx +74 -0
  74. package/components/inputs/search/index.tsx +333 -0
  75. package/components/inputs/select/examples.mdx +218 -0
  76. package/components/inputs/select/index.tsx +862 -0
  77. package/components/inputs/select/variants.ts +32 -0
  78. package/components/inputs/slider/examples.mdx +127 -0
  79. package/components/inputs/slider/index.tsx +1725 -0
  80. package/components/inputs/switch/examples.mdx +94 -0
  81. package/components/inputs/switch/index.tsx +504 -0
  82. package/components/inputs/textarea/examples.mdx +88 -0
  83. package/components/inputs/textarea/index.tsx +161 -0
  84. package/components/navigation/navigation-menu/examples.mdx +109 -0
  85. package/components/navigation/navigation-menu/index.tsx +613 -0
  86. package/components/navigation/pagination/examples.mdx +64 -0
  87. package/components/navigation/pagination/index.tsx +395 -0
  88. package/components/navigation/sidebar/examples.mdx +319 -0
  89. package/components/navigation/sidebar/index.tsx +453 -0
  90. package/components/navigation/sidebar/sidebar-core.tsx +1408 -0
  91. package/components/navigation/sidebar/sidebar-menu.tsx +1495 -0
  92. package/components/navigation/sidebar/sidebar.css +74 -0
  93. package/components/navigation/sidebar/variants.ts +55 -0
  94. package/components/navigation/tabs/examples.mdx +89 -0
  95. package/components/navigation/tabs/index.tsx +494 -0
  96. package/components/navigation/tabs-subtle/examples.mdx +49 -0
  97. package/components/navigation/tabs-subtle/index.tsx +512 -0
  98. package/components/overlays/alert-dialog/examples.mdx +123 -0
  99. package/components/overlays/alert-dialog/index.tsx +258 -0
  100. package/components/overlays/alert-dialog/pending-demo.tsx +36 -0
  101. package/components/overlays/command/examples.mdx +113 -0
  102. package/components/overlays/command/index.tsx +416 -0
  103. package/components/overlays/context-menu/examples.mdx +56 -0
  104. package/components/overlays/context-menu/index.tsx +279 -0
  105. package/components/overlays/dialog/examples.mdx +144 -0
  106. package/components/overlays/dialog/index.tsx +366 -0
  107. package/components/overlays/dropdown/dropdown-search.tsx +395 -0
  108. package/components/overlays/dropdown/examples.mdx +294 -0
  109. package/components/overlays/dropdown/index.tsx +1168 -0
  110. package/components/overlays/dropdown/menu-item.tsx +464 -0
  111. package/components/overlays/dropdown/menu-parts.tsx +660 -0
  112. package/components/overlays/popover/anchored-demo.tsx +59 -0
  113. package/components/overlays/popover/examples.mdx +180 -0
  114. package/components/overlays/popover/index.tsx +315 -0
  115. package/components/overlays/preview-card/examples.mdx +61 -0
  116. package/components/overlays/preview-card/index.tsx +204 -0
  117. package/components/overlays/sheet/examples.mdx +89 -0
  118. package/components/overlays/sheet/index.tsx +247 -0
  119. package/components/overlays/toast/examples.mdx +68 -0
  120. package/components/overlays/toast/global-demo.tsx +72 -0
  121. package/components/overlays/toast/index.tsx +474 -0
  122. package/components/overlays/toast/manager.ts +193 -0
  123. package/components/overlays/tooltip/examples.mdx +106 -0
  124. package/components/overlays/tooltip/index.tsx +519 -0
  125. package/components/progress/examples.mdx +59 -0
  126. package/components/progress/index.tsx +174 -0
  127. package/components/progress/progress.css +49 -0
  128. package/components/progress/step-demo.tsx +27 -0
  129. package/components/table/examples.mdx +184 -0
  130. package/components/table/index.tsx +510 -0
  131. package/components/table/sort-demo.tsx +61 -0
  132. package/dist/base.css +100 -0
  133. package/dist/blocks/sidebar-app/index.js +39 -26
  134. package/dist/components/AI/input-message/index.js +22 -16
  135. package/dist/components/AI/thinking-indicator/index.js +9 -6
  136. package/dist/components/accordion/index.js +16 -13
  137. package/dist/components/avatar/index.d.ts +46 -0
  138. package/dist/components/avatar/index.js +18 -0
  139. package/dist/components/badge/index.d.ts +18 -11
  140. package/dist/components/badge/index.js +11 -7
  141. package/dist/components/badge/variants.d.ts +33 -0
  142. package/dist/components/badge/variants.js +10 -0
  143. package/dist/components/button/index.d.ts +53 -17
  144. package/dist/components/button/index.js +10 -6
  145. package/dist/components/button/variants.d.ts +39 -0
  146. package/dist/components/button/variants.js +8 -0
  147. package/dist/components/card/index.d.ts +16 -3
  148. package/dist/components/card/index.js +16 -11
  149. package/dist/components/copy/index.js +13 -9
  150. package/dist/components/inputs/checkbox/index.d.ts +28 -9
  151. package/dist/components/inputs/checkbox/index.js +17 -13
  152. package/dist/components/inputs/color-picker/index.d.ts +13 -13
  153. package/dist/components/inputs/color-picker/index.js +25 -18
  154. package/dist/components/inputs/color-picker/variants.d.ts +61 -0
  155. package/dist/components/inputs/color-picker/variants.js +28 -0
  156. package/dist/components/inputs/combobox/index.d.ts +60 -17
  157. package/dist/components/inputs/combobox/index.js +26 -18
  158. package/dist/components/inputs/date-time/calendar/index.js +14 -11
  159. package/dist/components/inputs/date-time/date-picker/index.d.ts +53 -11
  160. package/dist/components/inputs/date-time/date-picker/index.js +33 -25
  161. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +3 -3
  162. package/dist/components/inputs/date-time/date-range-picker/index.js +32 -24
  163. package/dist/components/inputs/date-time/duration-picker/index.d.ts +3 -3
  164. package/dist/components/inputs/date-time/duration-picker/index.js +32 -24
  165. package/dist/components/inputs/date-time/month-picker/index.d.ts +3 -3
  166. package/dist/components/inputs/date-time/month-picker/index.js +31 -23
  167. package/dist/components/inputs/date-time/time-picker/index.d.ts +3 -3
  168. package/dist/components/inputs/date-time/time-picker/index.js +32 -24
  169. package/dist/components/inputs/field/index.d.ts +116 -0
  170. package/dist/components/inputs/field/index.js +28 -0
  171. package/dist/components/inputs/file-dropzone/index.d.ts +165 -0
  172. package/dist/components/inputs/file-dropzone/index.js +32 -0
  173. package/dist/components/inputs/file-dropzone/variants.d.ts +35 -0
  174. package/dist/components/inputs/file-dropzone/variants.js +12 -0
  175. package/dist/components/inputs/input/index.d.ts +95 -13
  176. package/dist/components/inputs/input/index.js +17 -9
  177. package/dist/components/inputs/label/index.d.ts +32 -0
  178. package/dist/components/inputs/label/index.js +11 -0
  179. package/dist/components/inputs/number-input/index.d.ts +84 -0
  180. package/dist/components/inputs/number-input/index.js +20 -0
  181. package/dist/components/inputs/radio/index.d.ts +20 -5
  182. package/dist/components/inputs/radio/index.js +15 -12
  183. package/dist/components/inputs/search/index.js +25 -18
  184. package/dist/components/inputs/select/index.d.ts +41 -11
  185. package/dist/components/inputs/select/index.js +27 -14
  186. package/dist/components/inputs/select/variants.d.ts +5 -0
  187. package/dist/components/inputs/select/variants.js +6 -0
  188. package/dist/components/inputs/slider/index.js +7 -5
  189. package/dist/components/inputs/switch/index.d.ts +8 -2
  190. package/dist/components/inputs/switch/index.js +14 -12
  191. package/dist/components/inputs/textarea/index.d.ts +4 -0
  192. package/dist/components/inputs/textarea/index.js +8 -6
  193. package/dist/components/navigation/navigation-menu/index.d.ts +114 -0
  194. package/dist/components/navigation/navigation-menu/index.js +40 -0
  195. package/dist/components/navigation/pagination/index.d.ts +73 -0
  196. package/dist/components/navigation/pagination/index.js +39 -0
  197. package/dist/components/navigation/sidebar/index.d.ts +44 -21
  198. package/dist/components/navigation/sidebar/index.js +41 -31
  199. package/dist/components/navigation/sidebar/variants.d.ts +25 -0
  200. package/dist/components/navigation/sidebar/variants.js +28 -0
  201. package/dist/components/navigation/tabs/index.d.ts +8 -2
  202. package/dist/components/navigation/tabs/index.js +14 -10
  203. package/dist/components/navigation/tabs-subtle/index.js +12 -10
  204. package/dist/components/overlays/alert-dialog/index.d.ts +121 -15
  205. package/dist/components/overlays/alert-dialog/index.js +22 -13
  206. package/dist/components/overlays/command/index.js +24 -17
  207. package/dist/components/overlays/context-menu/index.d.ts +33 -17
  208. package/dist/components/overlays/context-menu/index.js +27 -18
  209. package/dist/components/overlays/dialog/index.js +17 -12
  210. package/dist/components/overlays/dropdown/index.d.ts +257 -31
  211. package/dist/components/overlays/dropdown/index.js +42 -17
  212. package/dist/components/overlays/popover/index.d.ts +31 -9
  213. package/dist/components/overlays/popover/index.js +12 -9
  214. package/dist/components/overlays/preview-card/index.js +11 -8
  215. package/dist/components/overlays/sheet/index.js +18 -13
  216. package/dist/components/overlays/toast/index.d.ts +123 -47
  217. package/dist/components/overlays/toast/index.js +18 -10
  218. package/dist/components/overlays/tooltip/index.d.ts +113 -21
  219. package/dist/components/overlays/tooltip/index.js +15 -6
  220. package/dist/components/progress/index.d.ts +56 -0
  221. package/dist/components/progress/index.js +15 -0
  222. package/dist/components/table/index.d.ts +63 -8
  223. package/dist/components/table/index.js +19 -9
  224. package/dist/index.d.ts +2201 -736
  225. package/dist/index.js +223 -82
  226. package/dist/lib/date-locale.d.ts +14 -0
  227. package/dist/lib/date-locale.js +16 -0
  228. package/dist/lib/field-classes.d.ts +144 -0
  229. package/dist/lib/field-classes.js +40 -0
  230. package/dist/lib/field-variants.d.ts +47 -0
  231. package/dist/lib/field-variants.js +26 -0
  232. package/dist/lib/fluid-hover-highlight.d.ts +112 -0
  233. package/dist/lib/fluid-hover-highlight.js +17 -0
  234. package/dist/lib/focus-ring.d.ts +18 -0
  235. package/dist/lib/focus-ring.js +10 -0
  236. package/dist/lib/goo-indicator.d.ts +46 -0
  237. package/dist/lib/goo-indicator.js +14 -0
  238. package/dist/lib/icon-context.d.ts +30 -0
  239. package/dist/lib/icon-context.js +12 -0
  240. package/dist/lib/inert.d.ts +9 -0
  241. package/dist/lib/inert.js +8 -0
  242. package/dist/lib/morph-layers.d.ts +93 -0
  243. package/dist/lib/morph-layers.js +13 -0
  244. package/dist/lib/morph-part.d.ts +21 -0
  245. package/dist/lib/morph-part.js +12 -0
  246. package/dist/lib/popup.d.ts +20 -0
  247. package/dist/lib/popup.js +12 -0
  248. package/dist/lib/reduced-motion.d.ts +9 -0
  249. package/dist/lib/reduced-motion.js +6 -0
  250. package/dist/lib/row-index.d.ts +10 -0
  251. package/dist/lib/row-index.js +8 -0
  252. package/dist/lib/row-order.d.ts +28 -0
  253. package/dist/lib/row-order.js +10 -0
  254. package/dist/lib/scroll-column.d.ts +27 -0
  255. package/dist/lib/scroll-column.js +18 -0
  256. package/dist/lib/shape-context.d.ts +30 -0
  257. package/dist/lib/shape-context.js +12 -0
  258. package/dist/lib/size-context.d.ts +94 -0
  259. package/dist/lib/size-context.js +20 -0
  260. package/dist/lib/size-scale.d.ts +79 -0
  261. package/dist/lib/size-scale.js +8 -0
  262. package/dist/lib/slot.d.ts +12 -0
  263. package/dist/lib/slot.js +6 -0
  264. package/dist/lib/springs.d.ts +44 -0
  265. package/dist/lib/springs.js +8 -0
  266. package/dist/lib/surface-classes.d.ts +19 -0
  267. package/dist/lib/surface-classes.js +10 -0
  268. package/dist/lib/surface-context.d.ts +9 -0
  269. package/dist/lib/surface-context.js +8 -0
  270. package/dist/lib/use-controllable-state.d.ts +2 -0
  271. package/dist/lib/use-controllable-state.js +6 -0
  272. package/dist/lib/use-fluid-hover.d.ts +138 -0
  273. package/dist/lib/use-fluid-hover.js +14 -0
  274. package/dist/lib/use-is-mobile.d.ts +2 -0
  275. package/dist/lib/use-is-mobile.js +6 -0
  276. package/dist/lib/use-keyboard-nav-gate.d.ts +20 -0
  277. package/dist/lib/use-keyboard-nav-gate.js +7 -0
  278. package/dist/lib/use-merge-split.d.ts +47 -0
  279. package/dist/lib/use-merge-split.js +15 -0
  280. package/dist/lib/use-morph.d.ts +101 -0
  281. package/dist/lib/use-morph.js +20 -0
  282. package/dist/lib/use-touch-primary.d.ts +2 -0
  283. package/dist/lib/use-touch-primary.js +6 -0
  284. package/dist/lib/utils.d.ts +3 -0
  285. package/dist/lib/utils.js +6 -0
  286. package/dist/preset.d.ts +17 -0
  287. package/dist/preset.js +22 -2
  288. package/dist/primitives/code/index.js +7 -5
  289. package/dist/primitives/craft/index.js +9 -6
  290. package/dist/primitives/fluid-hover/index.js +9 -8
  291. package/dist/primitives/morph/index.js +15 -10
  292. package/dist/primitives/motion/index.js +10 -8
  293. package/dist/primitives/scroll-area/index.js +4 -3
  294. package/dist/primitives/separator/index.d.ts +25 -0
  295. package/dist/primitives/separator/index.js +9 -0
  296. package/dist/primitives/sizes/index.js +6 -4
  297. package/dist/primitives/skeleton/index.d.ts +35 -0
  298. package/dist/primitives/skeleton/index.js +12 -0
  299. package/dist/primitives/surface/index.js +6 -4
  300. package/dist/shared/{components-3mkepka5.js → components-0jmqhx2k.js} +28 -25
  301. package/dist/shared/components-124epb7f.js +53 -0
  302. package/dist/shared/{components-gz8zf3ec.js → components-1k0wv53d.js} +15 -43
  303. package/dist/shared/components-26snnbs9.js +322 -0
  304. package/dist/shared/{components-2dxkv6zm.js → components-2c0qcfx4.js} +8 -7
  305. package/dist/shared/{components-473g814w.js → components-2mt9te6d.js} +243 -137
  306. package/dist/shared/{components-hawvy2qz.js → components-2z48g4fp.js} +9 -30
  307. package/dist/shared/components-34txk9rj.js +35 -0
  308. package/dist/shared/components-3h92d94m.js +23 -0
  309. package/dist/shared/{components-zfnmn5d4.js → components-3sp623ae.js} +36 -31
  310. package/dist/shared/{components-jwst3vtj.js → components-3wxkjgqn.js} +186 -76
  311. package/dist/shared/{components-s2bz1xpk.js → components-49bsynbw.js} +8 -5
  312. package/dist/shared/components-4bwsd46w.js +71 -0
  313. package/dist/shared/components-4n4f5m3e.js +297 -0
  314. package/dist/shared/{components-0q0sqfh4.js → components-4y816hwp.js} +27 -49
  315. package/dist/shared/components-52ngsgca.js +399 -0
  316. package/dist/shared/{components-y9y0g8er.js → components-52r5763g.js} +22 -19
  317. package/dist/shared/components-559tf7yb.js +28 -0
  318. package/dist/shared/components-5mkga9hj.js +440 -0
  319. package/dist/shared/{components-kwa6mrf9.js → components-5s0nfqvw.js} +14 -9
  320. package/dist/shared/{components-dk0gjr5a.js → components-60atskhp.js} +19 -18
  321. package/dist/shared/{components-rr7a6ja9.js → components-658b26nb.js} +21 -18
  322. package/dist/shared/{components-xxvdw7k1.js → components-65vwfjs6.js} +37 -34
  323. package/dist/shared/{components-frw34b65.js → components-6j2fh74k.js} +114 -123
  324. package/dist/shared/components-6zg1yft7.js +167 -0
  325. package/dist/shared/components-6zng858q.js +53 -0
  326. package/dist/shared/components-72w4ge68.js +64 -0
  327. package/dist/shared/{components-cv16mryv.js → components-78x40gee.js} +13 -7
  328. package/dist/shared/{components-b2wyq2cq.js → components-7d4r7hh1.js} +39 -57
  329. package/dist/shared/components-7ntvvegr.js +316 -0
  330. package/dist/shared/components-8nwsqjn8.js +34 -0
  331. package/dist/shared/{components-5xc2j8jz.js → components-8tt3xbb5.js} +19 -18
  332. package/dist/shared/{components-vc7vq5nz.js → components-9as3rf68.js} +6 -3
  333. package/dist/shared/components-9mys9jna.js +255 -0
  334. package/dist/shared/components-adkarj8n.js +35 -0
  335. package/dist/shared/{components-b3c5et5b.js → components-ae9fncz4.js} +32 -31
  336. package/dist/shared/{components-hqeq3w9q.js → components-awra5sff.js} +24 -16
  337. package/dist/shared/components-b1e239g3.js +28 -0
  338. package/dist/shared/{components-q5xj5c16.js → components-b1f2bjf1.js} +9 -8
  339. package/dist/shared/{components-qmmjx1qw.js → components-bn42vq6d.js} +71 -72
  340. package/dist/shared/components-c96g82th.js +1444 -0
  341. package/dist/shared/components-c9yr4ed1.js +44 -0
  342. package/dist/shared/components-d3shbwqq.js +95 -0
  343. package/dist/shared/{components-jjw69c4c.js → components-dcc02hs2.js} +7 -4
  344. package/dist/shared/{components-men63rwj.js → components-de99ykt7.js} +3 -2
  345. package/dist/shared/components-dhgk6mbt.js +16 -0
  346. package/dist/shared/components-drxrtnp2.js +25 -0
  347. package/dist/shared/{components-6p8tfcd6.js → components-ds9fqat9.js} +21 -18
  348. package/dist/shared/{components-mjthm1kf.js → components-egy4tyjt.js} +11 -8
  349. package/dist/shared/components-f0xfpkhh.js +82 -0
  350. package/dist/shared/components-f7h57m6t.js +59 -0
  351. package/dist/shared/{components-d6xtjht3.js → components-fjkpbg6n.js} +33 -30
  352. package/dist/shared/{components-gc5vtq2f.js → components-gh0qeey3.js} +63 -29
  353. package/dist/shared/{components-39jq9va1.js → components-gvbapfen.js} +29 -24
  354. package/dist/shared/{components-5vgpnxgy.js → components-gzesg7hy.js} +19 -16
  355. package/dist/shared/components-h6p86be3.js +19 -0
  356. package/dist/shared/components-h83az0bz.js +33 -0
  357. package/dist/shared/components-jjrj6ymn.js +104 -0
  358. package/dist/shared/{components-wvp1m52g.js → components-jp5v0m0f.js} +15 -14
  359. package/dist/shared/{components-zrkzcayz.js → components-jqpefjsn.js} +54 -6
  360. package/dist/shared/{components-1z6tdadk.js → components-k74j4hwh.js} +20 -17
  361. package/dist/shared/components-kswxnm3h.js +24 -0
  362. package/dist/shared/{components-jbbmdr74.js → components-mkkbbgb7.js} +36 -17
  363. package/dist/shared/{components-pbmqctfa.js → components-mpw667rb.js} +169 -65
  364. package/dist/shared/{components-s5ngg32c.js → components-paa08427.js} +121 -47
  365. package/dist/shared/{components-vfbhxf5r.js → components-pd42emw3.js} +6 -5
  366. package/dist/shared/{components-hw9yk32z.js → components-pmfszv7a.js} +35 -23
  367. package/dist/shared/{components-chacnk61.js → components-pw06rbxq.js} +19 -19
  368. package/dist/shared/components-q03cktx8.js +246 -0
  369. package/dist/shared/{components-kvr7f9hy.js → components-qdm3yh26.js} +19 -10
  370. package/dist/shared/{components-27nnvrwe.js → components-qs4rkqsj.js} +15 -14
  371. package/dist/shared/{components-s083f38z.js → components-r6gbmkew.js} +22 -21
  372. package/dist/shared/{components-ta7shw28.js → components-r8eqs7gz.js} +4 -4
  373. package/dist/shared/{components-sn1rxx1d.js → components-rm02h9nt.js} +12 -11
  374. package/dist/shared/{components-c6s6b43g.js → components-rrbpmfx4.js} +3 -2
  375. package/dist/shared/{components-n7mjr357.js → components-tz1fv35x.js} +3 -2
  376. package/dist/shared/components-tz4gaf1v.js +23 -0
  377. package/dist/shared/{components-zx6b6yby.js → components-vf45j12c.js} +4 -18
  378. package/dist/shared/{components-3ky44908.js → components-vfjc9jzz.js} +2 -3
  379. package/dist/shared/{components-39vmfq9s.js → components-vsa625vk.js} +21 -20
  380. package/dist/shared/components-wawnmkph.js +500 -0
  381. package/dist/shared/components-wktrrb4m.js +38 -0
  382. package/dist/shared/{components-8snj490h.js → components-wwd363me.js} +78 -32
  383. package/dist/shared/{components-5pnw8183.js → components-xcwvgcrd.js} +23 -22
  384. package/dist/shared/components-xdqmt6ed.js +20 -0
  385. package/dist/shared/{components-h2ajmhwn.js → components-y1w3a6wx.js} +107 -35
  386. package/dist/shared/{components-ar66gnt0.js → components-y6578b8h.js} +37 -34
  387. package/dist/shared/{components-59efxjd9.js → components-yhgq0qxx.js} +13 -12
  388. package/dist/shared/{components-jthe1pwb.js → components-yz84gpkq.js} +101 -404
  389. package/dist/shared/components-z3v0n0de.js +11 -0
  390. package/dist/shared/{components-7r3etddy.js → components-z9n19fyk.js} +19 -54
  391. package/dist/shared/components-zbvjzh1p.js +141 -0
  392. package/dist/styles.css +241 -16
  393. package/dist/tailwind.css +13 -0
  394. package/dist/tokens.css +34 -0
  395. package/index.ts +60 -0
  396. package/lib/date-locale.ts +50 -0
  397. package/lib/field-classes.ts +115 -0
  398. package/lib/field-variants.ts +117 -0
  399. package/lib/fluid-hover-highlight.tsx +139 -0
  400. package/lib/focus-ring.tsx +46 -0
  401. package/lib/goo-indicator.tsx +261 -0
  402. package/lib/icon-context.tsx +310 -0
  403. package/lib/inert.ts +22 -0
  404. package/lib/morph-layers.tsx +102 -0
  405. package/lib/morph-part.tsx +49 -0
  406. package/lib/popup.ts +52 -0
  407. package/lib/reduced-motion.ts +19 -0
  408. package/lib/row-index.tsx +29 -0
  409. package/lib/row-order.tsx +115 -0
  410. package/lib/scroll-column.tsx +244 -0
  411. package/lib/shape-context.tsx +124 -0
  412. package/lib/size-context.tsx +66 -0
  413. package/lib/size-scale.ts +115 -0
  414. package/lib/slot.ts +25 -0
  415. package/lib/springs.ts +62 -0
  416. package/lib/surface-classes.ts +58 -0
  417. package/lib/surface-context.tsx +21 -0
  418. package/lib/use-controllable-state.ts +44 -0
  419. package/lib/use-fluid-hover.ts +541 -0
  420. package/lib/use-is-mobile.ts +25 -0
  421. package/lib/use-keyboard-nav-gate.ts +45 -0
  422. package/lib/use-merge-split.tsx +209 -0
  423. package/lib/use-morph.ts +557 -0
  424. package/lib/use-touch-primary.tsx +43 -0
  425. package/lib/utils.ts +37 -0
  426. package/package.json +177 -4
  427. package/preset.ts +196 -0
  428. package/primitives/code/examples.mdx +27 -0
  429. package/primitives/code/index.tsx +80 -0
  430. package/primitives/craft/demos.tsx +173 -0
  431. package/primitives/craft/examples.mdx +138 -0
  432. package/primitives/craft/index.tsx +76 -0
  433. package/primitives/fluid-hover/examples.mdx +84 -0
  434. package/primitives/fluid-hover/index.tsx +316 -0
  435. package/primitives/morph/examples.mdx +135 -0
  436. package/primitives/morph/index.tsx +192 -0
  437. package/primitives/motion/examples.mdx +100 -0
  438. package/primitives/motion/index.tsx +160 -0
  439. package/primitives/scroll-area/examples.mdx +58 -0
  440. package/primitives/scroll-area/index.tsx +200 -0
  441. package/primitives/separator/examples.mdx +30 -0
  442. package/primitives/separator/index.tsx +53 -0
  443. package/primitives/sizes/examples.mdx +167 -0
  444. package/primitives/sizes/index.tsx +89 -0
  445. package/primitives/skeleton/examples.mdx +51 -0
  446. package/primitives/skeleton/index.tsx +72 -0
  447. package/primitives/skeleton/skeleton.css +15 -0
  448. package/primitives/surface/examples.mdx +173 -0
  449. package/primitives/surface/index.tsx +109 -0
  450. package/primitives/surface/surface.css +176 -0
  451. package/dist/shared/components-0sn7ypjw.js +0 -61
  452. package/dist/shared/components-1434p1mh.js +0 -123
  453. package/dist/shared/components-a86dytt0.js +0 -986
  454. package/dist/shared/components-gf5s6ap9.js +0 -42
  455. package/dist/shared/components-hyq23phv.js +0 -310
  456. package/dist/shared/components-ng72k97e.js +0 -128
  457. package/dist/shared/components-raknhppb.js +0 -64
  458. package/dist/shared/components-vynjtrk6.js +0 -19
  459. package/dist/shared/components-wy9jpqny.js +0 -146
  460. /package/dist/shared/{components-1v7rxytx.js → components-3ey0hzpb.js} +0 -0
  461. /package/dist/shared/{components-ezxhrmer.js → components-jcnjay5g.js} +0 -0
@@ -0,0 +1,395 @@
1
+ /*
2
+ * Local addition (not part of Fluid Functionalism): the field and panel every
3
+ * date and time picker shares. Not an item (a file in a group folder): the
4
+ * pickers import it.
5
+ *
6
+ * The field is a text field on the shared field box (`useFieldBox` in
7
+ * `lib/field-classes.ts`, `bordered` unless the picker's `variant` says
8
+ * otherwise) showing the formatted value. Pressed, it opens its panel through Base UI's
9
+ * Popover and the morph layer, with Combobox's options: goo out of the field
10
+ * by default, or in place over it (`effect="morph"` with `hideSource`), the
11
+ * panel's header landing where the field was. On a phone-width screen the
12
+ * panel is the Sheet item instead, from the bottom edge.
13
+ *
14
+ * With an `editor` (DatePicker's `editable`) the field is a box around that
15
+ * text control instead of a button, and only its leading icon, an
16
+ * `InputAction`, opens the panel; the panel still anchors to and grows out
17
+ * of the whole field. `readOnly` shows the value and never opens.
18
+ */
19
+
20
+ import { Popover } from '@base-ui/react/popover'
21
+ import {
22
+ type ButtonHTMLAttributes,
23
+ forwardRef,
24
+ type ReactElement,
25
+ type ReactNode,
26
+ type RefObject,
27
+ useCallback,
28
+ useRef,
29
+ useState,
30
+ } from 'react'
31
+ import { type FieldVariant, fieldIconClasses, useFieldBox } from '../../../lib/field-classes'
32
+ import { type IconName, useIcon } from '../../../lib/icon-context'
33
+ import { MorphSurface } from '../../../lib/morph-layers'
34
+ import { useShape } from '../../../lib/shape-context'
35
+ import { type SizeVariant, useSize } from '../../../lib/size-context'
36
+ import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
37
+ import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
38
+ import { useControllableState } from '../../../lib/use-controllable-state'
39
+ import { useIsMobile } from '../../../lib/use-is-mobile'
40
+ import {
41
+ inPlaceOffset,
42
+ type MorphEffect,
43
+ type MorphFrom,
44
+ type MorphTier,
45
+ opensInPlace,
46
+ useMorph,
47
+ useMorphOrigin,
48
+ } from '../../../lib/use-morph'
49
+ import { cn } from '../../../lib/utils'
50
+ import { SizeProvider } from '../../../primitives/sizes'
51
+ import Button from '../../button'
52
+ import Sheet from '../../overlays/sheet'
53
+ import Input, { InputAction, InputAdornment, type InputProps } from '../input'
54
+
55
+ interface PickerShellProps {
56
+ open?: boolean
57
+ defaultOpen?: boolean
58
+ onOpenChange?: (open: boolean) => void
59
+ size?: SizeVariant
60
+ variant?: FieldVariant
61
+ invalid?: boolean
62
+ disabled?: boolean
63
+ /** The field's leading icon. */
64
+ icon: IconName
65
+ /** The formatted value; empty shows the placeholder. */
66
+ display: ReactNode
67
+ placeholder?: string
68
+ /** The panel header's inputs; the shell adds the close button after them. */
69
+ header: ReactNode
70
+ /** What the panel focuses when it opens on a desktop (its header input). */
71
+ initialFocus?: RefObject<HTMLElement | null>
72
+ /** Accessible name of the close button. */
73
+ closeLabel?: string
74
+ id?: string
75
+ dir?: string
76
+ /** Classes for the field. */
77
+ className?: string
78
+ /** Classes for the panel on a desktop: its width. */
79
+ popupClassName?: string
80
+ /** The desktop panel's morph (see Morph); a phone's sheet grows from its edge. */
81
+ from?: MorphFrom
82
+ effect?: MorphEffect
83
+ hideSource?: boolean
84
+ tier?: MorphTier
85
+ /** The field's own text control (an editable picker): the field becomes a box around it, and its icon a button that opens the panel. */
86
+ editor?: ReactNode
87
+ /** Accessible name of that icon button. Defaults to `"Open calendar"`. */
88
+ triggerLabel?: string
89
+ /** Shows the value and never opens. */
90
+ readOnly?: boolean
91
+ /** The panel body under the header. */
92
+ children: ReactNode
93
+ }
94
+
95
+ /** The field and its panel: a popover out of the field (or in place of it), or a bottom sheet on a phone. */
96
+ function PickerShell({ open: openProp, defaultOpen = false, onOpenChange, size, ...props }: PickerShellProps) {
97
+ const [open, setOpenState] = useControllableState(openProp, defaultOpen, onOpenChange)
98
+ const setOpen = (next: boolean) => {
99
+ if (next && props.readOnly) return
100
+ setOpenState(next)
101
+ }
102
+ const mobile = useIsMobile(640)
103
+ const shell = mobile ? (
104
+ <PickerSheet open={open} setOpen={setOpen} {...props} />
105
+ ) : (
106
+ <PickerPopover open={open} setOpen={setOpen} {...props} />
107
+ )
108
+ // A size pins the field and the portalled panel (context crosses portals).
109
+ return size ? <SizeProvider size={size}>{shell}</SizeProvider> : shell
110
+ }
111
+
112
+ type PanelProps = Omit<PickerShellProps, 'open' | 'defaultOpen' | 'onOpenChange' | 'size'> & {
113
+ open: boolean
114
+ setOpen: (open: boolean) => void
115
+ }
116
+
117
+ type PickerFieldProps = Pick<
118
+ PanelProps,
119
+ 'icon' | 'display' | 'placeholder' | 'variant' | 'invalid' | 'disabled' | 'readOnly' | 'id' | 'className'
120
+ > &
121
+ Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'className'>
122
+
123
+ // Forwards its ref: Base UI anchors the panel to the field through it.
124
+ const PickerField = forwardRef<HTMLButtonElement, PickerFieldProps>(
125
+ ({ icon, display, placeholder, variant = 'bordered', invalid, disabled, readOnly, id, className, ...props }, ref) => {
126
+ const Icon = useIcon(icon)
127
+ const { size: sizeClasses, props: boxProps } = useFieldBox({ variant, invalid, disabled, readOnly })
128
+
129
+ return (
130
+ <button
131
+ ref={ref}
132
+ type="button"
133
+ id={id}
134
+ disabled={disabled}
135
+ aria-invalid={invalid || undefined}
136
+ {...boxProps}
137
+ // Base UI's trigger props (open state, handlers, aria) arrive here.
138
+ {...props}
139
+ className={cn(
140
+ boxProps.className,
141
+ 'flex w-full min-w-0 items-center text-left cursor-pointer outline-none focus-visible:ring-focus-ring',
142
+ sizeClasses.gap,
143
+ sizeClasses.control,
144
+ className,
145
+ )}
146
+ >
147
+ <Icon size={sizeClasses.icon} strokeWidth={1.5} className={fieldIconClasses} />
148
+ <span
149
+ className={cn('flex-1 truncate', sizeClasses.text, display ? 'text-foreground' : 'text-muted-foreground')}
150
+ >
151
+ {display || placeholder}
152
+ </span>
153
+ </button>
154
+ )
155
+ },
156
+ )
157
+ PickerField.displayName = 'PickerField'
158
+
159
+ type EditableFieldProps = Pick<
160
+ PanelProps,
161
+ 'icon' | 'variant' | 'invalid' | 'disabled' | 'readOnly' | 'className' | 'editor' | 'triggerLabel'
162
+ > & {
163
+ /** Wraps the icon button in the panel's trigger. */
164
+ trigger: (button: ReactNode) => ReactNode
165
+ }
166
+
167
+ /** An editable picker's field: its text control, led by the icon button that opens the panel. */
168
+ const EditableField = forwardRef<HTMLDivElement, EditableFieldProps>(
169
+ (
170
+ {
171
+ icon,
172
+ variant = 'bordered',
173
+ invalid,
174
+ disabled,
175
+ readOnly,
176
+ className,
177
+ editor,
178
+ triggerLabel = 'Open calendar',
179
+ trigger,
180
+ },
181
+ ref,
182
+ ) => {
183
+ const Icon = useIcon(icon)
184
+ const { size: sizeClasses, props: boxProps } = useFieldBox({ variant, invalid, disabled, readOnly })
185
+ return (
186
+ <div
187
+ ref={ref}
188
+ {...boxProps}
189
+ className={cn(
190
+ boxProps.className,
191
+ 'flex w-full min-w-0 items-center',
192
+ sizeClasses.gap,
193
+ sizeClasses.control,
194
+ className,
195
+ )}
196
+ >
197
+ <InputAdornment>
198
+ {trigger(<InputAction label={triggerLabel} icon={Icon} disabled={disabled || readOnly} />)}
199
+ </InputAdornment>
200
+ {editor}
201
+ </div>
202
+ )
203
+ },
204
+ )
205
+ EditableField.displayName = 'EditableField'
206
+
207
+ function PanelHeader({
208
+ header,
209
+ closeLabel = 'Close',
210
+ close,
211
+ }: {
212
+ header: ReactNode
213
+ closeLabel?: string
214
+ close: () => void
215
+ }) {
216
+ const X = useIcon('x')
217
+ const sizeClasses = useSize()
218
+ return (
219
+ <div className="flex items-center gap-1 border-b border-border pb-1">
220
+ <div className="flex min-w-0 flex-1 items-center">{header}</div>
221
+ <Button variant="ghost" size="icon-sm" aria-label={closeLabel} onClick={close}>
222
+ <X size={sizeClasses.icon} strokeWidth={1.5} />
223
+ </Button>
224
+ </div>
225
+ )
226
+ }
227
+
228
+ function PickerPopover({
229
+ open,
230
+ setOpen,
231
+ header,
232
+ initialFocus,
233
+ closeLabel,
234
+ dir,
235
+ popupClassName,
236
+ from,
237
+ effect,
238
+ hideSource,
239
+ tier = 'moderate',
240
+ children,
241
+ editor,
242
+ triggerLabel,
243
+ ...field
244
+ }: PanelProps) {
245
+ const fieldRef = useRef<HTMLDivElement>(null)
246
+ const { origin, capture } = useMorphOrigin()
247
+ const morph = useMorph(open, origin, { from, effect, hideSource, tier })
248
+ const shape = useShape()
249
+ const level = Math.min(useSurface() + 2, 8)
250
+
251
+ const handleOpenChange = useCallback(
252
+ (next: boolean, details: { reason?: string; trigger?: Element; event?: Event }) => {
253
+ // Pressing or focusing the editable field's own text is no reason to
254
+ // close: the panel belongs to that field.
255
+ if (!next && editor && (details.reason === 'outside-press' || details.reason === 'focus-out')) {
256
+ const event = details.event as (Event & { relatedTarget?: EventTarget | null }) | undefined
257
+ const target = (event?.relatedTarget ?? event?.target) as Node | null | undefined
258
+ if (target && fieldRef.current?.contains(target)) return
259
+ }
260
+ // An editable field's panel grows out of the whole field, not its icon.
261
+ if (next) capture(editor ? { event: details.event, trigger: fieldRef.current ?? undefined } : details)
262
+ setOpen(next)
263
+ },
264
+ [capture, setOpen, editor],
265
+ )
266
+
267
+ return (
268
+ // Non-modal: the page keeps scrolling and the panel follows its field.
269
+ <Popover.Root open={open} onOpenChange={handleOpenChange} modal={false}>
270
+ {editor ? (
271
+ <EditableField
272
+ ref={fieldRef}
273
+ {...field}
274
+ editor={editor}
275
+ triggerLabel={triggerLabel}
276
+ trigger={button => <Popover.Trigger disabled={field.disabled} render={button as ReactElement} />}
277
+ />
278
+ ) : (
279
+ <Popover.Trigger disabled={field.disabled} render={<PickerField {...field} />} />
280
+ )}
281
+ <Popover.Portal>
282
+ <Popover.Positioner
283
+ anchor={editor ? fieldRef : undefined}
284
+ side="bottom"
285
+ align="start"
286
+ sideOffset={opensInPlace({ effect, hideSource }) ? inPlaceOffset : 6}
287
+ className="z-50 outline-none"
288
+ >
289
+ <Popover.Popup ref={morph.popupRef} initialFocus={initialFocus} className="relative outline-none">
290
+ <SurfaceProvider value={level}>
291
+ <MorphSurface
292
+ morph={morph}
293
+ bg={SURFACE_BG[level]}
294
+ shadow={SURFACE_SHADOW[3]}
295
+ radius={shape.container}
296
+ className={cn('flex max-w-[calc(100vw-1rem)] flex-col gap-1 p-1', popupClassName)}
297
+ >
298
+ <div dir={dir} className="flex min-h-0 flex-col gap-1">
299
+ <PanelHeader header={header} closeLabel={closeLabel} close={() => setOpen(false)} />
300
+ {children}
301
+ </div>
302
+ </MorphSurface>
303
+ </SurfaceProvider>
304
+ </Popover.Popup>
305
+ </Popover.Positioner>
306
+ </Popover.Portal>
307
+ </Popover.Root>
308
+ )
309
+ }
310
+
311
+ function PickerSheet({ open, setOpen, header, closeLabel, dir, children, editor, triggerLabel, ...field }: PanelProps) {
312
+ const {
313
+ initialFocus: _focus,
314
+ popupClassName: _width,
315
+ from: _from,
316
+ effect: _effect,
317
+ hideSource: _hide,
318
+ tier: _tier,
319
+ ...fieldProps
320
+ } = field
321
+ return (
322
+ <Sheet open={open} onOpenChange={setOpen}>
323
+ {editor ? (
324
+ <EditableField
325
+ {...fieldProps}
326
+ editor={editor}
327
+ triggerLabel={triggerLabel}
328
+ trigger={button => <Sheet.Trigger disabled={field.disabled} render={button as ReactElement} />}
329
+ />
330
+ ) : (
331
+ <Sheet.Trigger disabled={field.disabled} render={<PickerField {...fieldProps} />} />
332
+ )}
333
+ <Sheet.Content className="gap-2 p-3 pt-3">
334
+ <div dir={dir} className="flex min-h-0 flex-col gap-2">
335
+ <PanelHeader header={header} closeLabel={closeLabel} close={() => setOpen(false)} />
336
+ {children}
337
+ </div>
338
+ </Sheet.Content>
339
+ </Sheet>
340
+ )
341
+ }
342
+
343
+ interface PickerInputProps extends Omit<InputProps, 'value' | 'defaultValue' | 'onChange'> {
344
+ /** The value as the field shows it, while nothing is being typed. */
345
+ formatted: string
346
+ /** Reads typed text and commits it; returns whether it could. */
347
+ onText: (text: string) => boolean
348
+ /** Enter, after a valid entry: the pickers close. */
349
+ onEnter?: () => void
350
+ }
351
+
352
+ /**
353
+ * A header input: shows the formatted value, selects it all on focus, and
354
+ * commits what is typed as soon as it reads as a value (marking it invalid
355
+ * until then). Leaving it restores the formatted value. Hover and focus fill
356
+ * it without the field ring.
357
+ */
358
+ const PickerInput = forwardRef<HTMLInputElement, PickerInputProps>(
359
+ ({ formatted, onText, onEnter, onFocus, onBlur, onKeyDown, className, ...props }, ref) => {
360
+ const [draft, setDraft] = useState<string | null>(null)
361
+ const [invalid, setInvalid] = useState(false)
362
+ return (
363
+ <Input
364
+ ref={ref}
365
+ {...props}
366
+ // The header is already framed by the panel: hover and focus change
367
+ // the fill only (an invalid entry keeps its destructive ring).
368
+ className={cn('hover:ring-transparent focus-within:ring-transparent data-[active]:ring-transparent', className)}
369
+ value={draft ?? formatted}
370
+ invalid={invalid}
371
+ onChange={text => {
372
+ setDraft(text)
373
+ setInvalid(!onText(text))
374
+ }}
375
+ onFocus={e => {
376
+ e.currentTarget.select()
377
+ onFocus?.(e)
378
+ }}
379
+ onBlur={e => {
380
+ setDraft(null)
381
+ setInvalid(false)
382
+ onBlur?.(e)
383
+ }}
384
+ onKeyDown={e => {
385
+ if (e.key === 'Enter' && !invalid) onEnter?.()
386
+ onKeyDown?.(e)
387
+ }}
388
+ />
389
+ )
390
+ },
391
+ )
392
+ PickerInput.displayName = 'PickerInput'
393
+
394
+ export type { PickerShellProps }
395
+ export { PickerInput, PickerShell }
@@ -0,0 +1,91 @@
1
+ /*
2
+ * Local addition (not part of Fluid Functionalism): the hour column (its
3
+ * AM/PM folded in on a 12-hour clock) and the minute column the date picker
4
+ * (with time) and the time picker share. Not an item (a file in a group folder).
5
+ */
6
+
7
+ import { format, type Locale } from 'date-fns'
8
+ import { useMemo } from 'react'
9
+ import { fieldLabel, type HourCycle } from '../../../lib/date-locale'
10
+ import { ScrollColumn } from '../../../lib/scroll-column'
11
+ import { cn } from '../../../lib/utils'
12
+
13
+ interface Time {
14
+ hours: number
15
+ minutes: number
16
+ }
17
+
18
+ interface TimeColumnsProps {
19
+ value: Time | null
20
+ onValueChange: (time: Time) => void
21
+ hourCycle: HourCycle
22
+ minuteStep: number
23
+ locale: Locale
24
+ orientation: 'vertical' | 'horizontal'
25
+ /** Columns share the row's width equally instead of keeping their narrow fixed widths. Defaults to `false`. */
26
+ fill?: boolean
27
+ }
28
+
29
+ const pad = (n: number) => String(n).padStart(2, '0')
30
+
31
+ /** Columns for the hour (with its AM/PM on a 12-hour clock) and the minute; a row of them divides itself. */
32
+ function TimeColumns({
33
+ value,
34
+ onValueChange,
35
+ hourCycle,
36
+ minuteStep,
37
+ locale,
38
+ orientation,
39
+ fill = false,
40
+ }: TimeColumnsProps) {
41
+ const twelve = hourCycle === 'h12' || hourCycle === 'h11'
42
+ const hours = value?.hours ?? null
43
+ const minutes = value?.minutes ?? 0
44
+
45
+ const hourOptions = useMemo(
46
+ () =>
47
+ Array.from({ length: 24 }, (_, h) => ({
48
+ value: h,
49
+ // "12 AM" … "11 PM" (h11: "0 AM" … "11 PM"), in the locale's words.
50
+ label: twelve ? format(new Date(2000, 0, 1, h), hourCycle === 'h11' ? 'K a' : 'h a', { locale }) : pad(h),
51
+ })),
52
+ [twelve, hourCycle, locale],
53
+ )
54
+ const minuteOptions = useMemo(
55
+ () =>
56
+ Array.from({ length: Math.ceil(60 / minuteStep) }, (_, i) => ({
57
+ value: i * minuteStep,
58
+ label: pad(i * minuteStep),
59
+ })),
60
+ [minuteStep],
61
+ )
62
+
63
+ const vertical = orientation === 'vertical'
64
+ // Beside a calendar: narrower than its month and year columns, an hour
65
+ // with its AM/PM a little wider. On their own (`fill`): an equal share.
66
+ const width = (fixed: string) => (!vertical ? 'w-full' : fill ? 'min-w-0 flex-1' : cn('shrink-0', fixed))
67
+
68
+ return (
69
+ <>
70
+ <ScrollColumn
71
+ label={fieldLabel(locale, 'hour')}
72
+ options={hourOptions}
73
+ value={hours}
74
+ onValueChange={h => onValueChange({ hours: h, minutes })}
75
+ orientation={orientation}
76
+ className={width(twelve ? 'w-15' : 'w-12')}
77
+ />
78
+ <ScrollColumn
79
+ label={fieldLabel(locale, 'minute')}
80
+ options={minuteOptions}
81
+ value={hours === null ? null : minutes}
82
+ onValueChange={m => onValueChange({ hours: hours ?? 0, minutes: m })}
83
+ orientation={orientation}
84
+ className={width('w-12')}
85
+ />
86
+ </>
87
+ )
88
+ }
89
+
90
+ export type { Time }
91
+ export { pad, TimeColumns }
@@ -0,0 +1,26 @@
1
+ import TimePicker from './index'
2
+ import { de } from 'react-day-picker/locale'
3
+
4
+ # Basic
5
+
6
+ The locale's clock: in English, 12-hour, the hours read with AM/PM.
7
+
8
+ <div className="w-48 max-w-full">
9
+ <TimePicker defaultValue="15:33" />
10
+ </div>
11
+
12
+ # 24-hour clock
13
+
14
+ A German locale writes 24-hour times.
15
+
16
+ <div className="w-48 max-w-full">
17
+ <TimePicker locale={de} defaultValue="15:33" />
18
+ </div>
19
+
20
+ # Quarter hours
21
+
22
+ `minuteStep={15}` leaves four minutes to pick from.
23
+
24
+ <div className="w-48 max-w-full">
25
+ <TimePicker hourCycle="h23" minuteStep={15} placeholder="Start time" />
26
+ </div>
@@ -0,0 +1,166 @@
1
+ /*
2
+ * Local addition (not part of Fluid Functionalism): a time-of-day field with
3
+ * scrolling hour and minute columns. Props after utegsk/ui's TimePicker
4
+ * (`hourCycle`, a string value), kept in line with the other pickers here.
5
+ */
6
+
7
+ import { format, type Locale, set } from 'date-fns'
8
+ import { type RefObject, useRef } from 'react'
9
+ import { defaultLocale, type HourCycle, hourCycleOf, parseTimeText } from '../../../../lib/date-locale'
10
+ import type { FieldVariant } from '../../../../lib/field-classes'
11
+ import { dividedColumns } from '../../../../lib/scroll-column'
12
+ import type { SizeVariant } from '../../../../lib/size-context'
13
+ import { useControllableState } from '../../../../lib/use-controllable-state'
14
+ import { cn } from '../../../../lib/utils'
15
+ import { PickerInput, PickerShell } from '../picker-shell'
16
+ import { pad, type Time, TimeColumns } from '../time-columns'
17
+
18
+ interface TimePickerProps {
19
+ /** The picked time as `"HH:mm"` on a 24-hour clock (an `<input type="time">`'s value); `null` when empty. */
20
+ value?: string | null
21
+ /** Initial time, for an uncontrolled picker. Defaults to `null`. */
22
+ defaultValue?: string | null
23
+ /** Called with the new `"HH:mm"`, or `null` when the field is cleared. */
24
+ onValueChange?: (value: string | null) => void
25
+ /** The clock shown: on `'h12'` / `'h11'` the hours read with AM/PM. Defaults to the locale's. */
26
+ hourCycle?: HourCycle
27
+ /** Steps between the minute column's options. Defaults to `1`. */
28
+ minuteStep?: number
29
+ /** A react-day-picker / date-fns locale: the time's format and the AM/PM names. Defaults to `enUS`. */
30
+ locale?: Locale
31
+ /** Shown while empty. */
32
+ placeholder?: string
33
+ /** Disables the field. Defaults to `false`. */
34
+ disabled?: boolean
35
+ /** Framed at rest, or invisible until hovered or focused. Defaults to `"bordered"`. */
36
+ variant?: FieldVariant
37
+ /** Marks the field invalid. Defaults to `false`. */
38
+ invalid?: boolean
39
+ /** Pins the field and its panel to one step of the size ladder. Defaults to the surrounding SizeProvider. */
40
+ size?: SizeVariant
41
+ /** Controlled open state. */
42
+ open?: boolean
43
+ /** Initial open state, for an uncontrolled panel. Defaults to `false`. */
44
+ defaultOpen?: boolean
45
+ /** Called when the panel opens or closes. */
46
+ onOpenChange?: (open: boolean) => void
47
+ /** Id of the field, for a `Label`'s `htmlFor`. */
48
+ id?: string
49
+ /** Where the panel grows from (see Morph): the field, the press point, its own center, a viewport edge, or a ref to any element. Defaults to `'trigger'` (the field). */
50
+ from?: 'trigger' | 'pointer' | 'center' | 'top' | 'right' | 'bottom' | 'left' | RefObject<HTMLElement | null>
51
+ /** How the panel grows (see Morph), as Combobox's list does: with the liquid goo neck, a plain morph, a slide or a fade. `'morph'` with `hideSource` opens it in place over the field. Defaults to `'goo'`. */
52
+ effect?: 'goo' | 'morph' | 'slide' | 'fade'
53
+ /** Hide the field while open, so it reads as turning into the panel. Defaults to `false`. */
54
+ hideSource?: boolean
55
+ /** Spring tier of a plain morph, slide or fade; goo runs on its own `spring.goo`. Defaults to `'moderate'`. */
56
+ tier?: 'moderate' | 'slow'
57
+ /** Classes for the field. */
58
+ className?: string
59
+ }
60
+
61
+ const toTime = (value: string | null): Time | null => {
62
+ const [hours, minutes] = value?.split(':').map(Number) ?? []
63
+ return hours === undefined || minutes === undefined ? null : { hours, minutes }
64
+ }
65
+ const toValue = ({ hours, minutes }: Time) => `${pad(hours)}:${pad(minutes)}`
66
+
67
+ /**
68
+ * A time-of-day field with scrolling hour and minute columns.
69
+ *
70
+ * The panel opens with the time as text, selected and ready to type over
71
+ * ("15:33", "3:33 PM"), above an hour column and a minute column; on a
72
+ * 12-hour clock the hours read with AM/PM. Each column keeps its pick in the middle and
73
+ * marks it with a dot. The value is `"HH:mm"` on a 24-hour clock whatever the
74
+ * locale shows, the same as a native time input's.
75
+ *
76
+ * @example {@include ./examples.mdx}
77
+ */
78
+ function TimePicker({
79
+ value: valueProp,
80
+ defaultValue = null,
81
+ onValueChange,
82
+ hourCycle,
83
+ minuteStep = 1,
84
+ locale = defaultLocale,
85
+ placeholder,
86
+ disabled,
87
+ variant,
88
+ invalid,
89
+ size,
90
+ open: openProp,
91
+ defaultOpen = false,
92
+ onOpenChange,
93
+ id,
94
+ from,
95
+ effect,
96
+ hideSource,
97
+ tier,
98
+ className,
99
+ }: TimePickerProps) {
100
+ const [value, setValue] = useControllableState(valueProp, defaultValue, onValueChange)
101
+ const [open, setOpen] = useControllableState(openProp, defaultOpen, onOpenChange)
102
+ const input = useRef<HTMLInputElement>(null)
103
+ const time = toTime(value)
104
+ const formatted = time ? format(set(new Date(), time), 'p', { locale }) : ''
105
+
106
+ const readTime = (text: string) => {
107
+ if (!text.trim()) {
108
+ setValue(null)
109
+ return true
110
+ }
111
+ const read = parseTimeText(text, locale)
112
+ if (!read) return false
113
+ setValue(toValue({ hours: read.getHours(), minutes: read.getMinutes() }))
114
+ return true
115
+ }
116
+
117
+ return (
118
+ <PickerShell
119
+ open={open}
120
+ onOpenChange={setOpen}
121
+ size={size}
122
+ variant={variant}
123
+ invalid={invalid}
124
+ disabled={disabled}
125
+ icon="clock"
126
+ display={formatted}
127
+ placeholder={placeholder}
128
+ header={
129
+ <PickerInput
130
+ ref={input}
131
+ aria-label={placeholder ?? 'Time'}
132
+ placeholder={placeholder}
133
+ formatted={formatted}
134
+ onText={readTime}
135
+ onEnter={() => setOpen(false)}
136
+ className="flex-1"
137
+ />
138
+ }
139
+ initialFocus={input}
140
+ popupClassName="w-[max(var(--anchor-width,0px),14rem)]"
141
+ id={id}
142
+ from={from}
143
+ effect={effect}
144
+ hideSource={hideSource}
145
+ tier={tier}
146
+ className={className}
147
+ >
148
+ <div className={cn(dividedColumns, 'h-56')}>
149
+ <TimeColumns
150
+ value={time}
151
+ onValueChange={next => setValue(toValue(next))}
152
+ hourCycle={hourCycle ?? hourCycleOf(locale)}
153
+ minuteStep={minuteStep}
154
+ locale={locale}
155
+ orientation="vertical"
156
+ fill
157
+ />
158
+ </div>
159
+ </PickerShell>
160
+ )
161
+ }
162
+
163
+ export type { TimePickerProps }
164
+ export { TimePicker }
165
+
166
+ export default TimePicker