@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,494 @@
1
+ /*
2
+ * Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
3
+ * `registry/base/tabs.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
4
+ * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
5
+ * Local modifications: `"use client"` dropped; `@/lib/*` and `@/hooks/*`
6
+ * imports rewritten to `../../../lib/*` (`SizeProvider` to
7
+ * `../../../primitives/sizes`), `framer-motion` to `motion/react`; `defaultSelectedIndex` added as the index-mode twin of the
8
+ * existing value-mode `defaultValue` (it only seeds the first render, like
9
+ * `defaultValue`); modo docs — TSDoc with FF's docs/API text, `Tabs.List` /
10
+ * `Tabs.Item` / `Tabs.Panel` statics (typed via a cast on the root), default
11
+ * export (upstream has none).
12
+ * Styling reads DS tokens (AGENTS.md styling): inline `fontVariationSettings`
13
+ * → `weight-*`; the hex focus-ring fallback → `ring-focus-ring` /
14
+ * `border-focus-ring`; `duration-80|120|160` and tier-length JS durations →
15
+ * `duration-<tier>` / `spring.*`.
16
+ * The active indicator and the hover pill are `GooIndicator`s
17
+ * (`lib/goo-indicator.tsx`, local): they melt from tab to tab instead of
18
+ * sliding; the hover pill now always returns to the active tab as it fades.
19
+ * - `Tabs.Item` `label` takes any node (a counter, a badge) besides a string;
20
+ * the weight ghost-span keeps holding the width, its hidden copy `inert`
21
+ * (`markInert` from `lib/inert.ts`).
22
+ * `href` renders the tab as an anchor (link tabs) through Base UI's `render`.
23
+ */
24
+
25
+ import { Tabs as TabsPrimitive } from '@base-ui/react/tabs'
26
+ import { AnimatePresence, motion } from 'motion/react'
27
+ import {
28
+ Children,
29
+ type ComponentPropsWithoutRef,
30
+ cloneElement,
31
+ createContext,
32
+ type ForwardRefExoticComponent,
33
+ forwardRef,
34
+ isValidElement,
35
+ type ReactNode,
36
+ type RefAttributes,
37
+ useCallback,
38
+ useContext,
39
+ useEffect,
40
+ useLayoutEffect,
41
+ useRef,
42
+ useState,
43
+ } from 'react'
44
+ import { GooIndicator } from '../../../lib/goo-indicator'
45
+ import type { IconComponent } from '../../../lib/icon-context'
46
+ import { markInert } from '../../../lib/inert'
47
+ import { useShape } from '../../../lib/shape-context'
48
+ import { type SizeVariant, useSize } from '../../../lib/size-context'
49
+ import { spring } from '../../../lib/springs'
50
+ import { SURFACE_BG, SURFACE_SHADOW } from '../../../lib/surface-classes'
51
+ import { useSurface } from '../../../lib/surface-context'
52
+ import { useFluidHover } from '../../../lib/use-fluid-hover'
53
+ import { cn } from '../../../lib/utils'
54
+ import { SizeProvider } from '../../../primitives/sizes'
55
+
56
+ /* ─────────────────────── Contexts ─────────────────────── */
57
+
58
+ interface TabsValueOrderContextValue {
59
+ valueOrder: string[]
60
+ setValueOrder: (order: string[]) => void
61
+ selectedValue: string | undefined
62
+ }
63
+
64
+ const TabsValueOrderContext = createContext<TabsValueOrderContextValue | null>(null)
65
+
66
+ interface TabsListContextValue {
67
+ registerTab: (index: number, value: string, el: HTMLElement | null) => void
68
+ hoveredIndex: number | null
69
+ selectedValue: string | undefined
70
+ setOptimisticIdx: (index: number) => void
71
+ }
72
+
73
+ const TabsListContext = createContext<TabsListContextValue | null>(null)
74
+
75
+ function useTabsList() {
76
+ const ctx = useContext(TabsListContext)
77
+ if (!ctx) throw new Error('TabItem must be used within a TabsList')
78
+ return ctx
79
+ }
80
+
81
+ /* ─────────────────────── Tabs (Root) ─────────────────────── */
82
+
83
+ interface TabsProps
84
+ extends Omit<
85
+ ComponentPropsWithoutRef<typeof TabsPrimitive.Root>,
86
+ 'onValueChange' | 'value' | 'defaultValue' | 'onSelect'
87
+ > {
88
+ /** Controlled active tab value. Takes precedence over selectedIndex. */
89
+ value?: string
90
+ /** Called when the active tab changes. */
91
+ onValueChange?: (value: string) => void
92
+ /** Index-based controlled alternative. */
93
+ selectedIndex?: number
94
+ /** Called with the new index when the active tab changes. */
95
+ onSelect?: (index: number) => void
96
+ /** Default active tab for uncontrolled usage. Defaults to the first tab. */
97
+ defaultValue?: string
98
+ /** Index-based uncontrolled alternative: the tab active on first render. Ignored when `defaultValue` is set. */
99
+ defaultSelectedIndex?: number
100
+ /** Pins the segmented control to one step of the size ladder (default 36px outer, compact 28px). Defaults to the surrounding SizeProvider. */
101
+ size?: SizeVariant
102
+ /** Tabs.List and Tabs.Panel children. */
103
+ children?: ReactNode
104
+ }
105
+
106
+ type TabsComponent = ForwardRefExoticComponent<TabsProps & RefAttributes<HTMLDivElement>> & {
107
+ List: typeof TabsList
108
+ Item: typeof TabItem
109
+ Panel: typeof TabPanel
110
+ }
111
+
112
+ /**
113
+ * Segmented control tabs with sliding active indicator, fluid hover, and
114
+ * spring animations.
115
+ *
116
+ * Built on Base UI Tabs: `Tabs` owns the selection, `Tabs.List` draws the
117
+ * segmented track with an elevated indicator that melts toward the active tab
118
+ * like a drop (lifted three surface levels above whatever it sits on; see the
119
+ * liquid indicators in Morph) and a translucent hover pill that drips out of
120
+ * it, and `Tabs.Panel` shows the content of the active tab. Arrow keys
121
+ * move and activate. Select by value — `defaultValue` uncontrolled, `value` +
122
+ * `onValueChange` controlled — or by index with `defaultSelectedIndex` /
123
+ * `selectedIndex` + `onSelect`.
124
+ *
125
+ * Statics:
126
+ * - `Tabs.List` — the segmented track holding the tabs.
127
+ * - `Tabs.Item` — one tab: `value`, `label` (text, or a node such as a
128
+ * label with a counter), optional `icon`, and `href` to render it as a link.
129
+ * - `Tabs.Panel` — content shown while the tab with the same `value` is active.
130
+ *
131
+ * @example {@include ./examples.mdx}
132
+ */
133
+ const Tabs = forwardRef<HTMLDivElement, TabsProps>(
134
+ (
135
+ { value, onValueChange, selectedIndex, onSelect, defaultValue, defaultSelectedIndex, size, children, ...props },
136
+ ref,
137
+ ) => {
138
+ const [valueOrder, setValueOrder] = useState<string[]>([])
139
+ const [uncontrolledValue, setUncontrolledValue] = useState<string | undefined>(defaultValue)
140
+ const updateValueOrder = useCallback((order: string[]) => {
141
+ setValueOrder(current => {
142
+ if (current.length === order.length && current.every((v, i) => v === order[i])) {
143
+ return current
144
+ }
145
+ return order
146
+ })
147
+ }, [])
148
+
149
+ // Resolve value: explicit value > selectedIndex lookup > uncontrolled state.
150
+ // Uncontrolled with no defaultValue falls back to the first tab so the
151
+ // FF layer's selectedValue matches what the primitive shows.
152
+ // Local: ...or to the `defaultSelectedIndex` tab, when given.
153
+ const resolvedValue =
154
+ value ??
155
+ (selectedIndex != null ? valueOrder[selectedIndex] : (uncontrolledValue ?? valueOrder[defaultSelectedIndex ?? 0]))
156
+
157
+ // Base UI passes (value, eventDetails); we only need value.
158
+ const handleValueChange = useCallback(
159
+ (newValue: unknown) => {
160
+ const v = newValue as string
161
+ if (value === undefined && selectedIndex == null) {
162
+ setUncontrolledValue(v)
163
+ }
164
+ onValueChange?.(v)
165
+ if (onSelect) {
166
+ const idx = valueOrder.indexOf(v)
167
+ if (idx !== -1) onSelect(idx)
168
+ }
169
+ },
170
+ [onValueChange, onSelect, valueOrder, value, selectedIndex],
171
+ )
172
+
173
+ const root = (
174
+ <TabsValueOrderContext.Provider
175
+ value={{
176
+ valueOrder,
177
+ setValueOrder: updateValueOrder,
178
+ selectedValue: resolvedValue,
179
+ }}
180
+ >
181
+ {/*
182
+ Always controlled: Base UI's useControlled logs a dev warning when
183
+ value flips undefined → defined. valueOrder is empty on the first
184
+ commit, so fall back to an empty-string sentinel — TabsList's
185
+ layout effect populates valueOrder pre-paint, so the corrected
186
+ value lands before anything is visible.
187
+ */}
188
+ <TabsPrimitive.Root ref={ref} value={resolvedValue ?? ''} onValueChange={handleValueChange} {...props}>
189
+ {children}
190
+ </TabsPrimitive.Root>
191
+ </TabsValueOrderContext.Provider>
192
+ )
193
+
194
+ // A size prop pins the whole compound (list + items) to one ladder step.
195
+ return size ? <SizeProvider size={size}>{root}</SizeProvider> : root
196
+ },
197
+ ) as TabsComponent
198
+
199
+ Tabs.displayName = 'Tabs'
200
+
201
+ /* ─────────────────────── TabsList ─────────────────────── */
202
+
203
+ type TabsListProps = ComponentPropsWithoutRef<typeof TabsPrimitive.List>
204
+
205
+ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(({ children, className, ...props }, ref) => {
206
+ const containerRef = useRef<HTMLDivElement>(null)
207
+ const isMouseInside = useRef(false)
208
+ const shape = useShape()
209
+ const sizeClasses = useSize()
210
+ const substrate = useSurface()
211
+ const indicatorLevel = Math.min(substrate + 3, 8)
212
+ const valueOrderCtx = useContext(TabsValueOrderContext)
213
+ const [optimisticIdx, setOptimisticIdx] = useState<number | null>(null)
214
+
215
+ const values = Children.toArray(children)
216
+ .filter(isValidElement)
217
+ .map(child => (child.props as { value?: string }).value)
218
+ .filter((v): v is string => typeof v === 'string')
219
+ const valueOrderKey = values.join(',')
220
+ const setValueOrder = valueOrderCtx?.setValueOrder
221
+
222
+ useLayoutEffect(() => {
223
+ setValueOrder?.(values)
224
+ }, [setValueOrder, valueOrderKey])
225
+
226
+ const {
227
+ activeIndex: hoveredIndex,
228
+ setActiveIndex: setHoveredIndex,
229
+ itemRects,
230
+ handlers,
231
+ registerItem,
232
+ measureItems,
233
+ } = useFluidHover(containerRef, { axis: 'x' })
234
+
235
+ const registerTab = useCallback(
236
+ (index: number, _value: string, el: HTMLElement | null) => {
237
+ registerItem(index, el)
238
+ },
239
+ [registerItem],
240
+ )
241
+
242
+ useEffect(() => {
243
+ measureItems()
244
+ }, [measureItems, children])
245
+
246
+ const handleMouseMove = useCallback(
247
+ (e: React.MouseEvent) => {
248
+ isMouseInside.current = true
249
+ handlers.onMouseMove(e)
250
+ },
251
+ [handlers],
252
+ )
253
+
254
+ const handleMouseLeave = useCallback(() => {
255
+ isMouseInside.current = false
256
+ handlers.onMouseLeave()
257
+ }, [handlers])
258
+
259
+ const [focusedIndex, setFocusedIndex] = useState<number | null>(null)
260
+ const selectedValue = valueOrderCtx?.selectedValue
261
+ const selectedIdx = selectedValue !== undefined ? values.indexOf(selectedValue) : -1
262
+
263
+ useEffect(() => {
264
+ setOptimisticIdx(selectedIdx >= 0 ? selectedIdx : null)
265
+ }, [selectedIdx])
266
+
267
+ const activeSelectedIdx = optimisticIdx
268
+ const selectedRect = activeSelectedIdx !== null ? itemRects[activeSelectedIdx] : null
269
+ const hoverRect = hoveredIndex !== null ? itemRects[hoveredIndex] : null
270
+ const focusRect = focusedIndex !== null ? itemRects[focusedIndex] : null
271
+ const isHoveringSelected = hoveredIndex === activeSelectedIdx
272
+ const isHovering = hoveredIndex !== null && !isHoveringSelected
273
+
274
+ const indexedChildren = Children.map(children, (child, i) => {
275
+ // Skip plain DOM elements — injecting _index into e.g. a <div>
276
+ // triggers React's unknown-prop warning.
277
+ if (isValidElement(child) && typeof child.type !== 'string') {
278
+ return cloneElement(child, { _index: i } as Record<string, unknown>)
279
+ }
280
+ return child
281
+ })
282
+
283
+ return (
284
+ <TabsListContext.Provider
285
+ value={{
286
+ registerTab,
287
+ hoveredIndex,
288
+ selectedValue,
289
+ setOptimisticIdx,
290
+ }}
291
+ >
292
+ <TabsPrimitive.List
293
+ // Match Radix's `activationMode="automatic"` — arrow keys move + activate.
294
+ activateOnFocus
295
+ ref={node => {
296
+ ;(containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node
297
+ if (typeof ref === 'function') ref(node)
298
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
299
+ }}
300
+ onMouseMove={handleMouseMove}
301
+ onMouseLeave={handleMouseLeave}
302
+ onFocus={e => {
303
+ const trigger = (e.target as HTMLElement).closest('[role="tab"]')
304
+ if (!trigger) return
305
+ const indexAttr = trigger.getAttribute('data-fluid-hover-index')
306
+ if (indexAttr != null) {
307
+ const idx = Number(indexAttr)
308
+ setHoveredIndex(idx)
309
+ setFocusedIndex((e.target as HTMLElement).matches(':focus-visible') ? idx : null)
310
+ }
311
+ }}
312
+ onBlur={e => {
313
+ if (containerRef.current?.contains(e.relatedTarget as Node)) return
314
+ setFocusedIndex(null)
315
+ if (isMouseInside.current) return
316
+ setHoveredIndex(null)
317
+ }}
318
+ className={cn(
319
+ // segmentPad + segmentItem add up to the ladder's control height
320
+ // (36px default, 28px compact) so the segmented control's outer
321
+ // box lines up with buttons, selects, and inputs beside it.
322
+ 'relative inline-flex items-center select-none bg-muted',
323
+ sizeClasses.segmentPad,
324
+ shape.container,
325
+ className,
326
+ )}
327
+ {...props}
328
+ >
329
+ {selectedRect && (
330
+ <GooIndicator
331
+ rect={selectedRect}
332
+ className={cn(SURFACE_BG[indicatorLevel], shape.bg)}
333
+ shadow={SURFACE_SHADOW[indicatorLevel]}
334
+ opacity={isHovering ? 0.85 : 1}
335
+ />
336
+ )}
337
+
338
+ {/* the hover pill rests on the active tab, so it drips back into it as it fades */}
339
+ {selectedRect && (
340
+ <GooIndicator
341
+ rect={isHovering && hoverRect ? hoverRect : selectedRect}
342
+ className={cn('bg-hover', shape.bg)}
343
+ transition={spring.fast}
344
+ opacity={isHovering ? 0.4 : 0}
345
+ />
346
+ )}
347
+
348
+ {/* Focus ring */}
349
+ <AnimatePresence>
350
+ {focusRect && (
351
+ <motion.div
352
+ className={cn('absolute pointer-events-none z-20 border border-focus-ring', shape.focusRing)}
353
+ initial={false}
354
+ animate={{
355
+ left: focusRect.left - 2,
356
+ top: focusRect.top - 2,
357
+ width: focusRect.width + 4,
358
+ height: focusRect.height + 4,
359
+ }}
360
+ exit={{ opacity: 0, transition: spring.fast.exit }}
361
+ transition={{
362
+ ...spring.fast,
363
+ opacity: { duration: spring.fast.duration },
364
+ }}
365
+ />
366
+ )}
367
+ </AnimatePresence>
368
+
369
+ {indexedChildren}
370
+ </TabsPrimitive.List>
371
+ </TabsListContext.Provider>
372
+ )
373
+ })
374
+
375
+ TabsList.displayName = 'TabsList'
376
+
377
+ /* ─────────────────────── TabItem ─────────────────────── */
378
+
379
+ interface TabItemProps extends ComponentPropsWithoutRef<typeof TabsPrimitive.Tab> {
380
+ /** The tab's value, matched by `Tabs.Panel` and the root's `value`. */
381
+ value: string
382
+ /** Leading icon; its stroke thickens while the tab is hovered or active. */
383
+ icon?: IconComponent
384
+ /** The label: text, or a node such as text with a counter or a badge. */
385
+ label: ReactNode
386
+ /** Render the tab as an anchor to this URL (link tabs): pressing it follows the link and selects the tab. */
387
+ href?: string
388
+ /** @internal Auto-assigned by TabsList. */
389
+ _index?: number
390
+ }
391
+
392
+ const TabItem = forwardRef<HTMLButtonElement, TabItemProps>(
393
+ ({ value, icon: Icon, label, href, _index = 0, className, onClick, ...props }, ref) => {
394
+ const internalRef = useRef<HTMLButtonElement>(null)
395
+ const sizeClasses = useSize()
396
+ const { registerTab, hoveredIndex, selectedValue, setOptimisticIdx } = useTabsList()
397
+
398
+ useEffect(() => {
399
+ registerTab(_index, value, internalRef.current)
400
+ return () => registerTab(_index, value, null)
401
+ }, [_index, value, registerTab])
402
+
403
+ const isSelected = selectedValue === value
404
+ const isActive = hoveredIndex === _index || isSelected
405
+ // A text label trims its box to the cap height; a node (a counter, a badge) lays out as a row.
406
+ const text = typeof label === 'string' || typeof label === 'number'
407
+ const labelLayout = text ? '[text-box:trim-both_cap_alphabetic]' : 'inline-flex items-center gap-1.5'
408
+
409
+ return (
410
+ <TabsPrimitive.Tab
411
+ // Composed (not spread-overridable): a consumer onClick must not
412
+ // replace the optimistic indicator jump.
413
+ onClick={e => {
414
+ setOptimisticIdx(_index)
415
+ onClick?.(e)
416
+ }}
417
+ ref={node => {
418
+ ;(internalRef as React.MutableRefObject<HTMLElement | null>).current = node as HTMLButtonElement | null
419
+ if (typeof ref === 'function') ref(node as HTMLButtonElement)
420
+ else if (ref)
421
+ (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node as HTMLButtonElement | null
422
+ }}
423
+ value={value}
424
+ // Local: link tabs render an anchor (Base UI keeps the tab semantics on it).
425
+ // biome-ignore lint/a11y/useAnchorContent: Base UI renders the tab's label into the anchor
426
+ {...(href !== undefined && { render: <a href={href} />, nativeButton: false })}
427
+ data-fluid-hover-index={_index}
428
+ className={cn(
429
+ // Fixed height (not py) so the text-box trim below doesn't shrink
430
+ // the tab — browsers without text-box support render identically.
431
+ 'relative z-10 flex items-center px-3 cursor-pointer bg-transparent border-none outline-none no-underline',
432
+ sizeClasses.segmentItem,
433
+ sizeClasses.gap,
434
+ className,
435
+ )}
436
+ {...props}
437
+ >
438
+ {Icon && (
439
+ <Icon
440
+ size={sizeClasses.icon}
441
+ strokeWidth={isActive ? 2 : 1.5}
442
+ className={cn(
443
+ 'transition-[color,stroke-width] duration-fast',
444
+ isActive ? 'text-foreground' : 'text-muted-foreground',
445
+ )}
446
+ />
447
+ )}
448
+ {/* Both stacked spans carry the same layout so the invisible bold
449
+ sizer and the visible label keep identical boxes. A node label
450
+ renders twice too; its hidden copy is inert, so keep it
451
+ presentational (text, counters, badges). */}
452
+ <span className={cn('inline-grid whitespace-nowrap', sizeClasses.text)}>
453
+ <span
454
+ className={cn('col-start-1 row-start-1 invisible weight-semibold', labelLayout)}
455
+ aria-hidden="true"
456
+ ref={text ? undefined : markInert}
457
+ >
458
+ {label}
459
+ </span>
460
+ <span
461
+ className={cn(
462
+ 'col-start-1 row-start-1 transition-[color,font-variation-settings] duration-fast',
463
+ labelLayout,
464
+ isActive ? 'text-foreground' : 'text-muted-foreground',
465
+ isSelected ? 'weight-semibold' : 'weight-normal',
466
+ )}
467
+ >
468
+ {label}
469
+ </span>
470
+ </span>
471
+ </TabsPrimitive.Tab>
472
+ )
473
+ },
474
+ )
475
+
476
+ TabItem.displayName = 'TabItem'
477
+
478
+ /* ─────────────────────── TabPanel ─────────────────────── */
479
+
480
+ interface TabPanelProps extends ComponentPropsWithoutRef<typeof TabsPrimitive.Panel> {
481
+ value: string
482
+ }
483
+
484
+ const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(({ className, ...props }, ref) => {
485
+ return <TabsPrimitive.Panel ref={ref} className={cn('outline-none', className)} {...props} />
486
+ })
487
+
488
+ TabPanel.displayName = 'TabPanel'
489
+
490
+ Object.assign(Tabs, { List: TabsList, Item: TabItem, Panel: TabPanel })
491
+
492
+ export type { TabItemProps, TabPanelProps, TabsListProps, TabsProps }
493
+ export { TabItem, TabPanel, Tabs, TabsList }
494
+ export default Tabs
@@ -0,0 +1,49 @@
1
+ import TabsSubtle from './index'
2
+ import { Clock, SquareLibrary, Star, Users } from 'lucide-react'
3
+
4
+ # Basic
5
+
6
+ The pill follows the selected tab; panels swap with it.
7
+
8
+ <div className="flex flex-col gap-4 w-full">
9
+ <TabsSubtle defaultSelectedIndex={0}>
10
+ {['Teamspaces', 'Recents', 'Favorites', 'Shared'].map((label, i) => (
11
+ <TabsSubtle.Item key={label} index={i} label={label} />
12
+ ))}
13
+ {['Teamspaces', 'Recents', 'Favorites', 'Shared'].map((label, i) => (
14
+ <TabsSubtle.Panel key={label} index={i}>
15
+ <p className="text-body text-muted-foreground px-3">{label} content goes here.</p>
16
+ </TabsSubtle.Panel>
17
+ ))}
18
+ </TabsSubtle>
19
+ </div>
20
+
21
+ # With Icons
22
+
23
+ An `icon` sits ahead of the label and thickens its stroke while its tab is
24
+ hovered or selected.
25
+
26
+ <div className="flex flex-col gap-4 w-full">
27
+ <TabsSubtle defaultSelectedIndex={0}>
28
+ <TabsSubtle.Item index={0} icon={SquareLibrary} label="Teamspaces" />
29
+ <TabsSubtle.Item index={1} icon={Clock} label="Recents" />
30
+ <TabsSubtle.Item index={2} icon={Star} label="Favorites" />
31
+ <TabsSubtle.Item index={3} icon={Users} label="Shared" />
32
+ <TabsSubtle.Panel index={0}><p className="text-body text-muted-foreground px-3">Teamspaces content goes here.</p></TabsSubtle.Panel>
33
+ <TabsSubtle.Panel index={1}><p className="text-body text-muted-foreground px-3">Recents content goes here.</p></TabsSubtle.Panel>
34
+ <TabsSubtle.Panel index={2}><p className="text-body text-muted-foreground px-3">Favorites content goes here.</p></TabsSubtle.Panel>
35
+ <TabsSubtle.Panel index={3}><p className="text-body text-muted-foreground px-3">Shared content goes here.</p></TabsSubtle.Panel>
36
+ </TabsSubtle>
37
+ </div>
38
+
39
+ # Active Label
40
+
41
+ With `activeLabel`, every tab shrinks to its icon and only the selected one
42
+ spells itself out.
43
+
44
+ <TabsSubtle activeLabel defaultSelectedIndex={0}>
45
+ <TabsSubtle.Item index={0} icon={SquareLibrary} label="Teamspaces" />
46
+ <TabsSubtle.Item index={1} icon={Clock} label="Recents" />
47
+ <TabsSubtle.Item index={2} icon={Star} label="Favorites" />
48
+ <TabsSubtle.Item index={3} icon={Users} label="Shared" />
49
+ </TabsSubtle>