@justanarthur/modo-ui 0.6.0-canary.9 → 0.6.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 (490) hide show
  1. package/README.md +66 -164
  2. package/dist/base.css +4 -141
  3. package/dist/blocks/sidebar-app/index.js +26 -40
  4. package/dist/components/AI/input-message/index.js +16 -23
  5. package/dist/components/AI/thinking-indicator/index.js +6 -9
  6. package/dist/components/accordion/index.d.ts +11 -39
  7. package/dist/components/accordion/index.js +13 -18
  8. package/dist/components/badge/index.d.ts +11 -18
  9. package/dist/components/badge/index.js +7 -12
  10. package/dist/components/button/index.d.ts +17 -53
  11. package/dist/components/button/index.js +6 -11
  12. package/dist/components/card/index.d.ts +9 -32
  13. package/dist/components/card/index.js +11 -17
  14. package/dist/components/copy/index.d.ts +8 -41
  15. package/dist/components/copy/index.js +9 -15
  16. package/dist/components/inputs/checkbox/index.d.ts +9 -28
  17. package/dist/components/inputs/checkbox/index.js +13 -18
  18. package/dist/components/inputs/color-picker/index.d.ts +14 -22
  19. package/dist/components/inputs/color-picker/index.js +18 -26
  20. package/dist/components/inputs/combobox/index.d.ts +20 -85
  21. package/dist/components/inputs/combobox/index.js +18 -27
  22. package/dist/components/inputs/date-time/calendar/index.d.ts +18 -24
  23. package/dist/components/inputs/date-time/calendar/index.js +11 -15
  24. package/dist/components/inputs/date-time/date-picker/index.d.ts +16 -105
  25. package/dist/components/inputs/date-time/date-picker/index.js +25 -35
  26. package/dist/components/inputs/date-time/date-range-picker/index.d.ts +12 -106
  27. package/dist/components/inputs/date-time/date-range-picker/index.js +24 -35
  28. package/dist/components/inputs/date-time/duration-picker/index.d.ts +13 -66
  29. package/dist/components/inputs/date-time/duration-picker/index.js +24 -33
  30. package/dist/components/inputs/date-time/month-picker/index.d.ts +10 -60
  31. package/dist/components/inputs/date-time/month-picker/index.js +23 -32
  32. package/dist/components/inputs/date-time/time-picker/index.d.ts +10 -58
  33. package/dist/components/inputs/date-time/time-picker/index.js +24 -33
  34. package/dist/components/inputs/input/index.d.ts +13 -95
  35. package/dist/components/inputs/input/index.js +9 -18
  36. package/dist/components/inputs/radio/index.d.ts +5 -20
  37. package/dist/components/inputs/radio/index.js +12 -16
  38. package/dist/components/inputs/search/index.d.ts +3 -3
  39. package/dist/components/inputs/search/index.js +18 -26
  40. package/dist/components/inputs/select/index.d.ts +23 -99
  41. package/dist/components/inputs/select/index.js +14 -29
  42. package/dist/components/inputs/slider/index.d.ts +0 -9
  43. package/dist/components/inputs/slider/index.js +5 -8
  44. package/dist/components/inputs/switch/index.d.ts +2 -8
  45. package/dist/components/inputs/switch/index.js +12 -15
  46. package/dist/components/inputs/textarea/index.d.ts +1 -20
  47. package/dist/components/inputs/textarea/index.js +6 -9
  48. package/dist/components/navigation/sidebar/index.d.ts +21 -44
  49. package/dist/components/navigation/sidebar/index.js +31 -42
  50. package/dist/components/navigation/tabs/index.d.ts +2 -8
  51. package/dist/components/navigation/tabs/index.js +10 -15
  52. package/dist/components/navigation/tabs-subtle/index.js +10 -13
  53. package/dist/components/overlays/alert-dialog/index.d.ts +16 -135
  54. package/dist/components/overlays/alert-dialog/index.js +13 -23
  55. package/dist/components/overlays/command/index.d.ts +20 -25
  56. package/dist/components/overlays/command/index.js +17 -25
  57. package/dist/components/overlays/context-menu/index.d.ts +17 -39
  58. package/dist/components/overlays/context-menu/index.js +18 -29
  59. package/dist/components/overlays/dialog/index.d.ts +16 -42
  60. package/dist/components/overlays/dialog/index.js +12 -20
  61. package/dist/components/overlays/dropdown/index.d.ts +34 -304
  62. package/dist/components/overlays/dropdown/index.js +17 -44
  63. package/dist/components/overlays/popover/index.d.ts +12 -38
  64. package/dist/components/overlays/popover/index.js +9 -13
  65. package/dist/components/overlays/preview-card/index.js +8 -12
  66. package/dist/components/overlays/sheet/index.d.ts +8 -25
  67. package/dist/components/overlays/sheet/index.js +13 -19
  68. package/dist/components/overlays/toast/index.d.ts +50 -209
  69. package/dist/components/overlays/toast/index.js +10 -23
  70. package/dist/components/overlays/tooltip/index.d.ts +21 -123
  71. package/dist/components/overlays/tooltip/index.js +6 -16
  72. package/dist/components/table/index.d.ts +8 -63
  73. package/dist/components/table/index.js +9 -20
  74. package/dist/index.d.ts +1048 -2999
  75. package/dist/index.js +82 -231
  76. package/dist/preset.d.ts +0 -22
  77. package/dist/preset.js +2 -24
  78. package/dist/primitives/code/index.js +5 -8
  79. package/dist/primitives/craft/index.js +6 -10
  80. package/dist/primitives/fluid-hover/index.js +8 -10
  81. package/dist/primitives/morph/index.js +10 -16
  82. package/dist/primitives/motion/index.js +8 -11
  83. package/dist/primitives/scroll-area/index.js +3 -5
  84. package/dist/primitives/sizes/index.js +4 -6
  85. package/dist/primitives/surface/index.js +4 -6
  86. package/dist/shared/{components-c7rwx0ap.js → components-0q0sqfh4.js} +49 -27
  87. package/dist/shared/components-0sn7ypjw.js +61 -0
  88. package/dist/shared/{components-vc864c51.js → components-11da9ajg.js} +5 -8
  89. package/dist/shared/components-1434p1mh.js +123 -0
  90. package/dist/shared/{components-c74gf16d.js → components-1z6tdadk.js} +24 -40
  91. package/dist/shared/{components-bae8sbjx.js → components-27nnvrwe.js} +14 -15
  92. package/dist/shared/{components-qh1cvj6p.js → components-2dxkv6zm.js} +7 -8
  93. package/dist/shared/{components-y0kfy2g4.js → components-2ecg8svx.js} +152 -234
  94. package/dist/shared/{components-c5w5zk0s.js → components-2ph6t61s.js} +34 -37
  95. package/dist/shared/{components-4j3t39dd.js → components-39jq9va1.js} +24 -29
  96. package/dist/shared/components-3ky44908.js +9 -0
  97. package/dist/shared/{components-syynwm1r.js → components-3mkepka5.js} +25 -28
  98. package/dist/shared/{components-wgtjt2af.js → components-4700p9kz.js} +22 -24
  99. package/dist/shared/{components-yhgq0qxx.js → components-59efxjd9.js} +12 -13
  100. package/dist/shared/{components-91zw3d9f.js → components-5vgpnxgy.js} +27 -51
  101. package/dist/shared/{components-kz5q11pp.js → components-5xc2j8jz.js} +18 -19
  102. package/dist/shared/{components-zkk3fmw8.js → components-6p8tfcd6.js} +29 -57
  103. package/dist/shared/{components-4nck8acq.js → components-7r3etddy.js} +54 -19
  104. package/dist/shared/{components-svkt577j.js → components-8snj490h.js} +32 -78
  105. package/dist/shared/{components-ctqmge8t.js → components-b0eac83j.js} +30 -9
  106. package/dist/shared/{components-9sttq78t.js → components-b2wyq2cq.js} +59 -41
  107. package/dist/shared/{components-pv1dyrtn.js → components-b3c5et5b.js} +38 -49
  108. package/dist/shared/{components-rrbpmfx4.js → components-c6s6b43g.js} +2 -3
  109. package/dist/shared/{components-pwayrkw9.js → components-chacnk61.js} +19 -19
  110. package/dist/shared/{components-71nbt03m.js → components-cv16mryv.js} +7 -26
  111. package/dist/shared/{components-hgv6tygd.js → components-d6xtjht3.js} +30 -35
  112. package/dist/shared/{components-z8fedth5.js → components-dk0gjr5a.js} +18 -19
  113. package/dist/shared/{components-cet3jpa0.js → components-fzrq6tr3.js} +34 -37
  114. package/dist/shared/{components-cnead5qm.js → components-gc5vtq2f.js} +30 -66
  115. package/dist/shared/components-gf5s6ap9.js +42 -0
  116. package/dist/shared/{components-0h78g3b5.js → components-gz8zf3ec.js} +55 -56
  117. package/dist/shared/{components-mattt5jp.js → components-h2ajmhwn.js} +35 -107
  118. package/dist/shared/{components-m06yq8wk.js → components-hqeq3w9q.js} +23 -38
  119. package/dist/shared/{components-7qx17na9.js → components-hw9yk32z.js} +24 -36
  120. package/dist/shared/components-hyq23phv.js +310 -0
  121. package/dist/shared/{components-9jmjnkqd.js → components-jbbmdr74.js} +22 -41
  122. package/dist/shared/{components-dcc02hs2.js → components-jjw69c4c.js} +4 -7
  123. package/dist/shared/{components-303jf96s.js → components-jthe1pwb.js} +420 -139
  124. package/dist/shared/{components-dtca4s6b.js → components-jwst3vtj.js} +76 -224
  125. package/dist/shared/{components-dc5q749x.js → components-kvr7f9hy.js} +10 -19
  126. package/dist/shared/{components-s8yqk2dc.js → components-kwa6mrf9.js} +11 -46
  127. package/dist/shared/{components-de99ykt7.js → components-men63rwj.js} +2 -3
  128. package/dist/shared/{components-qx27excf.js → components-mjthm1kf.js} +9 -13
  129. package/dist/shared/{components-1e64t8aj.js → components-n42c79bz.js} +22 -23
  130. package/dist/shared/{components-tz1fv35x.js → components-n7mjr357.js} +2 -3
  131. package/dist/shared/components-ng72k97e.js +128 -0
  132. package/dist/shared/{components-w2jqa6hx.js → components-pbmqctfa.js} +65 -169
  133. package/dist/shared/{components-px2zm7dc.js → components-q5xj5c16.js} +8 -9
  134. package/dist/shared/components-raknhppb.js +64 -0
  135. package/dist/shared/{components-2a7hcand.js → components-rr7a6ja9.js} +27 -59
  136. package/dist/shared/{components-005jt2nz.js → components-s083f38z.js} +34 -164
  137. package/dist/shared/components-s5ngg32c.js +267 -0
  138. package/dist/shared/components-sn1rxx1d.js +143 -0
  139. package/dist/shared/{components-r8eqs7gz.js → components-ta7shw28.js} +4 -4
  140. package/dist/shared/{components-9as3rf68.js → components-vc7vq5nz.js} +3 -6
  141. package/dist/shared/{components-ft3zdc79.js → components-vfbhxf5r.js} +5 -6
  142. package/dist/shared/components-vynjtrk6.js +19 -0
  143. package/dist/shared/{components-p0fm0bab.js → components-w1r67ag9.js} +149 -294
  144. package/dist/shared/{components-xd3emmy4.js → components-w90endve.js} +72 -71
  145. package/dist/shared/{components-t5pk5nmc.js → components-wvp1m52g.js} +102 -149
  146. package/dist/shared/components-wy9jpqny.js +146 -0
  147. package/dist/shared/{components-yxmxn2jp.js → components-y9y0g8er.js} +28 -52
  148. package/dist/shared/components-ypp01w3p.js +986 -0
  149. package/dist/shared/{components-czdvj1a8.js → components-zfnmn5d4.js} +36 -82
  150. package/dist/shared/{components-jqpefjsn.js → components-zrkzcayz.js} +6 -54
  151. package/dist/shared/{components-vf45j12c.js → components-zx6b6yby.js} +18 -4
  152. package/dist/styles.css +32 -320
  153. package/dist/tailwind.css +0 -16
  154. package/dist/tokens.css +0 -67
  155. package/package.json +4 -227
  156. package/DESIGN.md +0 -623
  157. package/blocks/sidebar-app/examples.mdx +0 -33
  158. package/blocks/sidebar-app/index.tsx +0 -218
  159. package/blocks/sidebar-app/inset-topbar.tsx +0 -25
  160. package/blocks/sidebar-app/menu-grid.ts +0 -26
  161. package/blocks/sidebar-app/nav-data.ts +0 -36
  162. package/blocks/sidebar-app/search-field.tsx +0 -48
  163. package/blocks/sidebar-app/user-footer.tsx +0 -67
  164. package/blocks/sidebar-app/workspace-header.tsx +0 -129
  165. package/components/AI/input-message/examples.mdx +0 -130
  166. package/components/AI/input-message/file-thumbnail.tsx +0 -173
  167. package/components/AI/input-message/index.tsx +0 -1385
  168. package/components/AI/thinking-indicator/examples.mdx +0 -27
  169. package/components/AI/thinking-indicator/index.tsx +0 -210
  170. package/components/accordion/examples.mdx +0 -233
  171. package/components/accordion/index.tsx +0 -1071
  172. package/components/avatar/examples.mdx +0 -95
  173. package/components/avatar/index.tsx +0 -129
  174. package/components/badge/change-demo.tsx +0 -29
  175. package/components/badge/examples.mdx +0 -124
  176. package/components/badge/index.tsx +0 -225
  177. package/components/badge/variants.ts +0 -73
  178. package/components/button/examples.mdx +0 -168
  179. package/components/button/index.tsx +0 -424
  180. package/components/button/loading-demo.tsx +0 -25
  181. package/components/button/variants.ts +0 -168
  182. package/components/card/examples.mdx +0 -251
  183. package/components/card/index.tsx +0 -1070
  184. package/components/copy/examples.mdx +0 -125
  185. package/components/copy/index.tsx +0 -662
  186. package/components/inputs/checkbox/examples.mdx +0 -87
  187. package/components/inputs/checkbox/index.tsx +0 -611
  188. package/components/inputs/checkbox/select-all-demo.tsx +0 -48
  189. package/components/inputs/color-picker/examples.mdx +0 -111
  190. package/components/inputs/color-picker/index.tsx +0 -1981
  191. package/components/inputs/color-picker/variants.ts +0 -401
  192. package/components/inputs/combobox/examples.mdx +0 -366
  193. package/components/inputs/combobox/index.tsx +0 -1527
  194. package/components/inputs/combobox/remote-demo.tsx +0 -105
  195. package/components/inputs/date-time/calendar/examples.mdx +0 -45
  196. package/components/inputs/date-time/calendar/index.tsx +0 -370
  197. package/components/inputs/date-time/calendar/use-month-window.ts +0 -173
  198. package/components/inputs/date-time/date-picker/examples.mdx +0 -243
  199. package/components/inputs/date-time/date-picker/index.tsx +0 -476
  200. package/components/inputs/date-time/date-picker/masked-date-input.tsx +0 -348
  201. package/components/inputs/date-time/date-range-picker/examples.mdx +0 -93
  202. package/components/inputs/date-time/date-range-picker/index.tsx +0 -525
  203. package/components/inputs/date-time/duration-picker/examples.mdx +0 -40
  204. package/components/inputs/date-time/duration-picker/index.tsx +0 -253
  205. package/components/inputs/date-time/month-picker/examples.mdx +0 -45
  206. package/components/inputs/date-time/month-picker/index.tsx +0 -254
  207. package/components/inputs/date-time/picker-shell.tsx +0 -581
  208. package/components/inputs/date-time/time-columns.tsx +0 -94
  209. package/components/inputs/date-time/time-picker/examples.mdx +0 -57
  210. package/components/inputs/date-time/time-picker/index.tsx +0 -227
  211. package/components/inputs/field/examples.mdx +0 -117
  212. package/components/inputs/field/index.tsx +0 -268
  213. package/components/inputs/file-dropzone/examples.mdx +0 -83
  214. package/components/inputs/file-dropzone/file-row.tsx +0 -201
  215. package/components/inputs/file-dropzone/index.tsx +0 -316
  216. package/components/inputs/file-dropzone/upload-demo.tsx +0 -67
  217. package/components/inputs/file-dropzone/variants.ts +0 -94
  218. package/components/inputs/input/examples.mdx +0 -166
  219. package/components/inputs/input/index.tsx +0 -494
  220. package/components/inputs/input/password-demo.tsx +0 -30
  221. package/components/inputs/label/examples.mdx +0 -67
  222. package/components/inputs/label/index.tsx +0 -85
  223. package/components/inputs/number-input/examples.mdx +0 -69
  224. package/components/inputs/number-input/index.tsx +0 -221
  225. package/components/inputs/radio/examples.mdx +0 -77
  226. package/components/inputs/radio/index.tsx +0 -636
  227. package/components/inputs/search/examples.mdx +0 -74
  228. package/components/inputs/search/index.tsx +0 -333
  229. package/components/inputs/select/examples.mdx +0 -296
  230. package/components/inputs/select/index.tsx +0 -1044
  231. package/components/inputs/select/variants.ts +0 -32
  232. package/components/inputs/slider/examples.mdx +0 -153
  233. package/components/inputs/slider/index.tsx +0 -1867
  234. package/components/inputs/switch/examples.mdx +0 -96
  235. package/components/inputs/switch/index.tsx +0 -504
  236. package/components/inputs/textarea/examples.mdx +0 -126
  237. package/components/inputs/textarea/index.tsx +0 -207
  238. package/components/navigation/navigation-menu/examples.mdx +0 -136
  239. package/components/navigation/navigation-menu/index.tsx +0 -704
  240. package/components/navigation/pagination/examples.mdx +0 -74
  241. package/components/navigation/pagination/index.tsx +0 -401
  242. package/components/navigation/sidebar/examples.mdx +0 -325
  243. package/components/navigation/sidebar/index.tsx +0 -453
  244. package/components/navigation/sidebar/sidebar-core.tsx +0 -1408
  245. package/components/navigation/sidebar/sidebar-menu.tsx +0 -1495
  246. package/components/navigation/sidebar/sidebar.css +0 -74
  247. package/components/navigation/sidebar/variants.ts +0 -55
  248. package/components/navigation/tabs/examples.mdx +0 -89
  249. package/components/navigation/tabs/index.tsx +0 -494
  250. package/components/navigation/tabs-subtle/examples.mdx +0 -49
  251. package/components/navigation/tabs-subtle/index.tsx +0 -512
  252. package/components/overlays/alert-dialog/examples.mdx +0 -123
  253. package/components/overlays/alert-dialog/index.tsx +0 -276
  254. package/components/overlays/alert-dialog/pending-demo.tsx +0 -36
  255. package/components/overlays/command/examples.mdx +0 -113
  256. package/components/overlays/command/index.tsx +0 -425
  257. package/components/overlays/context-menu/examples.mdx +0 -56
  258. package/components/overlays/context-menu/index.tsx +0 -279
  259. package/components/overlays/dialog/examples.mdx +0 -165
  260. package/components/overlays/dialog/index.tsx +0 -451
  261. package/components/overlays/dropdown/dropdown-search.tsx +0 -395
  262. package/components/overlays/dropdown/examples.mdx +0 -444
  263. package/components/overlays/dropdown/index.tsx +0 -1241
  264. package/components/overlays/dropdown/menu-item.tsx +0 -510
  265. package/components/overlays/dropdown/menu-parts.tsx +0 -725
  266. package/components/overlays/popover/anchored-demo.tsx +0 -59
  267. package/components/overlays/popover/examples.mdx +0 -203
  268. package/components/overlays/popover/index.tsx +0 -317
  269. package/components/overlays/preview-card/examples.mdx +0 -61
  270. package/components/overlays/preview-card/index.tsx +0 -204
  271. package/components/overlays/sheet/examples.mdx +0 -101
  272. package/components/overlays/sheet/index.tsx +0 -307
  273. package/components/overlays/toast/examples.mdx +0 -85
  274. package/components/overlays/toast/global-demo.tsx +0 -72
  275. package/components/overlays/toast/index.tsx +0 -544
  276. package/components/overlays/toast/manager.ts +0 -193
  277. package/components/overlays/tooltip/examples.mdx +0 -106
  278. package/components/overlays/tooltip/index.tsx +0 -539
  279. package/components/progress/examples.mdx +0 -59
  280. package/components/progress/index.tsx +0 -174
  281. package/components/progress/progress.css +0 -49
  282. package/components/progress/step-demo.tsx +0 -27
  283. package/components/table/examples.mdx +0 -188
  284. package/components/table/index.tsx +0 -535
  285. package/components/table/sort-demo.tsx +0 -61
  286. package/dist/components/avatar/index.d.ts +0 -46
  287. package/dist/components/avatar/index.js +0 -19
  288. package/dist/components/badge/variants.d.ts +0 -33
  289. package/dist/components/badge/variants.js +0 -10
  290. package/dist/components/button/variants.d.ts +0 -39
  291. package/dist/components/button/variants.js +0 -8
  292. package/dist/components/inputs/color-picker/variants.d.ts +0 -61
  293. package/dist/components/inputs/color-picker/variants.js +0 -28
  294. package/dist/components/inputs/field/index.d.ts +0 -116
  295. package/dist/components/inputs/field/index.js +0 -29
  296. package/dist/components/inputs/file-dropzone/index.d.ts +0 -167
  297. package/dist/components/inputs/file-dropzone/index.js +0 -33
  298. package/dist/components/inputs/file-dropzone/variants.d.ts +0 -35
  299. package/dist/components/inputs/file-dropzone/variants.js +0 -12
  300. package/dist/components/inputs/label/index.d.ts +0 -32
  301. package/dist/components/inputs/label/index.js +0 -11
  302. package/dist/components/inputs/number-input/index.d.ts +0 -87
  303. package/dist/components/inputs/number-input/index.js +0 -21
  304. package/dist/components/inputs/select/variants.d.ts +0 -5
  305. package/dist/components/inputs/select/variants.js +0 -6
  306. package/dist/components/navigation/navigation-menu/index.d.ts +0 -126
  307. package/dist/components/navigation/navigation-menu/index.js +0 -41
  308. package/dist/components/navigation/pagination/index.d.ts +0 -75
  309. package/dist/components/navigation/pagination/index.js +0 -41
  310. package/dist/components/navigation/sidebar/variants.d.ts +0 -25
  311. package/dist/components/navigation/sidebar/variants.js +0 -28
  312. package/dist/components/progress/index.d.ts +0 -56
  313. package/dist/components/progress/index.js +0 -15
  314. package/dist/docs/index.d.ts +0 -83
  315. package/dist/docs/index.js +0 -288
  316. package/dist/docs/vite.d.ts +0 -31
  317. package/dist/docs/vite.js +0 -45
  318. package/dist/lib/date-locale.d.ts +0 -26
  319. package/dist/lib/date-locale.js +0 -24
  320. package/dist/lib/field-classes.d.ts +0 -149
  321. package/dist/lib/field-classes.js +0 -41
  322. package/dist/lib/field-variants.d.ts +0 -47
  323. package/dist/lib/field-variants.js +0 -26
  324. package/dist/lib/fluid-hover-highlight.d.ts +0 -112
  325. package/dist/lib/fluid-hover-highlight.js +0 -17
  326. package/dist/lib/focus-ring.d.ts +0 -18
  327. package/dist/lib/focus-ring.js +0 -11
  328. package/dist/lib/goo-indicator.d.ts +0 -46
  329. package/dist/lib/goo-indicator.js +0 -14
  330. package/dist/lib/icon-context.d.ts +0 -30
  331. package/dist/lib/icon-context.js +0 -12
  332. package/dist/lib/inert.d.ts +0 -9
  333. package/dist/lib/inert.js +0 -8
  334. package/dist/lib/morph-layers.d.ts +0 -93
  335. package/dist/lib/morph-layers.js +0 -13
  336. package/dist/lib/morph-part.d.ts +0 -21
  337. package/dist/lib/morph-part.js +0 -12
  338. package/dist/lib/popup.d.ts +0 -20
  339. package/dist/lib/popup.js +0 -12
  340. package/dist/lib/reduced-motion.d.ts +0 -9
  341. package/dist/lib/reduced-motion.js +0 -6
  342. package/dist/lib/row-index.d.ts +0 -10
  343. package/dist/lib/row-index.js +0 -8
  344. package/dist/lib/row-order.d.ts +0 -28
  345. package/dist/lib/row-order.js +0 -10
  346. package/dist/lib/scroll-column.d.ts +0 -31
  347. package/dist/lib/scroll-column.js +0 -19
  348. package/dist/lib/shape-context.d.ts +0 -30
  349. package/dist/lib/shape-context.js +0 -13
  350. package/dist/lib/shape-radius.d.ts +0 -19
  351. package/dist/lib/shape-radius.js +0 -6
  352. package/dist/lib/size-context.d.ts +0 -94
  353. package/dist/lib/size-context.js +0 -20
  354. package/dist/lib/size-scale.d.ts +0 -79
  355. package/dist/lib/size-scale.js +0 -8
  356. package/dist/lib/slot.d.ts +0 -39
  357. package/dist/lib/slot.js +0 -14
  358. package/dist/lib/springs.d.ts +0 -44
  359. package/dist/lib/springs.js +0 -8
  360. package/dist/lib/surface-classes.d.ts +0 -19
  361. package/dist/lib/surface-classes.js +0 -10
  362. package/dist/lib/surface-context.d.ts +0 -9
  363. package/dist/lib/surface-context.js +0 -8
  364. package/dist/lib/use-controllable-state.d.ts +0 -2
  365. package/dist/lib/use-controllable-state.js +0 -6
  366. package/dist/lib/use-fluid-hover.d.ts +0 -138
  367. package/dist/lib/use-fluid-hover.js +0 -14
  368. package/dist/lib/use-is-mobile.d.ts +0 -2
  369. package/dist/lib/use-is-mobile.js +0 -6
  370. package/dist/lib/use-keyboard-nav-gate.d.ts +0 -20
  371. package/dist/lib/use-keyboard-nav-gate.js +0 -7
  372. package/dist/lib/use-merge-split.d.ts +0 -47
  373. package/dist/lib/use-merge-split.js +0 -15
  374. package/dist/lib/use-morph.d.ts +0 -101
  375. package/dist/lib/use-morph.js +0 -20
  376. package/dist/lib/use-touch-primary.d.ts +0 -2
  377. package/dist/lib/use-touch-primary.js +0 -6
  378. package/dist/lib/utils.d.ts +0 -3
  379. package/dist/lib/utils.js +0 -6
  380. package/dist/primitives/separator/index.d.ts +0 -25
  381. package/dist/primitives/separator/index.js +0 -9
  382. package/dist/primitives/skeleton/index.d.ts +0 -35
  383. package/dist/primitives/skeleton/index.js +0 -13
  384. package/dist/shared/components-01w9qcfa.js +0 -277
  385. package/dist/shared/components-124epb7f.js +0 -53
  386. package/dist/shared/components-26snnbs9.js +0 -322
  387. package/dist/shared/components-2krsc300.js +0 -254
  388. package/dist/shared/components-2qbqqpgq.js +0 -1545
  389. package/dist/shared/components-34txk9rj.js +0 -35
  390. package/dist/shared/components-3h92d94m.js +0 -23
  391. package/dist/shared/components-4vfm04wq.js +0 -82
  392. package/dist/shared/components-54nq988h.js +0 -123
  393. package/dist/shared/components-7s7pykk5.js +0 -281
  394. package/dist/shared/components-868v7scv.js +0 -399
  395. package/dist/shared/components-8jrk5eye.js +0 -448
  396. package/dist/shared/components-adkarj8n.js +0 -35
  397. package/dist/shared/components-b1e239g3.js +0 -28
  398. package/dist/shared/components-b94t6qtj.js +0 -143
  399. package/dist/shared/components-bsqynhaj.js +0 -179
  400. package/dist/shared/components-c9yr4ed1.js +0 -44
  401. package/dist/shared/components-d3shbwqq.js +0 -95
  402. package/dist/shared/components-dhgk6mbt.js +0 -16
  403. package/dist/shared/components-drxrtnp2.js +0 -25
  404. package/dist/shared/components-f7h57m6t.js +0 -59
  405. package/dist/shared/components-g3eafs70.js +0 -337
  406. package/dist/shared/components-gx4h5rbb.js +0 -40
  407. package/dist/shared/components-gyt7my41.js +0 -328
  408. package/dist/shared/components-h6p86be3.js +0 -19
  409. package/dist/shared/components-h83az0bz.js +0 -33
  410. package/dist/shared/components-hdzrqqpx.js +0 -55
  411. package/dist/shared/components-krtx3m5b.js +0 -84
  412. package/dist/shared/components-kswxnm3h.js +0 -24
  413. package/dist/shared/components-mhza2wdn.js +0 -482
  414. package/dist/shared/components-mrbaxt8q.js +0 -62
  415. package/dist/shared/components-pt5ftm4p.js +0 -38
  416. package/dist/shared/components-rtadhd9j.js +0 -64
  417. package/dist/shared/components-tv7b8tet.js +0 -271
  418. package/dist/shared/components-tz4gaf1v.js +0 -23
  419. package/dist/shared/components-wbbckbw8.js +0 -34
  420. package/dist/shared/components-wknae3m4.js +0 -298
  421. package/dist/shared/components-xdqmt6ed.js +0 -20
  422. package/dist/shared/components-xjkdvnbg.js +0 -107
  423. package/dist/shared/components-z3v0n0de.js +0 -11
  424. package/dist/tokens/colors.css +0 -230
  425. package/dist/tokens/motion.css +0 -40
  426. package/dist/tokens/radius.css +0 -10
  427. package/dist/tokens/spacing.css +0 -9
  428. package/dist/tokens/typography.css +0 -119
  429. package/docs/chrome.css +0 -120
  430. package/docs/chrome.tsx +0 -283
  431. package/docs/index.ts +0 -9
  432. package/docs/vite.ts +0 -85
  433. package/index.ts +0 -60
  434. package/lib/date-locale.ts +0 -195
  435. package/lib/field-classes.ts +0 -121
  436. package/lib/field-variants.ts +0 -117
  437. package/lib/fluid-hover-highlight.tsx +0 -139
  438. package/lib/focus-ring.tsx +0 -46
  439. package/lib/goo-indicator.tsx +0 -261
  440. package/lib/icon-context.tsx +0 -310
  441. package/lib/inert.ts +0 -22
  442. package/lib/morph-layers.tsx +0 -102
  443. package/lib/morph-part.tsx +0 -49
  444. package/lib/popup.ts +0 -52
  445. package/lib/reduced-motion.ts +0 -19
  446. package/lib/row-index.tsx +0 -29
  447. package/lib/row-order.tsx +0 -126
  448. package/lib/scroll-column.tsx +0 -273
  449. package/lib/shape-context.tsx +0 -138
  450. package/lib/shape-radius.ts +0 -67
  451. package/lib/size-context.tsx +0 -66
  452. package/lib/size-scale.ts +0 -115
  453. package/lib/slot.ts +0 -115
  454. package/lib/springs.ts +0 -62
  455. package/lib/surface-classes.ts +0 -58
  456. package/lib/surface-context.tsx +0 -21
  457. package/lib/use-controllable-state.ts +0 -44
  458. package/lib/use-fluid-hover.ts +0 -541
  459. package/lib/use-is-mobile.ts +0 -25
  460. package/lib/use-keyboard-nav-gate.ts +0 -45
  461. package/lib/use-merge-split.tsx +0 -209
  462. package/lib/use-morph.ts +0 -575
  463. package/lib/use-touch-primary.tsx +0 -43
  464. package/lib/utils.ts +0 -37
  465. package/preset.ts +0 -199
  466. package/primitives/code/examples.mdx +0 -27
  467. package/primitives/code/index.tsx +0 -80
  468. package/primitives/craft/demos.tsx +0 -173
  469. package/primitives/craft/examples.mdx +0 -142
  470. package/primitives/craft/index.tsx +0 -76
  471. package/primitives/fluid-hover/examples.mdx +0 -84
  472. package/primitives/fluid-hover/index.tsx +0 -316
  473. package/primitives/morph/examples.mdx +0 -139
  474. package/primitives/morph/index.tsx +0 -192
  475. package/primitives/motion/examples.mdx +0 -100
  476. package/primitives/motion/index.tsx +0 -160
  477. package/primitives/scroll-area/examples.mdx +0 -58
  478. package/primitives/scroll-area/index.tsx +0 -204
  479. package/primitives/separator/examples.mdx +0 -30
  480. package/primitives/separator/index.tsx +0 -53
  481. package/primitives/sizes/examples.mdx +0 -167
  482. package/primitives/sizes/index.tsx +0 -89
  483. package/primitives/skeleton/examples.mdx +0 -51
  484. package/primitives/skeleton/index.tsx +0 -72
  485. package/primitives/skeleton/skeleton.css +0 -15
  486. package/primitives/surface/examples.mdx +0 -173
  487. package/primitives/surface/index.tsx +0 -109
  488. package/primitives/surface/surface.css +0 -176
  489. /package/dist/shared/{components-3ey0hzpb.js → components-1v7rxytx.js} +0 -0
  490. /package/dist/shared/{components-jcnjay5g.js → components-ezxhrmer.js} +0 -0
@@ -1,1408 +0,0 @@
1
- // biome-ignore-all lint: vendored upstream code keeps its own patterns (see the header)
2
- /*
3
- * Ported from Fluid Functionalism — github.com/mickadesign/fluid-functionalism
4
- * `registry/default/sidebar-core.tsx` @ b3587bdbd83fc66c2a6aae3817ffb856cb09260b
5
- * MIT License © 2026 Micka Touillaud — fluidfunctionalism.com (notice: LICENSE.fluid-functionalism)
6
- * Local modifications:
7
- * - `"use client"` dropped; `framer-motion` → `motion/react`; `@/lib/*`
8
- * imports rewritten to `../../../lib/*`, `@/components/ui/{button,tooltip}`
9
- * to the Base siblings `../../button` / `../../overlays/tooltip`.
10
- * - Styling reads DS tokens (AGENTS.md styling): inline
11
- * `fontVariationSettings` → `weight-*`; `text-[10px]` →
12
- * `text-micro-compact`, the group label's `text-[11px]` / `text-[12px]` →
13
- * `text-caption-compact` / `text-caption`; the hex focus-ring fallback →
14
- * `ring-focus-ring`; `duration-80|160|240` → `duration-<tier>`.
15
- * - No collapsed peek (the card floated out of the edge strip, `peek`):
16
- * `openOnHover` on the provider opens the sidebar itself, in flow, while
17
- * the pointer is between its edge and the far side of a trigger beside it;
18
- * the trigger then switches between open and on hover (dashed glyph), and
19
- * the rail collapses or resizes the hover without keeping it open.
20
- * - What is folded or collapsed away is `inert`: a folded `SidebarGroup`, and
21
- * the rail panel of a collapsed sidebar. Their rows were still
22
- * tab stops while clipped away (React 18 has no `inert` prop type, hence
23
- * the spreads). `inertProps` (`lib/inert.ts`) picks the value per React
24
- * major: 18 passes `inert` through as an unknown attribute (a string), 19
25
- * reads it as a boolean prop and drops `''`.
26
- * - `SidebarInput`'s field ladder is the shared `lib/field-classes.ts` recipe.
27
- * - `useIsMobile` moved to `lib/use-is-mobile.ts`, shared with the pickers.
28
- * - The `SIDEBAR_*` constants live in `./variants.ts` (no JSX, hooks or
29
- * contexts, so server code can read them) and are re-exported here.
30
- */
31
-
32
- import { motion, useReducedMotion } from 'motion/react'
33
- import {
34
- Children,
35
- type CSSProperties,
36
- cloneElement,
37
- createContext,
38
- type ElementType,
39
- forwardRef,
40
- type HTMLAttributes,
41
- isValidElement,
42
- type MutableRefObject,
43
- type ReactElement,
44
- type ReactNode,
45
- type Ref,
46
- useCallback,
47
- useContext,
48
- useEffect,
49
- useId,
50
- useLayoutEffect,
51
- useMemo,
52
- useRef,
53
- useState,
54
- } from 'react'
55
- import { fieldVariants } from '../../../lib/field-classes'
56
- import { useIcon } from '../../../lib/icon-context'
57
- import { inertProps } from '../../../lib/inert'
58
- import { useShape } from '../../../lib/shape-context'
59
- import { useSize, useSizeVariant } from '../../../lib/size-context'
60
- import { exitFallbackMs, spring } from '../../../lib/springs'
61
- import { surfaceClasses } from '../../../lib/surface-classes'
62
- import { SurfaceProvider, useSurface } from '../../../lib/surface-context'
63
- import { useIsMobile } from '../../../lib/use-is-mobile'
64
- import { cn } from '../../../lib/utils'
65
- import { Button, type ButtonProps } from '../../button'
66
- import { Tooltip } from '../../overlays/tooltip'
67
- import {
68
- SIDEBAR_COLLAPSE_SLOP,
69
- SIDEBAR_COOKIE_MAX_AGE,
70
- SIDEBAR_COOKIE_NAME,
71
- SIDEBAR_HOVER_STRIP,
72
- SIDEBAR_HOVER_TRIGGER_GAP,
73
- SIDEBAR_KEYBOARD_SHORTCUT,
74
- SIDEBAR_KEYBOARD_SHORTCUT_RIGHT,
75
- SIDEBAR_MAX_WIDTH,
76
- SIDEBAR_MIN_WIDTH,
77
- SIDEBAR_WIDTH,
78
- SIDEBAR_WIDTH_MOBILE,
79
- } from './variants'
80
-
81
- // ─── Constants ───────────────────────────────────────────────────────────────
82
-
83
- // Plain values, in `./variants.ts` so server code can read them.
84
- export {
85
- SIDEBAR_COLLAPSE_SLOP,
86
- SIDEBAR_COOKIE_MAX_AGE,
87
- SIDEBAR_COOKIE_NAME,
88
- SIDEBAR_HOVER_STRIP,
89
- SIDEBAR_HOVER_TRIGGER_GAP,
90
- SIDEBAR_KEYBOARD_SHORTCUT,
91
- SIDEBAR_KEYBOARD_SHORTCUT_RIGHT,
92
- SIDEBAR_MAX_WIDTH,
93
- SIDEBAR_MIN_WIDTH,
94
- SIDEBAR_WIDTH,
95
- SIDEBAR_WIDTH_MOBILE,
96
- } from './variants'
97
-
98
- // ─── Context ─────────────────────────────────────────────────────────────────
99
-
100
- export type SidebarSide = 'left' | 'right'
101
- export type SidebarVariant = 'sidebar' | 'floating' | 'inset'
102
- export type SidebarCollapsible = 'offcanvas' | 'none'
103
-
104
- export interface SidebarContextValue {
105
- /** What shows: expanded while kept open or open on hover. */
106
- state: 'expanded' | 'collapsed'
107
- /** Kept open. */
108
- open: boolean
109
- setOpen: (open: boolean | ((prev: boolean) => boolean)) => void
110
- openMobile: boolean
111
- setOpenMobile: React.Dispatch<React.SetStateAction<boolean>>
112
- isMobile: boolean
113
- toggleSidebar: () => void
114
- /** Live rail width — the rail's drag-resize updates it. */
115
- width: string
116
- setWidth: (width: string) => void
117
- widthMobile: string
118
- mobileBreakpoint: number
119
- /** Which edge the rendered Sidebar sits on (registered by <Sidebar/>). */
120
- side: SidebarSide
121
- /** Internal: <Sidebar/> reports its side so the provider can resolve the
122
- * default shortcut and the rail can mirror. */
123
- registerSide: (side: SidebarSide) => void
124
- /** The resolved toggle key ("[" / "]" / custom / null when disabled). */
125
- shortcut: string | null
126
- /** The collapsed sidebar opens on hover (see the provider's prop). */
127
- openOnHover: boolean
128
- /** True while the collapsed sidebar is open on hover. */
129
- hoverOpen: boolean
130
- setHoverOpen: React.Dispatch<React.SetStateAction<boolean>>
131
- /** Internal: the shell and the triggers register their nodes; the hover
132
- * zone is measured off them. */
133
- shellRef: MutableRefObject<HTMLElement | null>
134
- registerTrigger: (node: HTMLElement) => () => void
135
- /** Internal: true while the rail is being drag-resized (disables the
136
- * width spring so the panel tracks the pointer 1:1). */
137
- isResizing: boolean
138
- setIsResizing: React.Dispatch<React.SetStateAction<boolean>>
139
- }
140
-
141
- // Mounted-provider registry for the global toggle shortcut. The listener has
142
- // to be global (the key should work without focus in the sidebar), but only
143
- // ONE provider may answer a keypress: the innermost one containing focus, or —
144
- // when focus is outside every provider — the OUTERMOST mounted one (the
145
- // app-shell provider that wraps everything else; mount order can't be used
146
- // because a persistent layout provider mounts once while demos mount later
147
- // on navigation). Same pattern as AskUserQuestions' 1-9 shortcuts.
148
- const mountedProviders: HTMLElement[] = []
149
-
150
- const SidebarContext = createContext<SidebarContextValue | null>(null)
151
-
152
- export function useSidebar(): SidebarContextValue {
153
- const ctx = useContext(SidebarContext)
154
- if (!ctx) throw new Error('useSidebar must be used within a SidebarProvider')
155
- return ctx
156
- }
157
-
158
- /** Open on hover's zone: from the frame's edge to the sidebar's inner edge,
159
- * plus the strip past it, widened to the far side of a trigger sitting
160
- * right beside that edge (an inset topbar's, which rides the edge as the
161
- * sidebar opens), within the frame's height. */
162
- function inHoverZone(
163
- event: PointerEvent,
164
- side: SidebarSide,
165
- frame: HTMLElement | null,
166
- shell: HTMLElement | null,
167
- triggers: Set<HTMLElement>,
168
- ): boolean {
169
- if (!frame || !shell) return false
170
- const bounds = frame.getBoundingClientRect()
171
- if (event.clientY < bounds.top || event.clientY > bounds.bottom) return false
172
- const box = shell.getBoundingClientRect()
173
- const inner = side === 'left' ? box.right : box.left
174
- let reach = SIDEBAR_HOVER_STRIP
175
- for (const trigger of triggers) {
176
- const button = trigger.getBoundingClientRect()
177
- const gap = side === 'left' ? button.left - inner : inner - button.right
178
- if (gap >= 0 && gap <= SIDEBAR_HOVER_TRIGGER_GAP) reach = Math.max(reach, gap + button.width)
179
- }
180
- return side === 'left'
181
- ? event.clientX >= bounds.left && event.clientX < inner + reach
182
- : event.clientX <= bounds.right && event.clientX > inner - reach
183
- }
184
-
185
- // ─── SidebarProvider ─────────────────────────────────────────────────────────
186
-
187
- export interface SidebarProviderProps extends HTMLAttributes<HTMLDivElement> {
188
- defaultOpen?: boolean
189
- open?: boolean
190
- onOpenChange?: (open: boolean) => void
191
- /** Persist the desktop open state to the `sidebar_state` cookie so a server
192
- * layout can read it back into `defaultOpen`. Mobile state never persists. */
193
- persist?: boolean
194
- /** Bare-key toggle shortcut. Defaults to "[" for a left sidebar and "]"
195
- * for a right one; `null` disables it. */
196
- shortcut?: string | null
197
- /** Viewport width (px) below which the sidebar renders as a drawer. */
198
- mobileBreakpoint?: number
199
- /** While collapsed, open the sidebar when the pointer reaches its edge or
200
- * the trigger beside it, and close it a beat after the pointer leaves.
201
- * `open` then says whether it is kept open, and the trigger switches
202
- * between the two. Hovering never writes the cookie. @default false */
203
- openOnHover?: boolean
204
- width?: string
205
- widthMobile?: string
206
- }
207
-
208
- const SidebarProvider = forwardRef<HTMLDivElement, SidebarProviderProps>(
209
- (
210
- {
211
- defaultOpen = true,
212
- open: openProp,
213
- onOpenChange,
214
- persist = true,
215
- shortcut: shortcutProp,
216
- mobileBreakpoint = 768,
217
- openOnHover = false,
218
- width: widthProp = SIDEBAR_WIDTH,
219
- widthMobile = SIDEBAR_WIDTH_MOBILE,
220
- className,
221
- style,
222
- children,
223
- ...props
224
- },
225
- ref,
226
- ) => {
227
- const isMobile = useIsMobile(mobileBreakpoint)
228
- const [openMobile, setOpenMobile] = useState(false)
229
- const [side, setSide] = useState<SidebarSide>('left')
230
- const wrapperRef = useRef<HTMLDivElement | null>(null)
231
-
232
- useEffect(() => {
233
- const el = wrapperRef.current
234
- if (!el) return
235
- mountedProviders.push(el)
236
- return () => {
237
- const i = mountedProviders.indexOf(el)
238
- if (i !== -1) mountedProviders.splice(i, 1)
239
- }
240
- }, [])
241
- const registerSide = useCallback((next: SidebarSide) => setSide(next), [])
242
-
243
- // Live width: the prop is the starting point, the rail's drag-resize
244
- // updates it at runtime.
245
- const [width, setWidth] = useState(widthProp)
246
- useEffect(() => setWidth(widthProp), [widthProp])
247
- const [isResizing, setIsResizing] = useState(false)
248
-
249
- // Default shortcut mirrors the sidebar's edge: "[" left, "]" right.
250
- const shortcut =
251
- shortcutProp === undefined
252
- ? side === 'right'
253
- ? SIDEBAR_KEYBOARD_SHORTCUT_RIGHT
254
- : SIDEBAR_KEYBOARD_SHORTCUT
255
- : shortcutProp
256
-
257
- const [internalOpen, setInternalOpen] = useState(defaultOpen)
258
- const open = openProp ?? internalOpen
259
- const openRef = useRef(open)
260
- openRef.current = open
261
-
262
- const setOpen = useCallback(
263
- (value: boolean | ((prev: boolean) => boolean)) => {
264
- const next = typeof value === 'function' ? value(openRef.current) : value
265
- if (onOpenChange) onOpenChange(next)
266
- else setInternalOpen(next)
267
- if (persist) {
268
- document.cookie = `${SIDEBAR_COOKIE_NAME}=${next}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`
269
- }
270
- },
271
- [onOpenChange, persist],
272
- )
273
-
274
- const toggleSidebar = useCallback(() => {
275
- if (isMobile) setOpenMobile(prev => !prev)
276
- else setOpen(prev => !prev)
277
- }, [isMobile, setOpen])
278
-
279
- // Open on hover. Keeping it open ends the hover; the zone arms only once
280
- // the pointer has been outside it, so collapsing from the trigger or the
281
- // rail under the pointer doesn't open it straight back.
282
- const [hoverOpen, setHoverOpen] = useState(false)
283
- useEffect(() => {
284
- if (open || !openOnHover) setHoverOpen(false)
285
- }, [open, openOnHover])
286
- const shellRef = useRef<HTMLElement | null>(null)
287
- const triggers = useRef(new Set<HTMLElement>())
288
- const registerTrigger = useCallback((node: HTMLElement) => {
289
- triggers.current.add(node)
290
- return () => {
291
- triggers.current.delete(node)
292
- }
293
- }, [])
294
- useEffect(() => {
295
- if (!openOnHover || open || isMobile || isResizing) return
296
- let armed = false
297
- let close: ReturnType<typeof setTimeout> | undefined
298
- const onPointerMove = (event: PointerEvent) => {
299
- if (event.pointerType !== 'mouse') return
300
- if (!inHoverZone(event, side, wrapperRef.current, shellRef.current, triggers.current)) {
301
- armed = true
302
- if (hoverOpen) close ??= setTimeout(() => setHoverOpen(false), 300)
303
- return
304
- }
305
- clearTimeout(close)
306
- close = undefined
307
- if (armed) setHoverOpen(true)
308
- }
309
- document.addEventListener('pointermove', onPointerMove)
310
- return () => {
311
- document.removeEventListener('pointermove', onPointerMove)
312
- clearTimeout(close)
313
- }
314
- }, [openOnHover, open, isMobile, isResizing, hoverOpen, side])
315
-
316
- // The bare shortcut key toggles the sidebar app-wide. Bound to the
317
- // provider's lifetime (not a docs-only global), skipped while typing, and
318
- // skipped when a modifier is held so ⌘[ / ⌘] keep their browser meaning.
319
- useEffect(() => {
320
- if (shortcut == null) return
321
- const onKeyDown = (event: KeyboardEvent) => {
322
- if (event.key.toLowerCase() !== shortcut.toLowerCase()) return
323
- if (event.metaKey || event.ctrlKey || event.altKey) return
324
- const target = event.target as HTMLElement
325
- if (
326
- target.tagName === 'INPUT' ||
327
- target.tagName === 'TEXTAREA' ||
328
- target.tagName === 'SELECT' ||
329
- target.isContentEditable
330
- )
331
- return
332
- // Only one mounted provider answers (see mountedProviders). Providers
333
- // can NEST (an app shell wrapping doc previews), so containment alone
334
- // isn't enough: the innermost provider containing focus wins.
335
- const root = wrapperRef.current
336
- if (!root) return
337
- if (root.contains(target)) {
338
- if (mountedProviders.some(el => el !== root && root.contains(el) && el.contains(target))) return
339
- // The focused element itself WRAPS another provider (a docs
340
- // preview frame holding keyboard scope for the demo inside it —
341
- // see click-to-focus): the wrapped provider answers, not this
342
- // ancestor shell.
343
- if (mountedProviders.some(el => el !== root && target.contains(el))) return
344
- } else {
345
- // Focus outside this provider. A focused element that WRAPS
346
- // providers — a docs preview frame holding keyboard scope for the
347
- // demo inside it (see click-to-focus) — scopes the key to what it
348
- // wraps: the outermost wrapped provider answers, and this check
349
- // must come BEFORE the containment guard below (an app-shell
350
- // provider always contains the preview frame, and must not steal
351
- // the key from the demo the frame scopes to). Focus on <body>
352
- // wraps every provider, which degenerates to the original rule:
353
- // the OUTERMOST provider overall answers — mount order is
354
- // unreliable here (a persistent app-shell provider mounts once,
355
- // while doc demos mount later on client-side navigation).
356
- const wrapped = mountedProviders.filter(el => target.contains(el))
357
- if (wrapped.length > 0) {
358
- const outermost = wrapped.find(el => !wrapped.some(other => other !== el && other.contains(el)))
359
- if (outermost !== root) return
360
- } else {
361
- if (mountedProviders.some(el => el !== root && el.contains(target))) return
362
- const outermost = mountedProviders.find(
363
- el => !mountedProviders.some(other => other !== el && other.contains(el)),
364
- )
365
- if (outermost !== root) return
366
- }
367
- }
368
- event.preventDefault()
369
- toggleSidebar()
370
- }
371
- window.addEventListener('keydown', onKeyDown)
372
- return () => window.removeEventListener('keydown', onKeyDown)
373
- }, [shortcut, toggleSidebar])
374
-
375
- const value = useMemo<SidebarContextValue>(
376
- () => ({
377
- state: open || hoverOpen ? 'expanded' : 'collapsed',
378
- open,
379
- setOpen,
380
- openMobile,
381
- setOpenMobile,
382
- isMobile,
383
- toggleSidebar,
384
- width,
385
- setWidth,
386
- widthMobile,
387
- mobileBreakpoint,
388
- side,
389
- registerSide,
390
- shortcut,
391
- openOnHover,
392
- hoverOpen,
393
- setHoverOpen,
394
- shellRef,
395
- registerTrigger,
396
- isResizing,
397
- setIsResizing,
398
- }),
399
- [
400
- open,
401
- setOpen,
402
- openMobile,
403
- isMobile,
404
- toggleSidebar,
405
- width,
406
- widthMobile,
407
- mobileBreakpoint,
408
- side,
409
- registerSide,
410
- shortcut,
411
- openOnHover,
412
- hoverOpen,
413
- registerTrigger,
414
- isResizing,
415
- ],
416
- )
417
-
418
- return (
419
- <SidebarContext.Provider value={value}>
420
- <div
421
- ref={node => {
422
- wrapperRef.current = node
423
- if (typeof ref === 'function') ref(node)
424
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
425
- }}
426
- data-slot="sidebar-wrapper"
427
- className={cn('group/sidebar-wrapper relative flex min-h-svh w-full', className)}
428
- style={
429
- {
430
- '--sidebar-width': width,
431
- '--sidebar-width-mobile': widthMobile,
432
- ...style,
433
- } as CSSProperties
434
- }
435
- {...props}
436
- >
437
- {children}
438
- </div>
439
- </SidebarContext.Provider>
440
- )
441
- },
442
- )
443
- SidebarProvider.displayName = 'SidebarProvider'
444
-
445
- // ─── Slot helpers (render / asChild polymorphism) ────────────────────────────
446
- //
447
- // A local slot instead of a primitive-library one so every menu part exists in
448
- // exactly one flavor-neutral copy: Radix's Slot would leak into the Base UI
449
- // flavor, and Base UI's useRender the other way around. Supports both the
450
- // library's `render={<Link/>}` convention and shadcn's `asChild`.
451
-
452
- type SlotProps = {
453
- className?: string
454
- style?: CSSProperties
455
- children?: ReactNode
456
- } & Record<string, unknown>
457
-
458
- function composeRefs<T>(...refs: (Ref<T> | undefined)[]): Ref<T> {
459
- return (node: T | null) => {
460
- for (const r of refs) {
461
- if (typeof r === 'function') r(node)
462
- else if (r) (r as React.MutableRefObject<T | null>).current = node
463
- }
464
- }
465
- }
466
-
467
- /** Resolves the element to clone: `render` wins, else `asChild`'s single
468
- * element child. `content` is what should render inside it — for `asChild`
469
- * the child element's own children, otherwise the caller's. */
470
- export function resolveSlotTemplate(
471
- render: ReactElement | undefined,
472
- asChild: boolean | undefined,
473
- children: ReactNode,
474
- ): { template: ReactElement<SlotProps> | null; content: ReactNode } {
475
- if (render && isValidElement(render)) {
476
- return { template: render as ReactElement<SlotProps>, content: children }
477
- }
478
- if (asChild) {
479
- const only = Children.toArray(children)[0]
480
- if (isValidElement(only)) {
481
- return {
482
- template: only as ReactElement<SlotProps>,
483
- content: (only.props as SlotProps).children,
484
- }
485
- }
486
- }
487
- return { template: null, content: children }
488
- }
489
-
490
- /** Renders `content` into the template element (merging class/style/handlers,
491
- * composing refs) or into the default tag when there is no template. */
492
- export function slotElement(
493
- template: ReactElement<SlotProps> | null,
494
- DefaultTag: ElementType,
495
- props: SlotProps & { ref?: Ref<HTMLElement> },
496
- content: ReactNode,
497
- ): ReactElement {
498
- if (!template) {
499
- const Tag = DefaultTag as ElementType
500
- return <Tag {...props}>{content}</Tag>
501
- }
502
- const templateProps = template.props
503
- const merged: SlotProps & { ref?: Ref<HTMLElement> } = {
504
- ...props,
505
- ...templateProps,
506
- className: cn(props.className, templateProps.className),
507
- style: { ...props.style, ...(templateProps.style as CSSProperties | undefined) },
508
- }
509
- // Chain duplicated event handlers, template's first (it owns the element).
510
- for (const key of Object.keys(props)) {
511
- if (!/^on[A-Z]/.test(key)) continue
512
- const ours = props[key]
513
- const theirs = templateProps[key]
514
- if (typeof ours === 'function' && typeof theirs === 'function') {
515
- merged[key] = (...args: unknown[]) => {
516
- ;(theirs as (...a: unknown[]) => void)(...args)
517
- ;(ours as (...a: unknown[]) => void)(...args)
518
- }
519
- }
520
- }
521
- const templateRef =
522
- (templateProps as { ref?: Ref<HTMLElement> }).ref ?? (template as unknown as { ref?: Ref<HTMLElement> }).ref
523
- merged.ref = composeRefs(props.ref, templateRef)
524
- return cloneElement(template, merged, content)
525
- }
526
-
527
- // ─── SidebarShell (shared desktop DOM for both flavors) ──────────────────────
528
-
529
- // Literal map so Tailwind's scanner emits the utilities: for the standard
530
- // breakpoints the shell is also hidden by CSS, avoiding a pre-hydration flash
531
- // of the rail on small screens. Non-standard breakpoints fall back to a
532
- // JS-driven `hidden` in the shell (no dissolve, but never rail + drawer at
533
- // once).
534
- // Crossing the drawer breakpoint DISSOLVES the desktop rail instead of
535
- // snapping it away: opacity fades while `display` rides the same transition
536
- // with allow-discrete, so none applies only once the fade lands (and
537
- // @starting-style fades it back in when the window grows). Literal classes
538
- // per breakpoint — Tailwind's scanner can't see composed strings.
539
- const BREAKPOINT_FADE_BASE =
540
- 'transition-[opacity,display] ease-out [transition-behavior:allow-discrete] motion-reduce:transition-none'
541
- const BREAKPOINT_HIDDEN: Record<number, string> = {
542
- 640: 'max-sm:hidden max-sm:opacity-0 max-sm:duration-moderate sm:duration-slow sm:starting:opacity-0',
543
- 768: 'max-md:hidden max-md:opacity-0 max-md:duration-moderate md:duration-slow md:starting:opacity-0',
544
- 1024: 'max-lg:hidden max-lg:opacity-0 max-lg:duration-moderate lg:duration-slow lg:starting:opacity-0',
545
- 1280: 'max-xl:hidden max-xl:opacity-0 max-xl:duration-moderate xl:duration-slow xl:starting:opacity-0',
546
- }
547
-
548
- // Props framer-motion redefines with incompatible signatures; they must not
549
- // be forwarded onto a motion.div.
550
- type MotionSafeDivProps = Omit<
551
- HTMLAttributes<HTMLDivElement>,
552
- 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onAnimationStart' | 'onAnimationEnd' | 'onAnimationIteration'
553
- >
554
-
555
- export interface SidebarShellProps extends MotionSafeDivProps {
556
- side: SidebarSide
557
- variant: SidebarVariant
558
- /** The `sidebar` variant's inner-edge border. Default true. */
559
- bordered?: boolean
560
- /** Render the built-in resize/collapse rail handle. `false` hides it and
561
- * disables drag-resize — the trigger and keyboard shortcut still toggle. */
562
- rail?: boolean
563
- /** Pin the rail's tooltip open (`true`) or closed (`false`); `undefined`
564
- * leaves it on hover. Dragging always hides it. */
565
- railTooltipOpen?: boolean
566
- }
567
-
568
- /** Internal: the expanded/collapsed desktop rail. An in-flow sticky column
569
- * animates its width (this is what reflows the inset) while the fixed-width
570
- * panel inside slides out under overflow clipping — container-relative, so
571
- * the whole sidebar works inside any bounded frame, not just the viewport.
572
- * Ships the resize/collapse rail handle on its inner edge by default. */
573
- const SidebarShell = forwardRef<HTMLDivElement, SidebarShellProps>(
574
- ({ side, variant, bordered = true, rail = true, railTooltipOpen, className, children, ...props }, ref) => {
575
- const { open: kept, hoverOpen, width, mobileBreakpoint, isMobile, isResizing, shellRef } = useSidebar()
576
- const open = kept || hoverOpen
577
- const shape = useShape()
578
-
579
- const substrate = useSurface()
580
- const floatingLevel = Math.min(substrate + 1, 8)
581
- // Drag-resize needs the panel glued to the pointer; the spring resumes
582
- // for open/close. Reduced motion snaps instead of sliding — the state
583
- // change stays legible without the 256px of travel. The open/close ride
584
- // the SLOW tier: a whole column moving is the largest thing this
585
- // component animates (the sheet stays on moderate — drawers
586
- // settle precisely, per the tier notes).
587
- const reduceMotion = useReducedMotion() ?? false
588
- // Mid-drag open flips — the collapse preview and its drag-back rescue —
589
- // ride the moderate tier instead of the drag's glued duration-0 tracking.
590
- // The flip is detected synchronously (transition must be right on the
591
- // very commit whose animate target changes; effects run too late), then
592
- // `dragFlip` holds the spring through its settle so pointer moves landing
593
- // right after a flip retarget the spring instead of snapping.
594
- const [dragFlip, setDragFlip] = useState(false)
595
- const prevOpenRef = useRef(open)
596
- const openFlipped = prevOpenRef.current !== open
597
- useEffect(() => {
598
- const flipped = prevOpenRef.current !== open
599
- prevOpenRef.current = open
600
- if (!isResizing) {
601
- setDragFlip(false)
602
- return
603
- }
604
- if (!flipped) return
605
- setDragFlip(true)
606
- const id = setTimeout(() => setDragFlip(false), exitFallbackMs(spring.moderate))
607
- return () => clearTimeout(id)
608
- }, [open, isResizing])
609
- const widthTransition = reduceMotion
610
- ? { duration: 0 }
611
- : isResizing
612
- ? openFlipped || dragFlip
613
- ? open
614
- ? spring.moderate
615
- : spring.moderate.exit
616
- : { duration: 0 }
617
- : open
618
- ? spring.slow
619
- : spring.slow.exit
620
-
621
- return (
622
- <motion.div
623
- ref={(node: HTMLDivElement | null) => {
624
- shellRef.current = node
625
- if (typeof ref === 'function') ref(node)
626
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node
627
- }}
628
- data-slot="sidebar"
629
- data-state={open ? 'expanded' : 'collapsed'}
630
- data-collapsible={open ? '' : 'offcanvas'}
631
- data-variant={variant}
632
- data-side={side}
633
- className={cn(
634
- // No bare `group` here: an unnamed group on the whole rail would
635
- // fire every descendant's group-hover (Button fills, icon strokes)
636
- // on rail hover. Named groups (menu-item etc.) handle row states.
637
- 'peer shrink-0 sticky top-0 h-svh overflow-hidden',
638
- // Flex order (not DOM order) decides the side, so consumers can
639
- // keep Sidebar before SidebarInset regardless of `side`.
640
- side === 'right' && 'order-last',
641
- // The inset rail has no card edge of its own, so its scroll hairline
642
- // hugs the rows' 8px gutter instead of running panel-wide.
643
- variant === 'inset' && '[--scroll-divider-inset:8px]',
644
- BREAKPOINT_FADE_BASE,
645
- BREAKPOINT_HIDDEN[mobileBreakpoint],
646
- // A non-standard breakpoint has no literal utility in the map, so
647
- // JS hides the shell (no fade, but never rail + drawer at once).
648
- !BREAKPOINT_HIDDEN[mobileBreakpoint] && isMobile && 'hidden',
649
- className,
650
- )}
651
- initial={false}
652
- animate={{ width: open ? width : '0rem' }}
653
- transition={widthTransition}
654
- {...props}
655
- >
656
- <motion.div
657
- className={cn(
658
- 'absolute inset-y-0 flex h-full flex-col',
659
- side === 'left' ? 'left-0' : 'right-0',
660
- // Floating floats its card inside a full gutter; inset only needs
661
- // the vertical inset (horizontal room belongs to the nav rows).
662
- variant === 'floating' && 'p-2',
663
- variant === 'inset' && 'py-2',
664
- )}
665
- style={{ width }}
666
- initial={false}
667
- animate={{ x: open ? '0%' : side === 'left' ? '-100%' : '100%' }}
668
- transition={widthTransition}
669
- // Not mid-drag: the rail inside holds the pointer through a collapse preview.
670
- {...inertProps(!(open || isResizing))}
671
- >
672
- {variant === 'floating' ? (
673
- <div
674
- data-sidebar="sidebar"
675
- className={cn('flex h-full w-full min-h-0 flex-col', shape.container, surfaceClasses(floatingLevel, 3))}
676
- >
677
- <SurfaceProvider value={floatingLevel}>{children}</SurfaceProvider>
678
- </div>
679
- ) : (
680
- <div
681
- data-sidebar="sidebar"
682
- className={cn(
683
- 'flex h-full w-full min-h-0 flex-col',
684
- bordered &&
685
- variant === 'sidebar' &&
686
- (side === 'left' ? 'border-r border-border' : 'border-l border-border'),
687
- )}
688
- >
689
- {children}
690
- </div>
691
- )}
692
- {rail && (
693
- <SidebarRail
694
- tooltipOpen={railTooltipOpen}
695
- className={cn(
696
- // The floating card sits inside the panel's p-2 gutter, so the
697
- // grab strip (and its hover hairline) moves in to straddle the
698
- // card's edge instead of the panel's.
699
- variant === 'floating' &&
700
- (side === 'left' ? 'right-1 after:right-[3.5px]' : 'left-1 after:left-[3.5px]'),
701
- // Cards are vertically inset and rounded — the hover hairline
702
- // hugs the card's straight run: fully transparent through the
703
- // corner radius, then fading in over 24px (mirrored at the
704
- // bottom). The radius rides the shape system via CSS vars.
705
- variant !== 'sidebar' &&
706
- 'after:inset-y-2 after:[mask-image:linear-gradient(to_bottom,transparent_var(--rail-fade-start),black_var(--rail-fade-end),black_calc(100%-var(--rail-fade-end)),transparent_calc(100%-var(--rail-fade-start)))]',
707
- )}
708
- style={
709
- variant !== 'sidebar'
710
- ? ({
711
- '--rail-fade-start': `${shape.bgRadius >= 20 ? 24 : 12}px`,
712
- '--rail-fade-end': `${(shape.bgRadius >= 20 ? 24 : 12) + 24}px`,
713
- } as CSSProperties)
714
- : undefined
715
- }
716
- />
717
- )}
718
- </motion.div>
719
- </motion.div>
720
- )
721
- },
722
- )
723
- SidebarShell.displayName = 'SidebarShell'
724
-
725
- // ─── SidebarTrigger ──────────────────────────────────────────────────────────
726
-
727
- export type SidebarTriggerProps = ButtonProps
728
-
729
- /** Keystroke chip rendered inside the (inverted) tooltip surface. */
730
- function ShortcutKbd({ children }: { children: ReactNode }) {
731
- return (
732
- <kbd className="-my-1 flex h-4 min-w-4 items-center justify-center rounded border border-background/30 px-1 font-sans text-micro-compact text-background/80">
733
- {children}
734
- </kbd>
735
- )
736
- }
737
-
738
- /** The tooltips always show the toggle keystroke, falling back to the
739
- * side's default key even when the provider's binding is disabled. */
740
- function useShortcutKey(): string {
741
- const { side, shortcut } = useSidebar()
742
- return shortcut ?? (side === 'right' ? SIDEBAR_KEYBOARD_SHORTCUT_RIGHT : SIDEBAR_KEYBOARD_SHORTCUT)
743
- }
744
-
745
- /** Ghost icon button calling toggleSidebar(). The icon mirrors the
746
- * sidebar's side, and its tooltip names the action with the toggle
747
- * keystroke by default. With `openOnHover` it switches between open and on
748
- * hover (a dashed glyph), and the hover zone reaches its far side when it
749
- * sits beside the sidebar's edge. */
750
- const SidebarTrigger = forwardRef<HTMLButtonElement, SidebarTriggerProps>(
751
- ({ onClick, size, children, ...props }, ref) => {
752
- const { toggleSidebar, open, openMobile, isMobile, side, openOnHover, registerTrigger } = useSidebar()
753
- const shortcutKey = useShortcutKey()
754
- const hoverMode = openOnHover && !isMobile
755
- const TriggerIcon = useIcon(`panel-${side}${hoverMode && !open ? '-dashed' : ''}`)
756
- const iconSize = useSizeVariant() === 'compact' ? ('icon-compact' as const) : ('icon' as const)
757
- const collapsed = isMobile ? !openMobile : !open
758
- const label = hoverMode
759
- ? open
760
- ? 'Show sidebar on hover'
761
- : 'Keep sidebar open'
762
- : collapsed
763
- ? 'Expand sidebar'
764
- : 'Collapse sidebar'
765
- const node = useRef<HTMLButtonElement | null>(null)
766
- useEffect(() => (node.current ? registerTrigger(node.current) : undefined), [registerTrigger])
767
-
768
- return (
769
- <Tooltip
770
- side="bottom"
771
- content={
772
- <span className="flex items-center gap-1.5">
773
- {/* A flex row escapes the surface's text-box trim, so the label
774
- re-applies it — otherwise the shortcut row would sit taller
775
- than a tooltip without a chip. */}
776
- <span className="[text-box:trim-both_cap_alphabetic]">{label}</span>
777
- <ShortcutKbd>{shortcutKey}</ShortcutKbd>
778
- </span>
779
- }
780
- >
781
- <Button
782
- ref={(element: HTMLButtonElement | null) => {
783
- node.current = element
784
- if (typeof ref === 'function') ref(element)
785
- else if (ref) ref.current = element
786
- }}
787
- variant="ghost"
788
- size={size ?? iconSize}
789
- data-sidebar="trigger"
790
- aria-label={hoverMode ? label : 'Toggle Sidebar'}
791
- onClick={event => {
792
- onClick?.(event)
793
- toggleSidebar()
794
- }}
795
- {...props}
796
- >
797
- {children ?? <TriggerIcon />}
798
- </Button>
799
- </Tooltip>
800
- )
801
- },
802
- )
803
- SidebarTrigger.displayName = 'SidebarTrigger'
804
-
805
- // ─── SidebarRail ─────────────────────────────────────────────────────────────
806
-
807
- export interface SidebarRailProps extends HTMLAttributes<HTMLButtonElement> {
808
- /** Pin the tooltip open/closed; `undefined` leaves it on hover. */
809
- tooltipOpen?: boolean
810
- }
811
-
812
- /** The grab strip on the sidebar's inner edge, rendered by default inside
813
- * the desktop shell: drag it to resize (clamped), click it to collapse, and
814
- * its tooltip explains both with the toggle keystroke. Hovering it
815
- * brightens the edge border. */
816
- const SidebarRail = forwardRef<HTMLButtonElement, SidebarRailProps>(({ className, tooltipOpen, ...props }, ref) => {
817
- const { toggleSidebar, open, setOpen, setHoverOpen, setWidth, side, setIsResizing } = useSidebar()
818
- const shortcutKey = useShortcutKey()
819
- const railRef = useRef<HTMLButtonElement | null>(null)
820
- const dragRef = useRef<{
821
- startX: number
822
- startWidth: number
823
- moved: boolean
824
- collapsed: boolean
825
- /** Kept open at the press; open on hover, the rail collapses and drags the hover, never keeping it open. */
826
- kept: boolean
827
- } | null>(null)
828
- const [dragging, setDragging] = useState(false)
829
-
830
- const onPointerDown = (event: React.PointerEvent<HTMLButtonElement>) => {
831
- const panel = railRef.current?.closest('[data-slot="sidebar"]') as HTMLElement | null
832
- if (!panel) return
833
- dragRef.current = {
834
- startX: event.clientX,
835
- startWidth: panel.offsetWidth,
836
- moved: false,
837
- collapsed: false,
838
- kept: open,
839
- }
840
- event.currentTarget.setPointerCapture(event.pointerId)
841
- }
842
-
843
- const onPointerMove = (event: React.PointerEvent<HTMLButtonElement>) => {
844
- const drag = dragRef.current
845
- if (!drag) return
846
- const dx = event.clientX - drag.startX
847
- if (!drag.moved && Math.abs(dx) < 4) return
848
- if (!drag.moved) {
849
- drag.moved = true
850
- setDragging(true)
851
- setIsResizing(true)
852
- }
853
- const delta = side === 'left' ? dx : -dx
854
- const raw = drag.startWidth + delta
855
- // Dragged well past the minimum, toward the edge: preview the collapse
856
- // but keep the drag session alive — pulling back past the threshold
857
- // re-expands, so an overshoot isn't committed until release.
858
- if (raw < SIDEBAR_MIN_WIDTH - SIDEBAR_COLLAPSE_SLOP) {
859
- if (!drag.collapsed) {
860
- drag.collapsed = true
861
- setWidth(`${SIDEBAR_MIN_WIDTH}px`)
862
- ;(drag.kept ? setOpen : setHoverOpen)(false)
863
- }
864
- return
865
- }
866
- if (drag.collapsed) {
867
- drag.collapsed = false
868
- ;(drag.kept ? setOpen : setHoverOpen)(true)
869
- }
870
- const next = Math.max(SIDEBAR_MIN_WIDTH, Math.min(SIDEBAR_MAX_WIDTH, raw))
871
- setWidth(`${next}px`)
872
- }
873
-
874
- const onPointerUp = (event: React.PointerEvent<HTMLButtonElement>) => {
875
- const drag = dragRef.current
876
- dragRef.current = null
877
- event.currentTarget.releasePointerCapture(event.pointerId)
878
- setDragging(false)
879
- setIsResizing(false)
880
- // A press that never turned into a drag is the collapse click.
881
- if (drag && !drag.moved) {
882
- if (drag.kept) toggleSidebar()
883
- else setHoverOpen(false)
884
- }
885
- }
886
-
887
- // A cancelled pointer (touch interruption, capture loss) ends the drag
888
- // where it stands — no collapse-click, capture is already released.
889
- const onPointerCancel = () => {
890
- dragRef.current = null
891
- setDragging(false)
892
- setIsResizing(false)
893
- }
894
-
895
- return (
896
- <Tooltip
897
- side={side === 'left' ? 'right' : 'left'}
898
- sideOffset={8}
899
- followCursor="y"
900
- forceOpen={dragging ? false : tooltipOpen}
901
- content={
902
- // The flex column escapes the tooltip surface's text-box trim, so
903
- // BOTH lines re-apply it and the gap alone carries the line rhythm
904
- // — an untrimmed line would smuggle its half-leading back in as
905
- // lopsided padding.
906
- <span className="flex flex-col items-start gap-2">
907
- <span className="[text-box:trim-both_cap_alphabetic]">
908
- <span className="weight-semibold">Drag</span> to resize
909
- </span>
910
- <span className="flex items-center gap-1.5">
911
- <span className="[text-box:trim-both_cap_alphabetic]">
912
- <span className="weight-semibold">Click</span> to collapse
913
- </span>
914
- <ShortcutKbd>{shortcutKey}</ShortcutKbd>
915
- </span>
916
- </span>
917
- }
918
- >
919
- <button
920
- ref={node => {
921
- railRef.current = node
922
- if (typeof ref === 'function') ref(node)
923
- else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node
924
- }}
925
- type="button"
926
- data-sidebar="rail"
927
- aria-label="Resize or collapse sidebar"
928
- tabIndex={-1}
929
- onPointerDown={onPointerDown}
930
- onPointerMove={onPointerMove}
931
- onPointerUp={onPointerUp}
932
- onPointerCancel={onPointerCancel}
933
- className={cn(
934
- // touch-none: without it the browser claims a touch drag for
935
- // scrolling and pointercancels the resize mid-gesture.
936
- 'absolute inset-y-0 z-20 w-2 cursor-col-resize touch-none outline-none',
937
- // Positioned from context (not group-data selectors) so variant
938
- // offsets passed via className can win the merge.
939
- side === 'left' ? 'right-0' : 'left-0',
940
- // Hovering brightens the edge border the shell draws by default;
941
- // a pinned tooltip brightens it too, so the spotlight reads as
942
- // the hover it stands in for.
943
- 'after:absolute after:inset-y-0 after:w-px after:bg-transparent hover:after:bg-foreground/25 after:transition-colors after:duration-fast',
944
- tooltipOpen && 'after:bg-foreground/25',
945
- side === 'left' ? 'after:right-0' : 'after:left-0',
946
- className,
947
- )}
948
- {...props}
949
- />
950
- </Tooltip>
951
- )
952
- })
953
- SidebarRail.displayName = 'SidebarRail'
954
-
955
- // ─── SidebarInset ────────────────────────────────────────────────────────────
956
-
957
- export type SidebarInsetProps = HTMLAttributes<HTMLElement>
958
-
959
- const SidebarInset = forwardRef<HTMLElement, SidebarInsetProps>(({ className, ...props }, ref) => {
960
- const shape = useShape()
961
- return (
962
- <main
963
- ref={ref}
964
- data-slot="sidebar-inset"
965
- className={cn(
966
- 'relative flex min-h-0 w-full min-w-0 flex-1 flex-col bg-background',
967
- 'peer-data-[variant=inset]:m-2 peer-data-[variant=inset]:peer-data-[side=left]:ml-0 peer-data-[variant=inset]:peer-data-[side=right]:mr-0',
968
- // With the rail collapsed away, restore the sidebar-side margin so
969
- // the card keeps symmetric insets.
970
- 'peer-data-[variant=inset]:peer-data-[state=collapsed]:peer-data-[side=left]:ml-2 peer-data-[variant=inset]:peer-data-[state=collapsed]:peer-data-[side=right]:mr-2',
971
- 'transition-[margin] duration-fast',
972
- // Container radius follows the shape system (literal classes so
973
- // Tailwind's scanner emits both).
974
- shape.bgRadius >= 20 ? 'peer-data-[variant=inset]:rounded-3xl' : 'peer-data-[variant=inset]:rounded-xl',
975
- 'peer-data-[variant=inset]:bg-surface-2 peer-data-[variant=inset]:shadow-surface-2',
976
- className,
977
- )}
978
- {...props}
979
- />
980
- )
981
- })
982
- SidebarInset.displayName = 'SidebarInset'
983
-
984
- // ─── SidebarInput ────────────────────────────────────────────────────────────
985
-
986
- export type SidebarInputProps = React.InputHTMLAttributes<HTMLInputElement>
987
-
988
- const SidebarInput = forwardRef<HTMLInputElement, SidebarInputProps>(({ className, ...props }, ref) => {
989
- const shape = useShape()
990
- const size = useSize()
991
- return (
992
- <input
993
- ref={ref}
994
- data-sidebar="input"
995
- className={cn(
996
- fieldVariants(),
997
- 'w-full px-3 text-foreground placeholder:text-muted-foreground outline-none',
998
- 'focus-visible:ring-focus-ring',
999
- size.variant === 'compact' ? 'h-7' : 'h-8',
1000
- size.text,
1001
- shape.input,
1002
- className,
1003
- )}
1004
- {...props}
1005
- />
1006
- )
1007
- })
1008
- SidebarInput.displayName = 'SidebarInput'
1009
-
1010
- // ─── SidebarHeader / SidebarFooter / SidebarSeparator ────────────────────────
1011
-
1012
- export type SidebarSectionProps = HTMLAttributes<HTMLDivElement>
1013
-
1014
- const SidebarHeader = forwardRef<HTMLDivElement, SidebarSectionProps>(({ className, ...props }, ref) => (
1015
- <div ref={ref} data-sidebar="header" className={cn('flex shrink-0 flex-col gap-2 p-2', className)} {...props} />
1016
- ))
1017
- SidebarHeader.displayName = 'SidebarHeader'
1018
-
1019
- const SidebarFooter = forwardRef<HTMLDivElement, SidebarSectionProps>(({ className, ...props }, ref) => (
1020
- <div
1021
- ref={ref}
1022
- data-sidebar="footer"
1023
- className={cn('mt-auto flex shrink-0 flex-col gap-2 p-2', className)}
1024
- {...props}
1025
- />
1026
- ))
1027
- SidebarFooter.displayName = 'SidebarFooter'
1028
-
1029
- const SidebarSeparator = forwardRef<HTMLDivElement, SidebarSectionProps>(({ className, ...props }, ref) => (
1030
- <div
1031
- ref={ref}
1032
- data-sidebar="separator"
1033
- role="separator"
1034
- aria-orientation="horizontal"
1035
- className={cn('mx-2 h-px shrink-0 bg-border', className)}
1036
- {...props}
1037
- />
1038
- ))
1039
- SidebarSeparator.displayName = 'SidebarSeparator'
1040
-
1041
- // ─── SidebarGroup family ─────────────────────────────────────────────────────
1042
-
1043
- // SSR-safe layout effect (client components still server-render in Next).
1044
- const useIsoLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
1045
-
1046
- interface SidebarGroupContextValue {
1047
- open: boolean
1048
- toggle: () => void
1049
- contentId: string
1050
- /** How many header action buttons overlay the label's right edge — the
1051
- * collapsible label pads itself so its chevron clears them. */
1052
- actionsCount: number
1053
- }
1054
-
1055
- const SidebarGroupContext = createContext<SidebarGroupContextValue | null>(null)
1056
-
1057
- export interface SidebarGroupProps extends SidebarSectionProps {
1058
- /** Makes the group's SidebarGroupLabel a toggle that collapses everything
1059
- * rendered after it — a group-level accordion. Uncontrolled by default;
1060
- * pass `open`/`onOpenChange` to control it. */
1061
- collapsible?: boolean
1062
- open?: boolean
1063
- defaultOpen?: boolean
1064
- onOpenChange?: (open: boolean) => void
1065
- }
1066
-
1067
- const SidebarGroup = forwardRef<HTMLDivElement, SidebarGroupProps>(
1068
- ({ className, collapsible = false, open: openProp, defaultOpen = true, onOpenChange, children, ...props }, ref) => {
1069
- const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen)
1070
- const open = openProp ?? uncontrolledOpen
1071
- const contentId = useId()
1072
- const toggle = useCallback(() => {
1073
- const next = !(openProp ?? uncontrolledOpen)
1074
- setUncontrolledOpen(next)
1075
- onOpenChange?.(next)
1076
- }, [openProp, uncontrolledOpen, onOpenChange])
1077
- // Measured-height collapse: animate between 0 and the content's real
1078
- // offsetHeight — never to "auto", which framer measures wrong under a
1079
- // scaled ancestor.
1080
- const contentRef = useRef<HTMLDivElement>(null)
1081
- const [contentHeight, setContentHeight] = useState<number | null>(null)
1082
- useIsoLayoutEffect(() => {
1083
- if (!collapsible) return
1084
- const el = contentRef.current
1085
- if (!el || typeof ResizeObserver === 'undefined') return
1086
- const measure = () => setContentHeight(el.offsetHeight)
1087
- measure()
1088
- const ro = new ResizeObserver(measure)
1089
- ro.observe(el)
1090
- return () => ro.disconnect()
1091
- }, [collapsible])
1092
- const measured = contentHeight !== null
1093
- // The collapse wrapper must clip while animating, but a permanently
1094
- // clipped box shaves the 2px focus ring off a group's first and last
1095
- // rows — so clipping lifts once an open group has settled.
1096
- const [settled, setSettled] = useState(open)
1097
- useEffect(() => {
1098
- if (!open) setSettled(false)
1099
- }, [open])
1100
-
1101
- // Height animates only when THIS group toggles. When the measured height
1102
- // changes underneath it instead — a nested sub-menu collapsing inside the
1103
- // group — the wrapper must snap: a spring re-targeted every frame chases
1104
- // the child's own animation, lands well after it, and drags everything
1105
- // below the group along late. Tracked with a ref so a controlled `open`
1106
- // is covered too, and cleared once the toggle's animation lands.
1107
- const prevOpenRef = useRef(open)
1108
- const togglingRef = useRef(false)
1109
- if (prevOpenRef.current !== open) {
1110
- prevOpenRef.current = open
1111
- togglingRef.current = true
1112
- }
1113
-
1114
- // The label and any header actions stay put; everything else after the
1115
- // label rides in the collapse wrapper. If no SidebarGroupLabel child is
1116
- // found the group renders untouched.
1117
- const isHeaderAction = (k: ReactNode) =>
1118
- isValidElement(k) && (k.type === SidebarGroupAction || k.type === SidebarGroupActions)
1119
- let inner: ReactNode = children
1120
- let actionsCount = 0
1121
- if (collapsible) {
1122
- const kids = Children.toArray(children)
1123
- const labelIdx = kids.findIndex(k => isValidElement(k) && k.type === SidebarGroupLabel)
1124
- if (labelIdx !== -1) {
1125
- const tail = kids.slice(labelIdx + 1)
1126
- const headerActions = tail.filter(isHeaderAction)
1127
- const rest = tail.filter(k => !isHeaderAction(k))
1128
- actionsCount = headerActions.reduce<number>(
1129
- (n, k) =>
1130
- n +
1131
- (isValidElement(k) && k.type === SidebarGroupActions
1132
- ? Children.count((k.props as { children?: ReactNode }).children)
1133
- : 1),
1134
- 0,
1135
- )
1136
- inner = (
1137
- <>
1138
- {kids.slice(0, labelIdx)}
1139
- {/* Header hover scope: hovering anywhere on the row — the
1140
- overlaid action cluster included, which never :hovers the
1141
- label element itself — reveals the label's chevron. Kept
1142
- position-static so the cluster's absolute box still anchors
1143
- to the group and stays on the rows' action axis. */}
1144
- <div className="group/group-header w-full">
1145
- {kids[labelIdx]}
1146
- {headerActions}
1147
- </div>
1148
- <motion.div
1149
- id={contentId}
1150
- aria-hidden={open ? undefined : true}
1151
- className={cn(open && settled ? 'overflow-visible' : 'overflow-hidden', !measured && !open && 'h-0')}
1152
- initial={false}
1153
- animate={
1154
- measured ? { height: open ? contentHeight : 0, opacity: open ? 1 : 0 } : { opacity: open ? 1 : 0 }
1155
- }
1156
- // Do NOT simplify this to `open ? spring.moderate : …`. The
1157
- // togglingRef arm is what stops a re-measure from springing —
1158
- // without it a nested collapse makes this wrapper chase its own
1159
- // child and everything below the group moves late.
1160
- transition={togglingRef.current ? (open ? spring.moderate : spring.moderate.exit) : { duration: 0 }}
1161
- onAnimationComplete={() => {
1162
- togglingRef.current = false
1163
- if (open) setSettled(true)
1164
- }}
1165
- >
1166
- <div ref={contentRef} className="flex w-full min-w-0 flex-col" {...inertProps(!open)}>
1167
- {rest}
1168
- </div>
1169
- </motion.div>
1170
- </>
1171
- )
1172
- }
1173
- }
1174
-
1175
- const ctx = useMemo(() => ({ open, toggle, contentId, actionsCount }), [open, toggle, contentId, actionsCount])
1176
-
1177
- return (
1178
- <div
1179
- ref={ref}
1180
- data-sidebar="group"
1181
- data-state={collapsible ? (open ? 'open' : 'closed') : undefined}
1182
- className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
1183
- {...props}
1184
- >
1185
- <SidebarGroupContext.Provider value={collapsible ? ctx : null}>{inner}</SidebarGroupContext.Provider>
1186
- </div>
1187
- )
1188
- },
1189
- )
1190
- SidebarGroup.displayName = 'SidebarGroup'
1191
-
1192
- export interface SidebarGroupLabelProps extends HTMLAttributes<HTMLDivElement> {
1193
- render?: ReactElement
1194
- asChild?: boolean
1195
- }
1196
-
1197
- const SidebarGroupLabel = forwardRef<HTMLDivElement, SidebarGroupLabelProps>(
1198
- ({ className, render, asChild, children, ...props }, ref) => {
1199
- const sizeVariant = useSizeVariant()
1200
- const sizeClasses = useSize()
1201
- const group = useContext(SidebarGroupContext)
1202
- const shape = useShape()
1203
- const ChevronRightIcon = useIcon('chevron-right')
1204
- const { template, content } = resolveSlotTemplate(render, asChild, children)
1205
-
1206
- // Truncate only the leading text; element children (count badges,
1207
- // trailing controls) stay flex siblings so the row's gap keeps spacing
1208
- // them — same split MenuRowLabel does for menu rows.
1209
- const nodes = Children.toArray(content)
1210
- let textEnd = 0
1211
- while (textEnd < nodes.length && (typeof nodes[textEnd] === 'string' || typeof nodes[textEnd] === 'number')) {
1212
- textEnd++
1213
- }
1214
- const leadingText = nodes.slice(0, textEnd).join('')
1215
- const labelContent = leadingText ? (
1216
- <>
1217
- <span className="min-w-0 truncate">{leadingText}</span>
1218
- {nodes.slice(textEnd)}
1219
- </>
1220
- ) : (
1221
- content
1222
- )
1223
-
1224
- // Inside a collapsible group the label becomes the toggle. Design
1225
- // treatment is unchanged — hover only raises the label's contrast and
1226
- // reveals a chevron (kept visible while collapsed as the reopen cue).
1227
- if (group) {
1228
- return slotElement(
1229
- template,
1230
- 'button',
1231
- {
1232
- ref: ref as Ref<HTMLElement>,
1233
- type: template ? undefined : 'button',
1234
- 'data-sidebar': 'group-label',
1235
- 'aria-expanded': group.open,
1236
- 'aria-controls': group.contentId,
1237
- onClick: group.toggle,
1238
- // The action cluster overlays the label's right edge, so the label
1239
- // pads past it — far enough that the hover-revealed chevron lands
1240
- // one cluster gap (4px) to its left and the whole trailing run
1241
- // keeps a single rhythm. Cluster width is 24px per action plus 4px
1242
- // between them; add that gap again, less the 8px the group's
1243
- // padding already gives back: 28n + 6. The cluster is always
1244
- // visible, so the reservation is permanent.
1245
- style:
1246
- group.actionsCount > 0
1247
- ? ({ '--group-actions-pad': `${group.actionsCount * 28 + 6}px` } as CSSProperties)
1248
- : undefined,
1249
- className: cn(
1250
- 'flex h-8 w-full shrink-0 cursor-pointer select-none items-center gap-2 px-2 text-left text-muted-foreground/70 outline-none',
1251
- 'transition-colors duration-fast hover:text-muted-foreground',
1252
- group.actionsCount > 0 && 'pr-[var(--group-actions-pad)]',
1253
- 'focus-visible:ring-1 focus-visible:ring-focus-ring',
1254
- shape.item,
1255
- sizeVariant === 'compact' ? 'text-caption-compact' : 'text-caption',
1256
- className,
1257
- ),
1258
- ...props,
1259
- },
1260
- <>
1261
- {labelContent}
1262
- {/* The chevron occupies an action-sized box, so it reads as one more
1263
- icon in the row rather than a smaller glyph tacked on the end.
1264
- One chevron-right glyph for both states, sprung 90° to point
1265
- down while the group is open — the motion wrapper is what
1266
- animates: Tailwind's rotate-* sets the standalone CSS `rotate`
1267
- property, which transition-transform never covers. While open
1268
- the whole box collapses to zero width at rest so the label text
1269
- keeps the full row; hover/focus (or an open action popup)
1270
- reveals it. Collapsed keeps it visible as the reopen cue. */}
1271
- {/* No width/opacity transition: animating the box's width slides
1272
- the glyph in from the side — the chevron should simply be
1273
- there once the header is hovered. */}
1274
- <span
1275
- className={cn(
1276
- 'ml-auto flex h-6 shrink-0 items-center justify-center overflow-hidden',
1277
- group.open
1278
- ? 'w-0 opacity-0 group-hover/group-header:w-6 group-hover/group-header:opacity-100 group-focus-within/group-header:w-6 group-focus-within/group-header:opacity-100 group-has-[[data-sidebar=group-action]:is([data-state=open],[data-popup-open],[aria-expanded=true])]/group-header:w-6 group-has-[[data-sidebar=group-action]:is([data-state=open],[data-popup-open],[aria-expanded=true])]/group-header:opacity-100 pointer-coarse:w-6 pointer-coarse:opacity-100'
1279
- : 'w-6 opacity-100',
1280
- )}
1281
- >
1282
- <motion.span className="inline-flex" animate={{ rotate: group.open ? 90 : 0 }} transition={spring.fast}>
1283
- <ChevronRightIcon size={sizeClasses.icon} strokeWidth={1.5} className="shrink-0" />
1284
- </motion.span>
1285
- </span>
1286
- </>,
1287
- )
1288
- }
1289
-
1290
- return slotElement(
1291
- template,
1292
- 'div',
1293
- {
1294
- ref: ref as Ref<HTMLElement>,
1295
- 'data-sidebar': 'group-label',
1296
- className: cn(
1297
- 'flex h-8 shrink-0 items-center gap-2 px-2 text-muted-foreground/70 outline-none',
1298
- sizeVariant === 'compact' ? 'text-caption-compact' : 'text-caption',
1299
- className,
1300
- ),
1301
- ...props,
1302
- },
1303
- labelContent,
1304
- )
1305
- },
1306
- )
1307
- SidebarGroupLabel.displayName = 'SidebarGroupLabel'
1308
-
1309
- export interface SidebarGroupActionProps extends HTMLAttributes<HTMLButtonElement> {
1310
- render?: ReactElement
1311
- asChild?: boolean
1312
- }
1313
-
1314
- /** True while rendering inside a SidebarGroupActions cluster, where each
1315
- * action sits in the flex row instead of positioning itself absolutely. */
1316
- const GroupActionsContext = createContext(false)
1317
-
1318
- const SidebarGroupAction = forwardRef<HTMLButtonElement, SidebarGroupActionProps>(
1319
- ({ className, render, asChild, children, ...props }, ref) => {
1320
- const shape = useShape()
1321
- const sizeClasses = useSize()
1322
- const inCluster = useContext(GroupActionsContext)
1323
- const { template, content } = resolveSlotTemplate(render, asChild, children)
1324
- return slotElement(
1325
- template,
1326
- 'button',
1327
- {
1328
- ref: ref as Ref<HTMLElement>,
1329
- type: template ? undefined : 'button',
1330
- 'data-sidebar': 'group-action',
1331
- className: cn(
1332
- inCluster
1333
- ? 'relative flex size-6 items-center justify-center text-muted-foreground outline-none'
1334
- : // size-6 matches the rows' action hit-box, and right-3.5 puts that
1335
- // 24px box's centre 26px from the sidebar's inner edge — the axis
1336
- // the rows' badges and actions already sit on.
1337
- 'absolute right-3.5 top-3 flex size-6 items-center justify-center text-muted-foreground outline-none',
1338
- 'hover:bg-hover hover:text-foreground transition-colors duration-fast',
1339
- 'focus-visible:ring-1 focus-visible:ring-focus-ring',
1340
- // Normalize bare icons to the site's 1.5 stroke (library defaults
1341
- // vary), thickening to 2 on hover — the same treatment Button's
1342
- // icon-only span applies.
1343
- '[&_svg]:size-[var(--icon-size)] [&_svg]:shrink-0 [&_svg]:stroke-[1.5] [&_svg]:transition-[stroke-width] [&_svg]:duration-fast hover:[&_svg]:stroke-[2]',
1344
- shape.item,
1345
- className,
1346
- ),
1347
- ...props,
1348
- // After ...props: the spread would otherwise replace this object
1349
- // wholesale and drop the icon-size the glyph is sized from.
1350
- style: {
1351
- ...({ '--icon-size': `${sizeClasses.icon}px` } as CSSProperties),
1352
- ...(props.style ?? {}),
1353
- },
1354
- },
1355
- content,
1356
- )
1357
- },
1358
- )
1359
- SidebarGroupAction.displayName = 'SidebarGroupAction'
1360
-
1361
- /** Header action cluster: 1–3 SidebarGroupActions laid out in a row over the
1362
- * group label's right edge. Use instead of a lone SidebarGroupAction when a
1363
- * section needs several controls. */
1364
- export type SidebarGroupActionsProps = HTMLAttributes<HTMLDivElement>
1365
-
1366
- const SidebarGroupActions = forwardRef<HTMLDivElement, SidebarGroupActionsProps>(
1367
- ({ className, children, ...props }, ref) => {
1368
- return (
1369
- <div
1370
- ref={ref}
1371
- data-sidebar="group-actions"
1372
- className={cn(
1373
- // right-3.5 lands the last 24px action's centre 26px from the
1374
- // sidebar's inner edge — the rows' badge/action axis, so the header's
1375
- // controls line up with the column below them.
1376
- 'absolute right-3.5 top-2 z-10 flex h-8 items-center gap-1',
1377
- className,
1378
- )}
1379
- {...props}
1380
- >
1381
- <GroupActionsContext.Provider value={true}>{children}</GroupActionsContext.Provider>
1382
- </div>
1383
- )
1384
- },
1385
- )
1386
- SidebarGroupActions.displayName = 'SidebarGroupActions'
1387
-
1388
- const SidebarGroupContent = forwardRef<HTMLDivElement, SidebarSectionProps>(({ className, ...props }, ref) => (
1389
- <div ref={ref} data-sidebar="group-content" className={cn('w-full', className)} {...props} />
1390
- ))
1391
- SidebarGroupContent.displayName = 'SidebarGroupContent'
1392
-
1393
- export {
1394
- SidebarFooter,
1395
- SidebarGroup,
1396
- SidebarGroupAction,
1397
- SidebarGroupActions,
1398
- SidebarGroupContent,
1399
- SidebarGroupLabel,
1400
- SidebarHeader,
1401
- SidebarInput,
1402
- SidebarInset,
1403
- SidebarProvider,
1404
- SidebarRail,
1405
- SidebarSeparator,
1406
- SidebarShell,
1407
- SidebarTrigger,
1408
- }