cus-base-ui 0.2.1

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 (317) hide show
  1. package/README.md +55 -0
  2. package/README_CLI.md +51 -0
  3. package/dist/assets/abap-BZhW0Cx0.js +1 -0
  4. package/dist/assets/actionscript-3-LeBFK6Bh.js +1 -0
  5. package/dist/assets/ada-NqWpZflS.js +1 -0
  6. package/dist/assets/andromeeda-Bz6Fhh1J.js +1 -0
  7. package/dist/assets/angular-html-BTaiG3kK.js +1 -0
  8. package/dist/assets/angular-ts-BTxRTU6v.js +1 -0
  9. package/dist/assets/apache-CUjmQNca.js +1 -0
  10. package/dist/assets/apex-Lnt0wCMk.js +1 -0
  11. package/dist/assets/apl-BMP17s5Y.js +1 -0
  12. package/dist/assets/applescript-DqDANhM3.js +1 -0
  13. package/dist/assets/ara-B57Nv7hO.js +1 -0
  14. package/dist/assets/asciidoc-BJu0_1of.js +1 -0
  15. package/dist/assets/asm-CuWCE7za.js +1 -0
  16. package/dist/assets/astro-BFlLe0Jt.js +1 -0
  17. package/dist/assets/aurora-x-C-FyNReN.js +1 -0
  18. package/dist/assets/awk-CcQf-hTf.js +1 -0
  19. package/dist/assets/ayu-dark-DTz3rt1D.js +1 -0
  20. package/dist/assets/ayu-light-4_lDvMFX.js +1 -0
  21. package/dist/assets/ayu-mirage-DEK31dHd.js +1 -0
  22. package/dist/assets/ballerina-CMoGdttB.js +1 -0
  23. package/dist/assets/base-80a1f760-CWBUL8Qn.js +1 -0
  24. package/dist/assets/bat-Du2vR4-z.js +1 -0
  25. package/dist/assets/beancount-Bi9B10Ux.js +1 -0
  26. package/dist/assets/berry-DGTfJRqr.js +1 -0
  27. package/dist/assets/bibtex-DcCCuWNc.js +1 -0
  28. package/dist/assets/bicep-DLKVW3S6.js +1 -0
  29. package/dist/assets/bird2-F7TUYfu4.js +1 -0
  30. package/dist/assets/blade-kxW2wQoS.js +1 -0
  31. package/dist/assets/bsl-DoLiY7vL.js +1 -0
  32. package/dist/assets/c-CjOo_kFY.js +1 -0
  33. package/dist/assets/c3-Sv48Lcih.js +1 -0
  34. package/dist/assets/cadence-DISWvDw3.js +1 -0
  35. package/dist/assets/cairo-D_2aS-5Z.js +1 -0
  36. package/dist/assets/catppuccin-frappe-B21liG2_.js +1 -0
  37. package/dist/assets/catppuccin-latte-CNvibSqZ.js +1 -0
  38. package/dist/assets/catppuccin-macchiato-Cx6PrsL1.js +1 -0
  39. package/dist/assets/catppuccin-mocha-BIm4AS9Y.js +1 -0
  40. package/dist/assets/clarity-BV8oI-Co.js +1 -0
  41. package/dist/assets/clojure-DImyyA30.js +1 -0
  42. package/dist/assets/cmake-COi4JEwX.js +1 -0
  43. package/dist/assets/cobol-DPcHQ5ld.js +1 -0
  44. package/dist/assets/codeowners-CIUzB3xZ.js +1 -0
  45. package/dist/assets/codeql-DpjoiHFk.js +1 -0
  46. package/dist/assets/coffee-B6eTlRtE.js +1 -0
  47. package/dist/assets/common-lisp-D9vmlDeK.js +1 -0
  48. package/dist/assets/consoleHook-59e792cb-CZnef-bR.js +2 -0
  49. package/dist/assets/coq-DZ8BF_1-.js +1 -0
  50. package/dist/assets/cpp-KQ_I6Shn.js +1 -0
  51. package/dist/assets/crystal-BZeo3Lqr.js +1 -0
  52. package/dist/assets/csharp-dHPIw-bc.js +1 -0
  53. package/dist/assets/css-CZs88HIP.js +1 -0
  54. package/dist/assets/csv-BGa2_mHk.js +1 -0
  55. package/dist/assets/cue-DR2zNE_H.js +1 -0
  56. package/dist/assets/cypher-Dg5YFlvT.js +1 -0
  57. package/dist/assets/d-DrZsIShC.js +1 -0
  58. package/dist/assets/dark-plus-B-5tgoOG.js +1 -0
  59. package/dist/assets/dart-DsQLQS9X.js +1 -0
  60. package/dist/assets/dax-CXg_sVxN.js +1 -0
  61. package/dist/assets/desktop-N0lvQBra.js +1 -0
  62. package/dist/assets/diff-D7s5hVgG.js +1 -0
  63. package/dist/assets/docker-BA8qRwzp.js +1 -0
  64. package/dist/assets/dotenv-BFGjaxJy.js +1 -0
  65. package/dist/assets/dracula-DRURUhCs.js +1 -0
  66. package/dist/assets/dracula-soft-Dp6xOjRx.js +1 -0
  67. package/dist/assets/dream-maker-2XBfktlT.js +1 -0
  68. package/dist/assets/edge-BGjX0NsG.js +1 -0
  69. package/dist/assets/elixir-ZT3zz1k7.js +1 -0
  70. package/dist/assets/elm-BcCDQdX7.js +1 -0
  71. package/dist/assets/emacs-lisp-D5CEN_ek.js +1 -0
  72. package/dist/assets/erb-B0Ylddxe.js +1 -0
  73. package/dist/assets/erlang-CQ_9b5Q0.js +1 -0
  74. package/dist/assets/everforest-dark-D0e45-qh.js +1 -0
  75. package/dist/assets/everforest-light-CV-9cMo-.js +1 -0
  76. package/dist/assets/fennel-CCh9rfhR.js +1 -0
  77. package/dist/assets/fish-juOF7Euk.js +1 -0
  78. package/dist/assets/fluent-DDP2E4tD.js +1 -0
  79. package/dist/assets/fortran-fixed-form-DNAmXaue.js +1 -0
  80. package/dist/assets/fortran-free-form-hUjoeXWd.js +1 -0
  81. package/dist/assets/fsharp-BQjqeKnO.js +1 -0
  82. package/dist/assets/gdresource-DVnnD8v8.js +1 -0
  83. package/dist/assets/gdscript-BzPcVPC8.js +1 -0
  84. package/dist/assets/gdshader-BdDwKR67.js +1 -0
  85. package/dist/assets/genie-qbJIkXcC.js +1 -0
  86. package/dist/assets/gherkin-Ch-BbHus.js +1 -0
  87. package/dist/assets/git-commit-BnOb2edR.js +1 -0
  88. package/dist/assets/git-rebase-CvpwEJrg.js +1 -0
  89. package/dist/assets/github-dark-B230dITH.js +1 -0
  90. package/dist/assets/github-dark-default-2TLSR-Px.js +1 -0
  91. package/dist/assets/github-dark-dimmed-DCC8Z3wE.js +1 -0
  92. package/dist/assets/github-dark-high-contrast-4CxjYhxn.js +1 -0
  93. package/dist/assets/github-light-ChJgJtUx.js +1 -0
  94. package/dist/assets/github-light-default-BebDo5t1.js +1 -0
  95. package/dist/assets/github-light-high-contrast-DufJ-VDq.js +1 -0
  96. package/dist/assets/gleam-Cyq4dQUt.js +1 -0
  97. package/dist/assets/glimmer-js-DH-fBOCR.js +1 -0
  98. package/dist/assets/glimmer-ts-kW54HLwU.js +1 -0
  99. package/dist/assets/glsl-Bmytusq6.js +1 -0
  100. package/dist/assets/gn-DgWw1A6X.js +1 -0
  101. package/dist/assets/gnuplot-BLSnNDQw.js +1 -0
  102. package/dist/assets/go-D753U82n.js +1 -0
  103. package/dist/assets/graphql-DeBU-cLI.js +1 -0
  104. package/dist/assets/groovy-C-Ecqvz2.js +1 -0
  105. package/dist/assets/gruvbox-dark-hard-Dx2sCn0Q.js +1 -0
  106. package/dist/assets/gruvbox-dark-medium-CXgyPHB1.js +1 -0
  107. package/dist/assets/gruvbox-dark-soft-DLfc9u0w.js +1 -0
  108. package/dist/assets/gruvbox-light-hard-D631YK5U.js +1 -0
  109. package/dist/assets/gruvbox-light-medium-CotNhrrp.js +1 -0
  110. package/dist/assets/gruvbox-light-soft-jFOOHYkF.js +1 -0
  111. package/dist/assets/hack-Dp6PdyaY.js +1 -0
  112. package/dist/assets/haml-Bb83gMJl.js +1 -0
  113. package/dist/assets/handlebars-4R2ucKwU.js +1 -0
  114. package/dist/assets/haskell-rmPPOegz.js +1 -0
  115. package/dist/assets/haxe-CKu_saXU.js +1 -0
  116. package/dist/assets/hcl-faVI7vm2.js +1 -0
  117. package/dist/assets/hjson-CO1T3zWN.js +1 -0
  118. package/dist/assets/hlsl-BhAWAfzJ.js +1 -0
  119. package/dist/assets/horizon-53_LdpdV.js +1 -0
  120. package/dist/assets/horizon-bright-CSJE0fg2.js +1 -0
  121. package/dist/assets/houston-BpRFjtYR.js +1 -0
  122. package/dist/assets/html-Ci7bSu-j.js +1 -0
  123. package/dist/assets/html-derivative-Bgjrt2Le.js +1 -0
  124. package/dist/assets/http-CzcxElj_.js +1 -0
  125. package/dist/assets/hurl-DU4lsuIz.js +1 -0
  126. package/dist/assets/hxml-BBMS41CJ.js +1 -0
  127. package/dist/assets/hy-BmdhWrf-.js +1 -0
  128. package/dist/assets/imba-CtNjQhN8.js +1 -0
  129. package/dist/assets/index-2HLs_nFj.css +1 -0
  130. package/dist/assets/index-599aeaf7-C4r7tddb.js +16 -0
  131. package/dist/assets/index-BOcAhqxV.js +1128 -0
  132. package/dist/assets/ini-v8Z5jhL_.js +1 -0
  133. package/dist/assets/java-CRTRlqfh.js +1 -0
  134. package/dist/assets/javascript-CNlkeRu4.js +1 -0
  135. package/dist/assets/jinja-Cmw44Rm8.js +1 -0
  136. package/dist/assets/jison-y7C7OqGi.js +1 -0
  137. package/dist/assets/json-TEdWkzyG.js +1 -0
  138. package/dist/assets/json5-Ay2rxHe-.js +1 -0
  139. package/dist/assets/jsonc-B5g7Bvk3.js +1 -0
  140. package/dist/assets/jsonl-D29jLPlx.js +1 -0
  141. package/dist/assets/jsonnet-et6t4-No.js +1 -0
  142. package/dist/assets/jssm-CfWVn5PB.js +1 -0
  143. package/dist/assets/jsx-CvVKWB9w.js +1 -0
  144. package/dist/assets/julia-CwCdIlc9.js +1 -0
  145. package/dist/assets/just-DsYr7DtD.js +1 -0
  146. package/dist/assets/kanagawa-dragon-BUEG3Nzw.js +1 -0
  147. package/dist/assets/kanagawa-lotus-BxmByTxd.js +1 -0
  148. package/dist/assets/kanagawa-wave-DiV3svz9.js +1 -0
  149. package/dist/assets/kdl-J3ti3M73.js +1 -0
  150. package/dist/assets/kotlin-DL7L1F2M.js +1 -0
  151. package/dist/assets/kusto-qibBLLok.js +1 -0
  152. package/dist/assets/laserwave-twIHjrzg.js +1 -0
  153. package/dist/assets/latex-BpmOboQG.js +1 -0
  154. package/dist/assets/lean-nIys3EXb.js +1 -0
  155. package/dist/assets/less-D3-GPjgt.js +1 -0
  156. package/dist/assets/light-plus-BLaDUeKu.js +1 -0
  157. package/dist/assets/liquid-De7rf4NV.js +1 -0
  158. package/dist/assets/llvm-DLvitYDc.js +1 -0
  159. package/dist/assets/log-CpfCrxX8.js +1 -0
  160. package/dist/assets/logo-BtGdk1-r.js +1 -0
  161. package/dist/assets/lua-BLofQ8Om.js +1 -0
  162. package/dist/assets/luau-BtzxLu36.js +1 -0
  163. package/dist/assets/make-7ApKNKSo.js +1 -0
  164. package/dist/assets/markdown-593J-HHD.js +1 -0
  165. package/dist/assets/marko-_ASnk5VL.js +1 -0
  166. package/dist/assets/material-theme-CfBPWgR2.js +1 -0
  167. package/dist/assets/material-theme-darker-CA8_s_rR.js +1 -0
  168. package/dist/assets/material-theme-lighter-CYlfxoXo.js +1 -0
  169. package/dist/assets/material-theme-ocean-BUi3vxXM.js +1 -0
  170. package/dist/assets/material-theme-palenight-DBwe4FQz.js +1 -0
  171. package/dist/assets/matlab-BW8IAV77.js +1 -0
  172. package/dist/assets/mdc-DvEA27F9.js +1 -0
  173. package/dist/assets/mdx-CThNn1AJ.js +1 -0
  174. package/dist/assets/mermaid-B1orlPdl.js +1 -0
  175. package/dist/assets/min-dark-CM4QSiY-.js +1 -0
  176. package/dist/assets/min-light-lnYMk6TN.js +1 -0
  177. package/dist/assets/mipsasm-BoF7lD-G.js +1 -0
  178. package/dist/assets/mojo-I8mrPp2q.js +1 -0
  179. package/dist/assets/monokai-CkHtS8AE.js +1 -0
  180. package/dist/assets/moonbit-7gV_EAWj.js +1 -0
  181. package/dist/assets/move-BC3qjKgo.js +1 -0
  182. package/dist/assets/narrat-CreRK8Of.js +1 -0
  183. package/dist/assets/nextflow-groovy-COkUU9Xo.js +1 -0
  184. package/dist/assets/nextflow-zEreFSl1.js +1 -0
  185. package/dist/assets/nginx-CV7Ml0NF.js +1 -0
  186. package/dist/assets/night-owl-DWPUUao2.js +1 -0
  187. package/dist/assets/night-owl-light-UWsCFnvo.js +1 -0
  188. package/dist/assets/nim-Bw6JkhsE.js +1 -0
  189. package/dist/assets/nix-vTa-TrnA.js +1 -0
  190. package/dist/assets/node-BaZpsMhE.js +4 -0
  191. package/dist/assets/nord-aoF20Emb.js +1 -0
  192. package/dist/assets/nushell-CBKIN0Df.js +1 -0
  193. package/dist/assets/objective-c-3fxkmuDp.js +1 -0
  194. package/dist/assets/objective-cpp-CEA4HtQL.js +1 -0
  195. package/dist/assets/ocaml-CQvEBP80.js +1 -0
  196. package/dist/assets/odin-CNF3G1pa.js +1 -0
  197. package/dist/assets/one-dark-pro-hYb6hiJM.js +1 -0
  198. package/dist/assets/one-light-CykW5KzK.js +1 -0
  199. package/dist/assets/openscad-CnqDNPox.js +1 -0
  200. package/dist/assets/pascal-Ds_J5C10.js +1 -0
  201. package/dist/assets/perl-Cghem8JI.js +1 -0
  202. package/dist/assets/php-Bv_uiLgZ.js +1 -0
  203. package/dist/assets/pkl-BHfXVEv0.js +1 -0
  204. package/dist/assets/plastic-gYsT69zC.js +1 -0
  205. package/dist/assets/plsql-BVshfseW.js +1 -0
  206. package/dist/assets/po-DGQTg6O8.js +1 -0
  207. package/dist/assets/poimandres-TB_8oCZj.js +1 -0
  208. package/dist/assets/polar-BDONHBaM.js +1 -0
  209. package/dist/assets/postcss-DTdSrw8q.js +1 -0
  210. package/dist/assets/powerquery-Bj3teieW.js +1 -0
  211. package/dist/assets/powershell-DzQXIeRD.js +1 -0
  212. package/dist/assets/prisma-e-tuwUHK.js +1 -0
  213. package/dist/assets/prolog-szw3pCXI.js +1 -0
  214. package/dist/assets/proto-BDxsyPZj.js +1 -0
  215. package/dist/assets/pug-Ctl01sr_.js +1 -0
  216. package/dist/assets/puppet-DIkUL05Q.js +1 -0
  217. package/dist/assets/purescript-6mbXpT_X.js +1 -0
  218. package/dist/assets/python-C72FlIEV.js +1 -0
  219. package/dist/assets/qml-DVeXuuO9.js +1 -0
  220. package/dist/assets/qmldir-jT2JFKaC.js +1 -0
  221. package/dist/assets/qss-CaOY579a.js +1 -0
  222. package/dist/assets/r-Hn-yDixW.js +1 -0
  223. package/dist/assets/racket-CL23vpaj.js +1 -0
  224. package/dist/assets/raku-D--0OsK6.js +1 -0
  225. package/dist/assets/razor-J_vDYOlw.js +1 -0
  226. package/dist/assets/red-DZRYD6K0.js +1 -0
  227. package/dist/assets/reg-CMx5SBwW.js +1 -0
  228. package/dist/assets/regexp-ClVttry8.js +1 -0
  229. package/dist/assets/rel-DYeuMJNs.js +1 -0
  230. package/dist/assets/riscv-DPgt2IM1.js +1 -0
  231. package/dist/assets/ron-CNUpRTUS.js +1 -0
  232. package/dist/assets/rose-pine-_Vxn4ntM.js +1 -0
  233. package/dist/assets/rose-pine-dawn-sY7FPYh0.js +1 -0
  234. package/dist/assets/rose-pine-moon-BIHtMi2S.js +1 -0
  235. package/dist/assets/rosmsg-efi2xErU.js +1 -0
  236. package/dist/assets/rst-nlfGseIM.js +1 -0
  237. package/dist/assets/ruby-DoVDlCwl.js +1 -0
  238. package/dist/assets/runtime-CQA7fIpc.js +1 -0
  239. package/dist/assets/rust-DnEBzrVL.js +1 -0
  240. package/dist/assets/sas-BcDSpQtn.js +1 -0
  241. package/dist/assets/sass-DP1OjaPs.js +1 -0
  242. package/dist/assets/scala-Bhuo3V1q.js +1 -0
  243. package/dist/assets/scheme-T2JZTR-5.js +1 -0
  244. package/dist/assets/scss-T4kOXV2Q.js +1 -0
  245. package/dist/assets/sdbl-DCrj75ur.js +1 -0
  246. package/dist/assets/shaderlab-C3scLHTE.js +1 -0
  247. package/dist/assets/shellscript-BunGOPZl.js +1 -0
  248. package/dist/assets/shellsession-C0IDaiU6.js +1 -0
  249. package/dist/assets/slack-dark-CVSkF4Cu.js +1 -0
  250. package/dist/assets/slack-ochin-CSE3d4AO.js +1 -0
  251. package/dist/assets/smalltalk-BAm-PEke.js +1 -0
  252. package/dist/assets/snazzy-light-lluQUjam.js +1 -0
  253. package/dist/assets/solarized-dark-ftCwpP34.js +1 -0
  254. package/dist/assets/solarized-light-44akI74N.js +1 -0
  255. package/dist/assets/solidity-CHjqn_02.js +1 -0
  256. package/dist/assets/soy-DroYVJzh.js +1 -0
  257. package/dist/assets/sparql-Ce4K2mvS.js +1 -0
  258. package/dist/assets/splunk-MwRh86cy.js +1 -0
  259. package/dist/assets/sql-glVt9tXx.js +1 -0
  260. package/dist/assets/ssh-config-d1rEeQrl.js +1 -0
  261. package/dist/assets/stata-C450JpKP.js +1 -0
  262. package/dist/assets/stylus-rQkaPWdf.js +1 -0
  263. package/dist/assets/surrealql-DadcZCP3.js +1 -0
  264. package/dist/assets/svelte-BT5ewu7M.js +1 -0
  265. package/dist/assets/swift-tyNOsy40.js +1 -0
  266. package/dist/assets/synthwave-84-BNJQQOX0.js +1 -0
  267. package/dist/assets/system-verilog-Bi8d1SdY.js +1 -0
  268. package/dist/assets/systemd-Cbw3HuPR.js +1 -0
  269. package/dist/assets/talonscript-RJjG5DH-.js +1 -0
  270. package/dist/assets/tasl-XZffQMVP.js +1 -0
  271. package/dist/assets/tcl-DDxomfuG.js +1 -0
  272. package/dist/assets/templ-Bs-UR7bT.js +1 -0
  273. package/dist/assets/terraform-C-a_z6EL.js +1 -0
  274. package/dist/assets/tex-0SQU_25t.js +1 -0
  275. package/dist/assets/tokyo-night-C5m3Iu1u.js +1 -0
  276. package/dist/assets/toml-Cpr_hoc4.js +1 -0
  277. package/dist/assets/ts-tags-DrI5ZCYg.js +1 -0
  278. package/dist/assets/tsv-0Nk6Vx0M.js +1 -0
  279. package/dist/assets/tsx-Cb1mPjps.js +1 -0
  280. package/dist/assets/turtle-Dm1TESdq.js +1 -0
  281. package/dist/assets/twig-DGOb00Fn.js +1 -0
  282. package/dist/assets/typescript-DfGIBSes.js +1 -0
  283. package/dist/assets/typespec-LRbCWivn.js +1 -0
  284. package/dist/assets/typst-C1_ki4iq.js +1 -0
  285. package/dist/assets/v-BxICDpo_.js +1 -0
  286. package/dist/assets/vala-BCAVMbt1.js +1 -0
  287. package/dist/assets/vb-C74Oc43Q.js +1 -0
  288. package/dist/assets/verilog-c8krZF_X.js +1 -0
  289. package/dist/assets/vesper-DVU53IPV.js +1 -0
  290. package/dist/assets/vhdl-C7FYgf5r.js +1 -0
  291. package/dist/assets/viml-1MXIIzly.js +1 -0
  292. package/dist/assets/vitesse-black-Ca3I6Jeb.js +1 -0
  293. package/dist/assets/vitesse-dark-CGGsP_ry.js +1 -0
  294. package/dist/assets/vitesse-light-BzG99g10.js +1 -0
  295. package/dist/assets/vue-DqFNHlvr.js +1 -0
  296. package/dist/assets/vue-html-CkrO3fEY.js +1 -0
  297. package/dist/assets/vue-vine-BYYhuvKb.js +1 -0
  298. package/dist/assets/vyper-Dkgft_3R.js +1 -0
  299. package/dist/assets/wasm-CE7OjoBR.js +1 -0
  300. package/dist/assets/wasm-D_MQmneJ.js +1 -0
  301. package/dist/assets/wenyan-CxlvaCGE.js +1 -0
  302. package/dist/assets/wgsl-BAe_9Dgb.js +1 -0
  303. package/dist/assets/wikitext-DxZ8uM9m.js +1 -0
  304. package/dist/assets/wit-BKLwvvdc.js +1 -0
  305. package/dist/assets/wolfram-DnaeypqR.js +1 -0
  306. package/dist/assets/xml-DdarD9Fy.js +1 -0
  307. package/dist/assets/xsl-l8oVQM8u.js +1 -0
  308. package/dist/assets/yaml-BM_YwV-H.js +1 -0
  309. package/dist/assets/zenscript-spOPJke_.js +1 -0
  310. package/dist/assets/zig-Dr5aj8gQ.js +1 -0
  311. package/dist/favicon.svg +1 -0
  312. package/dist/icons.svg +24 -0
  313. package/dist/index.html +29 -0
  314. package/package.json +86 -0
  315. package/registry.json +593 -0
  316. package/scripts/build-registry.ts +209 -0
  317. package/scripts/ui-cli.ts +259 -0
package/registry.json ADDED
@@ -0,0 +1,593 @@
1
+ {
2
+ "core": {
3
+ "dependencies": [
4
+ "@base-ui/react",
5
+ "clsx",
6
+ "tailwind-merge",
7
+ "tailwind-variants",
8
+ "tailwindcss-animate",
9
+ "@tailwindcss/vite",
10
+ "autoprefixer",
11
+ "tailwindcss",
12
+ "postcss"
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "src/lib/utils/cn.ts",
17
+ "content": "import { clsx, type ClassValue } from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\n/**\r\n * Hàm cn kết hợp giữa:\r\n * 1. clsx: Cho phép truyền class theo kiểu object, array, conditional (true/false)\r\n * 2. tailwind-merge: Đảm bảo các class Tailwind sau cùng sẽ ghi đè các class trước đó một cách chính xác\r\n */\r\nexport function cn(...inputs: ClassValue[]) {\r\n return twMerge(clsx(inputs));\r\n}"
18
+ },
19
+ {
20
+ "path": "src/styles/index.css",
21
+ "content": "@import \"tailwindcss\";\r\n@plugin \"tailwindcss-animate\";\r\n@custom-variant dark (&:where(.dark, .dark *));\r\n\r\n/*\r\n View Transitions API: Tắt animation mặc định (fade cross-dissolve).\r\n ThemeToggle sẽ tự định nghĩa clip-path ripple animation thay thế.\r\n*/\r\n::view-transition-old(root),\r\n::view-transition-new(root) {\r\n animation: none;\r\n mix-blend-mode: normal;\r\n}\r\n\r\n::view-transition-old(root) {\r\n z-index: 1;\r\n}\r\n\r\n::view-transition-new(root) {\r\n z-index: 9999;\r\n}\r\n\r\n\r\n@theme {\r\n --animate-ping: ping 1.5s linear infinite; /* Chỉnh cho nó chạy chậm lại */\r\n\r\n\r\n\r\n --color-background: var(--background);\r\n --color-foreground: var(--foreground);\r\n\r\n --color-primary: var(--primary);\r\n --color-primary-foreground: var(--primary-foreground);\r\n\r\n --color-secondary: var(--secondary);\r\n --color-secondary-foreground: var(--secondary-foreground);\r\n\r\n --color-muted: var(--muted);\r\n --color-muted-foreground: var(--muted-foreground);\r\n\r\n --color-accent: var(--accent);\r\n --color-accent-foreground: var(--accent-foreground);\r\n\r\n --color-switch-background: var(--switch-background);\r\n\r\n --color-border: var(--border);\r\n\r\n --color-success: var(--success);\r\n --color-success-foreground: var(--success-foreground);\r\n\r\n --color-warning: var(--warning);\r\n --color-warning-foreground: var(--warning-foreground);\r\n\r\n --color-destructive: var(--danger);\r\n --color-destructive-foreground: var(--danger-foreground);\r\n\r\n --color-danger: var(--danger);\r\n --color-danger-foreground: var(--danger-foreground);\r\n\r\n --color-ring: var(--ring);\r\n --color-input: var(--input);\r\n\r\n --color-chart-1: var(--chart-1);\r\n --color-chart-2: var(--chart-2);\r\n --color-chart-3: var(--chart-3);\r\n --color-chart-4: var(--chart-4);\r\n --color-chart-5: var(--chart-5);\r\n\r\n --color-popover: var(--popover);\r\n --color-popover-foreground: var(--popover-foreground);\r\n\r\n --color-sidebar: var(--sidebar);\r\n --color-sidebar-foreground: var(--sidebar-foreground);\r\n --color-sidebar-border: var(--sidebar-border);\r\n --color-sidebar-accent: var(--sidebar-accent);\r\n --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\r\n --color-sidebar-ring: var(--sidebar-ring);\r\n\r\n --radius-sm: 0.125rem;\r\n --radius-md: 0.375rem;\r\n --radius-lg: 0.5rem;\r\n --radius-xl: 1rem;\r\n\r\n --animate-spin-slow: spin 3s linear infinite;\r\n --animate-progress-stripes: progress-stripes 1s linear infinite;\r\n\r\n @keyframes progress-stripes {\r\n from { background-position: 1rem 0; }\r\n to { background-position: 0 0; }\r\n }\r\n}\r\n\r\n@layer base {\r\n :root {\r\n /* Light Theme - Clean White */\r\n --background: #ffffff;\r\n --foreground: #0f172a;\r\n \r\n --primary: #2f27ce;\r\n --primary-foreground: #ffffff;\r\n \r\n --secondary: #dedcff; /* Vẫn giữ nét màu Light cũ nhưng tinh chỉnh nhẹ */\r\n --secondary-foreground: #2f27ce;\r\n \r\n --muted: #f8fafc;\r\n --muted-foreground: #64748b;\r\n \r\n --accent: #f1f5f9;\r\n --accent-foreground: #0f172a;\r\n \r\n --switch-background: #cbd5e1;\r\n --border: #e2e8f0;\r\n \r\n --success: #10b981;\r\n --success-foreground: #ffffff;\r\n \r\n --warning: #f59e0b;\r\n --warning-foreground: #ffffff;\r\n \r\n --danger: #ef4444;\r\n --danger-foreground: #ffffff;\r\n\r\n --popover: #ffffff;\r\n --popover-foreground: #0f172a;\r\n\r\n --sidebar: #f8fafc;\r\n --sidebar-foreground: #0f172a;\r\n --sidebar-border: #7a7a7a;\r\n --sidebar-accent: #f1f5f9;\r\n --sidebar-accent-foreground: #2f27ce;\r\n --sidebar-ring: #2f27ce;\r\n }\r\n\r\n .dark {\r\n /* Dark Theme - Deep Space Slate (Chuyên nghiệp & Hiện đại) */\r\n --background: #09090b; /* Very deep zinc/slate */\r\n --foreground: #f8fafc;\r\n \r\n --primary: #6366f1; /* Bright Indigo for pop */\r\n --primary-foreground: #ffffff;\r\n \r\n --secondary: #1e293b; /* Slate 800 */\r\n --secondary-foreground: #f8fafc;\r\n \r\n --muted: #0f172a; /* Slate 900 for subdued bg */\r\n --muted-foreground: #94a3b8; /* Slate 400 for text */\r\n \r\n --accent: #1e293b;\r\n --accent-foreground: #f8fafc;\r\n \r\n --switch-background: #334155;\r\n --border: #334155; /* Slate 700 */\r\n \r\n --success: #10b981;\r\n --success-foreground: #ffffff;\r\n \r\n --warning: #f59e0b;\r\n --warning-foreground: #ffffff;\r\n \r\n --danger: #ef4444;\r\n --danger-foreground: #ffffff;\r\n\r\n --ring: #6366f1;\r\n --input: #334155;\r\n\r\n --chart-1: #fb7185;\r\n --chart-2: #6366f1;\r\n --chart-3: #34d399;\r\n --chart-4: #fbbf24;\r\n --chart-5: #818cf8;\r\n\r\n --popover: #0f172a;\r\n --popover-foreground: #f8fafc;\r\n\r\n --sidebar: #0f172a;\r\n --sidebar-foreground: #f8fafc;\r\n --sidebar-border: #334155;\r\n --sidebar-accent: #1e293b;\r\n --sidebar-accent-foreground: #818cf8;\r\n --sidebar-ring: #6366f1;\r\n }\r\n}\r\n\r\n\r\n@layer base {\r\n * {\r\n border-color: var(--border);\r\n }\r\n\r\n html, body {\r\n margin: 0;\r\n padding: 0;\r\n background-color: var(--background);\r\n color: var(--foreground);\r\n font-family: 'Inter', system-ui, sans-serif;\r\n overflow: hidden; /* Khóa scroll tổng để dùng nội bộ */\r\n height: 100%;\r\n color-scheme: light;\r\n transition: background-color 0.3s ease, color 0.3s ease;\r\n }\r\n\r\n html.dark {\r\n color-scheme: dark;\r\n }\r\n\r\n /* Fix dải trắng khi mở modal/dialog trong các thư viện (Base UI, Radix) */\r\n body[style*=\"overflow: hidden\"],\r\n body[data-scroll-locked] {\r\n padding-right: 0 !important;\r\n margin-right: 0 !important;\r\n }\r\n\r\n /* Đảm bảo Backdrop luôn phủ kín màn hình bất chấp các tính toán của thư viện */\r\n [data-base-ui-dialog-backdrop],\r\n .base-ui-backdrop,\r\n [role=\"presentation\"] > div[style*=\"fixed\"] {\r\n width: 100vw !important;\r\n height: 100vh !important;\r\n left: 0 !important;\r\n top: 0 !important;\r\n right: 0 !important;\r\n bottom: 0 !important;\r\n }\r\n\r\n /* Custom Scrollbar - Sleek & Modern */\r\n ::-webkit-scrollbar {\r\n width: 8px;\r\n height: 8px;\r\n }\r\n\r\n ::-webkit-scrollbar-track {\r\n background: transparent;\r\n }\r\n\r\n ::-webkit-scrollbar-thumb {\r\n background: #cbd5e1; /* slate-300 */\r\n border-radius: 10px;\r\n border: 2px solid transparent;\r\n background-clip: content-box;\r\n }\r\n\r\n .dark ::-webkit-scrollbar-thumb {\r\n background: #334155; /* slate-700 */\r\n }\r\n\r\n ::-webkit-scrollbar-thumb:hover {\r\n background: #94a3b8; /* slate-400 */\r\n background-clip: content-box;\r\n }\r\n\r\n .dark ::-webkit-scrollbar-thumb:hover {\r\n background: #475569; /* slate-600 */\r\n }\r\n\r\n /* Firefox */\r\n * {\r\n scrollbar-width: thin;\r\n scrollbar-color: #cbd5e1 transparent;\r\n }\r\n\r\n .dark * {\r\n scrollbar-color: #334155 transparent;\r\n }\r\n}\r\n\r\n/* Ensure data-state=\"checked\" always works for background */\r\n[data-state=\"checked\"] {\r\n &.bg-primary {\r\n background-color: var(--primary);\r\n }\r\n}\r\n\r\n[data-state=\"indeterminate\"] {\r\n &.bg-primary {\r\n background-color: var(--primary);\r\n }\r\n}"
22
+ }
23
+ ]
24
+ },
25
+ "components": {
26
+ "accordion": {
27
+ "name": "accordion",
28
+ "dependencies": [
29
+ "@base-ui/react",
30
+ "lucide-react",
31
+ "tailwind-variants"
32
+ ],
33
+ "internalDependencies": [],
34
+ "files": [
35
+ {
36
+ "path": "src/components/ui/accordion/Accordion.tsx",
37
+ "content": "import * as React from 'react';\r\nimport { Accordion as BaseAccordion } from '@base-ui/react';\r\nimport { ChevronDown } from 'lucide-react';\r\nimport { tv } from 'tailwind-variants';\r\n\r\nconst accordionVariants = tv({\r\n slots: {\r\n root: 'w-full',\r\n item: 'border-b border-border/50 last:border-0',\r\n header: 'flex',\r\n trigger: 'flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:text-primary hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 [&[data-panel-open]>svg]:rotate-180',\r\n panel: 'overflow-hidden text-sm data-[open]:animate-accordion-down data-[closed]:animate-accordion-up transition-all',\r\n }\r\n});\r\n\r\nconst { root, item, header, trigger, panel } = accordionVariants();\r\n\r\nexport const Accordion = React.forwardRef<React.ElementRef<typeof BaseAccordion.Root>, Omit<React.ComponentPropsWithoutRef<typeof BaseAccordion.Root>, 'className'> & { className?: string }>(\r\n ({ className, ...props }, ref) => (\r\n <BaseAccordion.Root ref={ref} className={root({ className })} {...props} />\r\n )\r\n)\r\nAccordion.displayName = 'Accordion';\r\n\r\nexport const AccordionItem = React.forwardRef<React.ElementRef<typeof BaseAccordion.Item>, Omit<React.ComponentPropsWithoutRef<typeof BaseAccordion.Item>, 'className'> & { className?: string }>(\r\n ({ className, ...props }, ref) => (\r\n <BaseAccordion.Item ref={ref} className={item({ className })} {...props} />\r\n )\r\n)\r\nAccordionItem.displayName = 'AccordionItem';\r\n\r\nexport const AccordionTrigger = React.forwardRef<React.ElementRef<typeof BaseAccordion.Trigger>, Omit<React.ComponentPropsWithoutRef<typeof BaseAccordion.Trigger>, 'className'> & { className?: string; hideChevron?: boolean }>(\r\n ({ className, children, hideChevron, ...props }, ref) => (\r\n <BaseAccordion.Header className={header()}>\r\n <BaseAccordion.Trigger ref={ref} className={trigger({ className })} {...props}>\r\n {children}\r\n {!hideChevron && <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />}\r\n </BaseAccordion.Trigger>\r\n </BaseAccordion.Header>\r\n )\r\n)\r\nAccordionTrigger.displayName = 'AccordionTrigger';\r\n\r\nexport const AccordionContent = React.forwardRef<React.ElementRef<typeof BaseAccordion.Panel>, Omit<React.ComponentPropsWithoutRef<typeof BaseAccordion.Panel>, 'className'> & { className?: string }>(\r\n ({ className, children, ...props }, ref) => (\r\n <BaseAccordion.Panel ref={ref} className={panel({ className })} {...props}>\r\n <div className=\"pb-4 pt-0\">{children}</div>\r\n </BaseAccordion.Panel>\r\n )\r\n)\r\nAccordionContent.displayName = 'AccordionContent';\r\n"
38
+ }
39
+ ]
40
+ },
41
+ "alert": {
42
+ "name": "alert",
43
+ "dependencies": [
44
+ "lucide-react",
45
+ "tailwind-variants"
46
+ ],
47
+ "internalDependencies": [],
48
+ "files": [
49
+ {
50
+ "path": "src/components/ui/alert/Alert.tsx",
51
+ "content": "import * as React from 'react';\r\nimport { AlertCircle, CheckCircle2, ChevronRight, Info, XCircle } from 'lucide-react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst alertVariants = tv({\r\n base: 'relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground',\r\n variants: {\r\n variant: {\r\n default: 'bg-background text-foreground',\r\n destructive: 'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',\r\n success: 'border-success/50 text-success dark:border-success [&>svg]:text-success',\r\n warning: 'border-warning/50 text-warning dark:border-warning [&>svg]:text-warning',\r\n info: 'border-blue-500/50 text-blue-500 dark:border-blue-500 [&>svg]:text-blue-500',\r\n }\r\n },\r\n defaultVariants: {\r\n variant: 'default'\r\n }\r\n});\r\n\r\nconst Alert = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>>(\r\n ({ className, variant, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n role=\"alert\"\r\n className={alertVariants({ variant, className })}\r\n {...props}\r\n />\r\n )\r\n);\r\nAlert.displayName = 'Alert';\r\n\r\nconst AlertTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\r\n ({ className, ...props }, ref) => (\r\n <h5\r\n ref={ref}\r\n className={cn('mb-1 font-medium leading-none tracking-tight', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nAlertTitle.displayName = 'AlertTitle';\r\n\r\nconst AlertDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n className={cn('text-sm [&_p]:leading-relaxed', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nAlertDescription.displayName = 'AlertDescription';\r\n\r\nexport { Alert, AlertTitle, AlertDescription };\r\n"
52
+ }
53
+ ]
54
+ },
55
+ "alert-dialog": {
56
+ "name": "alert-dialog",
57
+ "dependencies": [
58
+ "@base-ui/react",
59
+ "tailwind-variants"
60
+ ],
61
+ "internalDependencies": [],
62
+ "files": [
63
+ {
64
+ "path": "src/components/ui/alert-dialog/AlertDialog.tsx",
65
+ "content": "import * as React from 'react';\r\nimport { AlertDialog as BaseAlertDialog } from '@base-ui/react';\r\nimport { tv } from 'tailwind-variants';\r\n\r\nconst alertDialogVariants = tv({\r\n slots: {\r\n overlay: 'fixed inset-0 z-50 bg-black/50 backdrop-blur-sm data-starting:animate-in data-ending:animate-out data-ending:fade-out-0 data-starting:fade-in-0',\r\n content: 'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 data-starting:animate-in data-ending:animate-out data-ending:fade-out-0 data-starting:fade-in-0 data-ending:zoom-out-95 data-starting:zoom-in-95 sm:rounded-lg',\r\n header: 'flex flex-col space-y-2 text-center sm:text-left',\r\n footer: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-2',\r\n title: 'text-lg font-semibold leading-none tracking-tight',\r\n description: 'text-sm text-muted-foreground',\r\n }\r\n});\r\n\r\nconst { overlay, content, header, footer, title, description } = alertDialogVariants();\r\n\r\nexport interface AlertDialogProps extends React.ComponentPropsWithoutRef<typeof BaseAlertDialog.Root> {\r\n trigger?: React.ReactNode;\r\n headerTitle?: string;\r\n headerDescription?: string;\r\n cancelContent?: React.ReactNode;\r\n actionContent?: React.ReactNode;\r\n}\r\n\r\nconst AlertDialog = React.forwardRef<React.ElementRef<typeof BaseAlertDialog.Root>, AlertDialogProps>(\r\n ({ trigger, headerTitle, headerDescription, cancelContent, actionContent, ...props }, ref) => {\r\n return (\r\n <BaseAlertDialog.Root {...props}>\r\n {trigger && <BaseAlertDialog.Trigger render={trigger as React.ReactElement} />}\r\n <BaseAlertDialog.Portal>\r\n <BaseAlertDialog.Backdrop className={overlay()} />\r\n <BaseAlertDialog.Popup className={content()}>\r\n {(headerTitle || headerDescription) && (\r\n <div className={header()}>\r\n {headerTitle && <BaseAlertDialog.Title className={title()}>{headerTitle}</BaseAlertDialog.Title>}\r\n {headerDescription && <BaseAlertDialog.Description className={description()}>{headerDescription}</BaseAlertDialog.Description>}\r\n </div>\r\n )}\r\n \r\n <div className={footer()}>\r\n {cancelContent && (\r\n <BaseAlertDialog.Close render={cancelContent as React.ReactElement} />\r\n )}\r\n {actionContent && (\r\n actionContent\r\n )}\r\n </div>\r\n </BaseAlertDialog.Popup>\r\n </BaseAlertDialog.Portal>\r\n </BaseAlertDialog.Root>\r\n );\r\n }\r\n);\r\n\r\nAlertDialog.displayName = 'AlertDialog';\r\n\r\nexport { AlertDialog };\r\n"
66
+ }
67
+ ]
68
+ },
69
+ "avatar": {
70
+ "name": "avatar",
71
+ "dependencies": [
72
+ "tailwind-variants"
73
+ ],
74
+ "internalDependencies": [],
75
+ "files": [
76
+ {
77
+ "path": "src/components/ui/avatar/Avatar.tsx",
78
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst avatarVariants = tv({\r\n base: 'relative flex shrink-0 overflow-hidden rounded-full items-center justify-center bg-secondary text-secondary-foreground outline-none',\r\n variants: {\r\n size: {\r\n sm: 'h-8 w-8 text-xs',\r\n md: 'h-10 w-10 text-sm',\r\n lg: 'h-12 w-12 text-base',\r\n xl: 'h-16 w-16 text-lg',\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface AvatarProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof avatarVariants> {\r\n src?: string;\r\n alt?: string;\r\n fallback?: string;\r\n}\r\n\r\nconst Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(\r\n ({ className, size, src, alt, fallback, ...props }, ref) => {\r\n const [hasError, setHasError] = React.useState(false);\r\n\r\n return (\r\n <div ref={ref} className={avatarVariants({ size, className })} {...props}>\r\n {src && !hasError ? (\r\n <img\r\n src={src}\r\n alt={alt || \"Avatar\"}\r\n className=\"aspect-square h-full w-full object-cover\"\r\n onError={() => setHasError(true)}\r\n />\r\n ) : (\r\n <span className=\"font-medium uppercase tracking-wider\">\r\n {fallback || (alt ? alt.substring(0, 2) : '??')}\r\n </span>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\nAvatar.displayName = 'Avatar';\r\n\r\nexport { Avatar };\r\n"
79
+ }
80
+ ]
81
+ },
82
+ "badge": {
83
+ "name": "badge",
84
+ "dependencies": [
85
+ "tailwind-variants"
86
+ ],
87
+ "internalDependencies": [],
88
+ "files": [
89
+ {
90
+ "path": "src/components/ui/badge/Badge.tsx",
91
+ "content": "import * as React from 'react';\nimport { tv, type VariantProps } from 'tailwind-variants';\n\nconst badgeVariants = tv({\n base: 'inline-flex items-center justify-center rounded-full border px-2.5 py-0.5 font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 w-fit',\n variants: {\n variant: {\n default: 'border-transparent bg-primary text-primary-foreground shadow-sm',\n secondary: 'border-transparent bg-secondary text-secondary-foreground shadow-sm',\n outline: 'border-border text-foreground hover:bg-muted',\n success: 'border-transparent bg-success text-success-foreground shadow-sm',\n warning: 'border-transparent bg-warning text-warning-foreground shadow-sm',\n danger: 'border-transparent bg-danger text-danger-foreground shadow-sm',\n\n // Soft variants\n 'soft-primary': 'border-transparent bg-primary/10 text-primary',\n 'soft-success': 'border-transparent bg-success/10 text-success',\n 'soft-warning': 'border-transparent bg-warning/10 text-warning',\n 'soft-danger': 'border-transparent bg-danger/10 text-danger',\n\n // Glass variant\n glass: 'border border-white/20 bg-white/10 text-foreground backdrop-blur-md shadow-sm',\n\n // Gradient variant\n gradient: 'border-transparent bg-gradient-to-r from-primary to-indigo-500 text-white shadow-sm',\n },\n size: {\n sm: 'text-[10px] px-2 py-0.5 leading-4',\n md: 'text-xs px-2.5 py-0.5 leading-5',\n lg: 'text-sm px-3 py-1 leading-6',\n }\n },\n defaultVariants: {\n variant: 'default',\n size: 'md',\n }\n});\n\nexport interface BadgeProps\n extends React.HTMLAttributes<HTMLSpanElement>,\n VariantProps<typeof badgeVariants> {\n pulse?: boolean;\n}\n\nconst Badge = React.forwardRef<HTMLSpanElement, BadgeProps>(\n ({ className, variant, size, pulse, children, ...props }, ref) => {\n return (\n <span ref={ref} className={badgeVariants({ variant, size, className })} {...props}>\n {pulse && (\n <span className=\"relative grid place-items-center h-2 w-2 mr-1.5\">\n <span className=\"animate-ping absolute inline-flex h-full w-full rounded-full bg-current opacity-75\"></span>\n <span className=\"relative inline-flex rounded-full h-1.5 w-1.5 bg-current\"></span>\n </span>\n )}\n {children}\n </span>\n );\n }\n);\nBadge.displayName = 'Badge';\n\nexport { Badge, badgeVariants };\n"
92
+ }
93
+ ]
94
+ },
95
+ "button": {
96
+ "name": "button",
97
+ "dependencies": [
98
+ "@base-ui/react",
99
+ "tailwind-variants"
100
+ ],
101
+ "internalDependencies": [
102
+ "spinner"
103
+ ],
104
+ "files": [
105
+ {
106
+ "path": "src/components/ui/button/Button.test.tsx",
107
+ "content": "import { render, screen } from '@testing-library/react';\nimport { describe, it, expect } from 'vitest';\nimport { Button } from './Button';\n\ndescribe('Button', () => {\n it('renders correctly with default props', () => {\n render(<Button>Click me</Button>);\n const button = screen.getByRole('button', { name: /click me/i });\n expect(button).toBeInTheDocument();\n });\n\n it('renders loading spinner when isLoading is true', () => {\n render(<Button isLoading>Submit</Button>);\n const button = screen.getByRole('button', { name: /submit/i });\n \n // Nút phải bị disable\n expect(button).toBeDisabled();\n \n // Spinner (có role=\"status\") phải xuất hiện\n const spinner = screen.getByRole('status');\n expect(spinner).toBeInTheDocument();\n });\n\n it('renders correctly with different variants', () => {\n const { container } = render(<Button variant=\"danger\">Delete</Button>);\n const button = container.firstChild as HTMLElement;\n expect(button.className).toContain('bg-danger');\n });\n});\n"
108
+ },
109
+ {
110
+ "path": "src/components/ui/button/Button.tsx",
111
+ "content": "import * as React from 'react';\r\nimport { Button as BaseButton } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Spinner } from '../spinner/Spinner';\r\n\r\nconst buttonVariants = tv({\r\n base: 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-30 disabled:hover:bg-transparent data-open:bg-muted cursor-pointer disabled:cursor-not-allowed',\r\n variants: {\r\n variant: {\r\n solid: 'bg-primary text-primary-foreground hover:bg-primary/70 shadow-sm',\r\n outline: 'border border-border bg-transparent hover:bg-secondary/90 hover:text-foreground',\r\n ghost: 'hover:bg-secondary/90 hover:text-foreground',\r\n secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/70 shadow-sm',\r\n danger: 'bg-danger text-danger-foreground hover:bg-danger/70 shadow-sm',\r\n link: 'text-primary underline-offset-4 hover:underline h-auto px-0 py-0 font-normal',\r\n // Kính mờ tối — trên nền tối\r\n glass: 'bg-white/15 backdrop-blur-md border border-white/30 text-white hover:bg-white/25 hover:border-white/50 shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_4px_20px_rgba(0,0,0,0.2)] transition-all',\r\n // ─── Glossy Bubble Variants ───────────────────────────────────────────────\r\n // Gradient from white highlight (top-left) → tinted color (bottom-right)\r\n // + inset top border = hiệu ứng gương bong bóng xà phòng\r\n 'glass-white': 'bg-gradient-to-br from-white/70 to-slate-100/60 backdrop-blur-md border border-black/5 text-slate-700 hover:from-white/85 hover:to-slate-100/70 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-amber': 'bg-gradient-to-br from-white/70 to-amber-300/40 backdrop-blur-sm border border-amber-100/80 text-amber-700 hover:from-white/85 hover:to-amber-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-green': 'bg-gradient-to-br from-white/70 to-emerald-300/40 backdrop-blur-sm border border-emerald-100/80 text-emerald-700 hover:from-white/85 hover:to-emerald-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-purple': 'bg-gradient-to-br from-white/70 to-violet-300/40 backdrop-blur-sm border border-violet-100/80 text-violet-700 hover:from-white/85 hover:to-violet-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n 'glass-pink': 'bg-gradient-to-br from-white/70 to-pink-300/40 backdrop-blur-sm border border-pink-100/80 text-pink-700 hover:from-white/85 hover:to-pink-300/60 shadow-[0_4px_12px_rgba(0,0,0,0.1),inset_0_1px_0_rgba(255,255,255,0.8)] transition-all',\r\n },\r\n size: {\r\n sm: 'h-8 px-3 text-xs',\r\n md: 'h-10 px-4 py-2',\r\n lg: 'h-11 px-8',\r\n icon: 'h-10 w-10',\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'solid',\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface ButtonProps\r\n extends Omit<React.ComponentPropsWithoutRef<typeof BaseButton>, 'className'>,\r\n VariantProps<typeof buttonVariants> {\r\n leftIcon?: React.ReactNode;\r\n rightIcon?: React.ReactNode;\r\n isLoading?: boolean;\r\n className?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst Button = React.forwardRef<React.ElementRef<typeof BaseButton>, ButtonProps>(\r\n ({ className, variant, size, leftIcon, rightIcon, isLoading, children, ...props }, ref) => {\r\n return (\r\n <BaseButton\r\n ref={ref}\r\n className={buttonVariants({ variant, size, className: className || '' })}\r\n disabled={isLoading || props.disabled}\r\n {...props}\r\n >\r\n {isLoading && <Spinner size=\"xs\" className=\"mr-2\" />}\r\n {!isLoading && leftIcon && <span className=\"mr-2\">{leftIcon}</span>}\r\n {children}\r\n {!isLoading && rightIcon && <span className=\"ml-2\">{rightIcon}</span>}\r\n </BaseButton>\r\n );\r\n }\r\n);\r\nButton.displayName = 'Button';\r\n\r\nexport { Button, buttonVariants };\r\n"
112
+ }
113
+ ]
114
+ },
115
+ "calendar": {
116
+ "name": "calendar",
117
+ "dependencies": [
118
+ "react-day-picker",
119
+ "tailwind-variants"
120
+ ],
121
+ "internalDependencies": [],
122
+ "files": [
123
+ {
124
+ "path": "src/components/ui/calendar/Calendar.tsx",
125
+ "content": "import * as React from 'react';\r\nimport { DayPicker, type DateRange, type Matcher } from 'react-day-picker';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport * as locales from 'react-day-picker/locale';\r\n\r\nimport 'react-day-picker/dist/style.css';\r\n\r\nconst calendarVariants = tv({\r\n base: 'rdp-custom',\r\n variants: {\r\n size: {\r\n sm: '[&_.rdp-day]:h-7 [&_.rdp-day]:w-7 [&_.rdp-day]:text-xs',\r\n md: '[&_.rdp-day]:h-9 [&_.rdp-day]:w-9 [&_.rdp-day]:text-sm',\r\n lg: '[&_.rdp-day]:h-11 [&_.rdp-day]:w-11 [&_.rdp-day]:text-base',\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\nconst wrapperVariants = tv({\r\n base: 'inline-block rounded-xl border border-border bg-background p-3 shadow-sm',\r\n});\r\n\r\nexport type CalendarMode = 'single' | 'range' | 'multiple';\r\n\r\nexport interface CalendarProps extends VariantProps<typeof calendarVariants> {\r\n mode?: CalendarMode;\r\n selected?: Date | DateRange | Date[];\r\n onSelect?: (value: Date | DateRange | Date[] | undefined) => void;\r\n disablePastDates?: boolean;\r\n disableFutureDates?: boolean;\r\n disabled?: boolean;\r\n locale?: keyof typeof locales;\r\n className?: string;\r\n wrapperClassName?: string;\r\n numberOfMonths?: number;\r\n showOutsideDays?: boolean;\r\n}\r\n\r\nconst Calendar = React.forwardRef<HTMLDivElement, CalendarProps>(({\r\n mode = 'single',\r\n selected,\r\n onSelect,\r\n disablePastDates = false,\r\n disableFutureDates = false,\r\n disabled = false,\r\n locale = 'vi',\r\n className,\r\n wrapperClassName,\r\n size,\r\n numberOfMonths = 1,\r\n showOutsideDays = true,\r\n}, ref) => {\r\n const getDisabled = (): Matcher | Matcher[] | undefined => {\r\n if (disabled) return true;\r\n if (disablePastDates && disableFutureDates) return () => true;\r\n if (disablePastDates) return { before: new Date() };\r\n if (disableFutureDates) return { after: new Date() };\r\n return undefined;\r\n };\r\n\r\n return (\r\n <div ref={ref} className={wrapperVariants({ className: wrapperClassName })}>\r\n {mode === 'single' && (\r\n <DayPicker\r\n locale={locales[locale as keyof typeof locales]}\r\n mode=\"single\"\r\n selected={selected as Date | undefined}\r\n onSelect={(d) => onSelect?.(d)}\r\n disabled={getDisabled()}\r\n numberOfMonths={numberOfMonths}\r\n showOutsideDays={showOutsideDays}\r\n className={calendarVariants({ size, className })}\r\n />\r\n )}\r\n {mode === 'range' && (\r\n <DayPicker\r\n locale={locales[locale as keyof typeof locales]}\r\n mode=\"range\"\r\n selected={selected as DateRange | undefined}\r\n onSelect={(d) => onSelect?.(d)}\r\n disabled={getDisabled()}\r\n numberOfMonths={numberOfMonths}\r\n showOutsideDays={showOutsideDays}\r\n className={calendarVariants({ size, className })}\r\n />\r\n )}\r\n {mode === 'multiple' && (\r\n <DayPicker\r\n locale={locales[locale as keyof typeof locales]}\r\n mode=\"multiple\"\r\n selected={selected as Date[] | undefined}\r\n onSelect={(d) => onSelect?.(d)}\r\n disabled={getDisabled()}\r\n numberOfMonths={numberOfMonths}\r\n showOutsideDays={showOutsideDays}\r\n className={calendarVariants({ size, className })}\r\n />\r\n )}\r\n </div>\r\n );\r\n});\r\n\r\nCalendar.displayName = 'Calendar';\r\n\r\nexport { Calendar };\r\n"
126
+ }
127
+ ]
128
+ },
129
+ "card": {
130
+ "name": "card",
131
+ "dependencies": [],
132
+ "internalDependencies": [],
133
+ "files": [
134
+ {
135
+ "path": "src/components/ui/card/Card.tsx",
136
+ "content": "import * as React from 'react';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n className={cn('rounded-xl border border-border bg-card text-card-foreground shadow-sm', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nCard.displayName = 'Card';\r\n\r\nconst CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n className={cn('flex flex-col space-y-1.5 p-6 border-b border-border/50', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nCardHeader.displayName = 'CardHeader';\r\n\r\nconst CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLHeadingElement>>(\r\n ({ className, ...props }, ref) => (\r\n <h3\r\n ref={ref}\r\n className={cn('text-lg font-semibold leading-none tracking-tight text-primary', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nCardTitle.displayName = 'CardTitle';\r\n\r\nconst CardDescription = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(\r\n ({ className, ...props }, ref) => (\r\n <p\r\n ref={ref}\r\n className={cn('text-sm text-muted-foreground', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nCardDescription.displayName = 'CardDescription';\r\n\r\nconst CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div ref={ref} className={cn('p-6 pt-6', className)} {...props} />\r\n )\r\n);\r\nCardContent.displayName = 'CardContent';\r\n\r\nconst CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n className={cn('flex items-center p-6 pt-0', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nCardFooter.displayName = 'CardFooter';\r\n\r\nexport { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent };\r\n"
137
+ }
138
+ ]
139
+ },
140
+ "checkbox": {
141
+ "name": "checkbox",
142
+ "dependencies": [
143
+ "@base-ui/react",
144
+ "lucide-react",
145
+ "tailwind-variants"
146
+ ],
147
+ "internalDependencies": [],
148
+ "files": [
149
+ {
150
+ "path": "src/components/ui/checkbox/Checkbox.tsx",
151
+ "content": "import * as React from 'react';\r\nimport { Checkbox as BaseCheckbox } from '@base-ui/react';\r\nimport { Check, Minus } from 'lucide-react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst checkboxVariants = tv({\r\n slots: {\r\n root: 'group flex shrink-0 items-center justify-center rounded border transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 border-border bg-background dark:data-checked:bg-primary/90 dark:data-checked:border-primary/90 dark:data-indeterminate:bg-primary/90 dark:data-indeterminate:border-primary/90',\r\n indicator: 'dark:text-primary-foreground text-primary flex items-center justify-center',\r\n icon: 'h-full w-full stroke-[4]',\r\n },\r\n variants: {\r\n size: {\r\n sm: { root: 'h-4 w-4' },\r\n md: { root: 'h-5 w-5' },\r\n lg: { root: 'h-6 w-6' },\r\n }\r\n },\r\n defaultVariants: {\r\n size: 'md'\r\n }\r\n});\r\n\r\nconst { root, indicator, icon } = checkboxVariants();\r\n\r\nexport interface CheckboxProps\r\n extends Omit<BaseCheckbox.Root.Props, 'className'>,\r\n VariantProps<typeof checkboxVariants> {\r\n label?: string;\r\n className?: string;\r\n}\r\n\r\nconst Checkbox = React.forwardRef<React.ElementRef<typeof BaseCheckbox.Root>, CheckboxProps>(\r\n ({ className, size = 'md', label, id, indeterminate, ...props }, ref) => {\r\n const defaultId = React.useId();\r\n const checkboxId = id || defaultId;\r\n const { root, indicator, icon } = checkboxVariants({ size });\r\n\r\n return (\r\n <div className={cn(\"flex items-center gap-2\", props.disabled && \"opacity-50 cursor-not-allowed\")}>\r\n <BaseCheckbox.Root\r\n ref={ref}\r\n id={checkboxId}\r\n className={root({ className: cn(!props.disabled&&'cursor-pointer', className) })}\r\n indeterminate={indeterminate}\r\n {...props}\r\n >\r\n <BaseCheckbox.Indicator className={indicator()}>\r\n {indeterminate ? (\r\n <Minus className={icon()} />\r\n ) : (\r\n <Check className={icon()} />\r\n )}\r\n </BaseCheckbox.Indicator>\r\n </BaseCheckbox.Root>\r\n {label && (\r\n <label\r\n htmlFor={checkboxId}\r\n className={cn(\"text-sm font-medium leading-none select-none\", props.disabled && \"opacity-50 cursor-not-allowed\")}\r\n >\r\n {label}\r\n </label>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nCheckbox.displayName = 'Checkbox';\r\n\r\nexport { Checkbox };\r\n"
152
+ }
153
+ ]
154
+ },
155
+ "collapsible": {
156
+ "name": "collapsible",
157
+ "dependencies": [
158
+ "@base-ui/react",
159
+ "lucide-react",
160
+ "tailwind-variants"
161
+ ],
162
+ "internalDependencies": [],
163
+ "files": [
164
+ {
165
+ "path": "src/components/ui/collapsible/Collapsible.tsx",
166
+ "content": "import * as React from 'react';\r\nimport { Collapsible as BaseCollapsible } from '@base-ui/react';\r\nimport { ChevronDown } from 'lucide-react';\r\nimport { tv } from 'tailwind-variants';\r\n\r\nconst collapsibleVariants = tv({\r\n slots: {\r\n root: 'w-full',\r\n trigger: 'flex w-full items-center justify-between py-3 px-4 text-sm font-medium rounded-md border border-border bg-background hover:bg-muted/50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary [&[data-panel-open]>svg]:rotate-180',\r\n panel: 'overflow-hidden text-sm data-[open]:animate-accordion-down data-[closed]:animate-accordion-up transition-all',\r\n content: 'pt-2',\r\n }\r\n});\r\n\r\nexport interface CollapsibleProps {\r\n trigger: React.ReactNode;\r\n children: React.ReactNode;\r\n defaultOpen?: boolean;\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n className?: string;\r\n triggerClassName?: string;\r\n}\r\n\r\nconst Collapsible = React.forwardRef<HTMLDivElement, CollapsibleProps>(({\r\n trigger,\r\n children,\r\n defaultOpen,\r\n open,\r\n onOpenChange,\r\n className,\r\n triggerClassName,\r\n}, ref) => {\r\n const { root, trigger: triggerCls, panel, content } = collapsibleVariants();\r\n\r\n return (\r\n <BaseCollapsible.Root\r\n ref={ref}\r\n className={root({ className })}\r\n defaultOpen={defaultOpen}\r\n open={open}\r\n onOpenChange={onOpenChange}\r\n >\r\n <BaseCollapsible.Trigger className={triggerCls({ className: triggerClassName })}>\r\n {trigger}\r\n <ChevronDown className=\"h-4 w-4 shrink-0 transition-transform duration-200\" />\r\n </BaseCollapsible.Trigger>\r\n <BaseCollapsible.Panel className={panel()}>\r\n <div className={content()}>\r\n {children}\r\n </div>\r\n </BaseCollapsible.Panel>\r\n </BaseCollapsible.Root>\r\n );\r\n});\r\n\r\nCollapsible.displayName = 'Collapsible';\r\n\r\nexport { Collapsible };\r\n"
167
+ }
168
+ ]
169
+ },
170
+ "combobox": {
171
+ "name": "combobox",
172
+ "dependencies": [
173
+ "@base-ui/react",
174
+ "lucide-react",
175
+ "tailwind-variants"
176
+ ],
177
+ "internalDependencies": [],
178
+ "files": [
179
+ {
180
+ "path": "src/components/ui/combobox/ComboBox.tsx",
181
+ "content": "import * as React from 'react';\nimport { Combobox as BaseCombobox } from '@base-ui/react';\nimport { Check, ChevronDown, X, Loader2 } from 'lucide-react';\nimport { tv } from 'tailwind-variants';\nimport { cn } from '@lib/utils/cn';\n\nconst comboboxVariants = tv({\n slots: {\n root: 'flex flex-col gap-1.5 w-full',\n inputContainer: 'flex flex-wrap items-center gap-1.5 min-h-10 w-full rounded-md border border-border bg-background px-3 py-1.5 text-sm focus-within:border-primary disabled:cursor-not-allowed disabled:opacity-50 transition-shadow transition-colors',\n input: 'flex-1 min-w-[120px] bg-transparent outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed',\n popup: 'z-50 w-[var(--anchor-width,var(--reference-width))] max-w-[var(--available-width)] overflow-hidden rounded-md border border-border bg-background text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\n item: 'cursor-pointer relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50',\n indicator: 'absolute left-2 flex h-3.5 w-3.5 items-center justify-center',\n chip: 'inline-flex items-center gap-1 rounded bg-secondary px-2 py-0.5 text-xs font-medium text-secondary-foreground outline-none focus:ring-1 focus:ring-primary',\n chipRemove: 'hover:bg-primary/20 rounded-full p-0.5 transition-colors cursor-pointer',\n actionsHeader: 'flex items-center gap-1 p-1 border-b border-border sticky top-0 bg-background z-10',\n actionButton: 'flex-1 text-[10px] uppercase tracking-wider font-bold py-1.5 px-2 rounded-sm hover:bg-accent hover:text-accent-foreground text-muted-foreground transition-colors text-center',\n }\n});\n\nexport interface ComboBoxOption {\n label: string;\n value: string;\n}\n\nexport interface ComboBoxProps {\n options: ComboBoxOption[];\n label?: string;\n placeholder?: string;\n value?: string | string[];\n defaultValue?: string | string[];\n onValueChange?: (value: string | string[]) => void;\n multiple?: boolean;\n isLoading?: boolean;\n className?: string;\n autocomplete?: boolean;\n emptyText?: string;\n selectAllText?: string;\n clearAllText?: string;\n leftIcon?: React.ReactNode;\n}\n\nconst ComboBox = React.forwardRef<HTMLInputElement, ComboBoxProps>(\n ({ options, label, placeholder, value, defaultValue, onValueChange, multiple, isLoading, className, autocomplete = true, emptyText = 'Không tìm thấy kết quả.', selectAllText = 'Chọn tất cả', clearAllText = 'Xóa tất cả', leftIcon }, ref) => {\n const [inputValue, setInputValue] = React.useState('');\n const [internalValue, setInternalValue] = React.useState<string | string[] | null>(defaultValue || (multiple ? [] : null));\n\n const activeValue = value !== undefined ? value : internalValue;\n\n const handleValueChange = (newVal: string | string[] | null) => {\n if (value === undefined) {\n setInternalValue(newVal);\n }\n if (newVal !== null) {\n onValueChange?.(newVal);\n }\n };\n\n const handleClear = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n handleValueChange(multiple ? [] : null);\n setInputValue('');\n };\n\n const hasValue = multiple\n ? Array.isArray(activeValue) && activeValue.length > 0\n : !!activeValue;\n\n // Lọc options theo text người dùng đang gõ\n const filteredOptions = React.useMemo(() => {\n if (!inputValue || !autocomplete) return options;\n // Khi đã có value được chọn, input hiển thị label → không filter theo label đó\n if (!multiple && activeValue) {\n const selectedOption = options.find((o) => o.value === activeValue);\n if (selectedOption && inputValue === selectedOption.label) return options;\n }\n return options.filter(opt =>\n opt.label.toLowerCase().includes(inputValue.toLowerCase())\n );\n }, [options, inputValue, autocomplete, multiple, activeValue]);\n\n const { root, inputContainer, input, popup, item, indicator, chip, chipRemove, actionsHeader, actionButton } = comboboxVariants();\n const inputGroupRef = React.useRef<HTMLDivElement>(null);\n\n return (\n <BaseCombobox.Root\n value={activeValue}\n onValueChange={handleValueChange}\n multiple={multiple}\n onInputValueChange={setInputValue}\n autoHighlight\n itemToStringLabel={(val: string) => options.find((o) => o.value === val)?.label ?? val}\n >\n <div className={root({ className })}>\n {label && <label className=\"text-sm font-medium text-foreground\">{label}</label>}\n\n <div className=\"relative w-full group\">\n <BaseCombobox.InputGroup ref={inputGroupRef} className={cn(inputContainer(), leftIcon && 'pl-9')}>\n {leftIcon && (\n <div className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground group-focus-within:text-primary transition-colors pointer-events-none\">\n {leftIcon}\n </div>\n )}\n {multiple ? (\n <BaseCombobox.Chips className=\"flex flex-wrap items-center gap-1.5 flex-1 w-full min-w-0\">\n {Array.isArray(activeValue) && activeValue.map((val) => {\n const option = options.find(o => o.value === val);\n return (\n <BaseCombobox.Chip key={val} className={chip()}>\n {option?.label || val}\n <BaseCombobox.ChipRemove className={chipRemove()}>\n <X className=\"h-3 w-3\" />\n </BaseCombobox.ChipRemove>\n </BaseCombobox.Chip>\n );\n })}\n <BaseCombobox.Input\n ref={ref}\n readOnly={!autocomplete}\n placeholder={Array.isArray(activeValue) && activeValue.length > 0 ? '' : placeholder}\n className={input()}\n />\n </BaseCombobox.Chips>\n ) : (\n <BaseCombobox.Input\n ref={ref}\n readOnly={!autocomplete}\n placeholder={placeholder}\n className={cn(input(), !autocomplete && 'cursor-pointer')}\n />\n )}\n\n {hasValue && (\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"p-1 hover:bg-muted rounded-full text-muted-foreground transition-colors mr-1\"\n >\n <X className=\"h-3.5 w-3.5\" />\n </button>\n )}\n\n <BaseCombobox.Trigger className=\"text-muted-foreground transition-transform group-data-open:rotate-180 ml-auto\">\n {isLoading ? <Loader2 className=\"h-4 w-4 animate-spin\" /> : <ChevronDown className=\"h-4 w-4\" />}\n </BaseCombobox.Trigger>\n </BaseCombobox.InputGroup>\n\n <BaseCombobox.Portal>\n <BaseCombobox.Positioner\n anchor={inputGroupRef}\n sideOffset={4}\n style={{ width: 'var(--anchor-width)' }}\n >\n <BaseCombobox.Popup className={cn(popup(), 'min-w-0')}>\n {multiple && options.length > 0 && (\n <div className={actionsHeader()}>\n <button\n type=\"button\"\n aria-label={selectAllText}\n onClick={(e) => {\n e.preventDefault();\n handleValueChange(options.map((o) => o.value));\n }}\n className={actionButton()}\n >\n {selectAllText}\n </button>\n <div className=\"w-px h-3 bg-border\" />\n <button\n type=\"button\"\n aria-label={clearAllText}\n onClick={(e) => {\n e.preventDefault();\n handleValueChange([]);\n }}\n className={actionButton()}\n >\n {clearAllText}\n </button>\n </div>\n )}\n <BaseCombobox.List className=\"p-1 max-h-[300px] overflow-auto\">\n {filteredOptions.length === 0 ? (\n <div className=\"py-2 px-8 text-sm text-muted-foreground italic\">{emptyText}</div>\n ) : (\n filteredOptions.map((option) => (\n <BaseCombobox.Item\n key={option.value}\n value={option.value}\n className={item()}\n >\n <BaseCombobox.ItemIndicator className={indicator()}>\n <Check className=\"h-4 w-4\" />\n </BaseCombobox.ItemIndicator>\n {option.label}\n </BaseCombobox.Item>\n ))\n )}\n </BaseCombobox.List>\n </BaseCombobox.Popup>\n </BaseCombobox.Positioner>\n </BaseCombobox.Portal>\n </div>\n </div>\n </BaseCombobox.Root>\n );\n }\n);\n\nComboBox.displayName = 'ComboBox';\n\nexport { ComboBox };\n"
182
+ }
183
+ ]
184
+ },
185
+ "datepicker": {
186
+ "name": "datepicker",
187
+ "dependencies": [
188
+ "@base-ui/react",
189
+ "react-day-picker",
190
+ "date-fns",
191
+ "lucide-react",
192
+ "tailwind-variants"
193
+ ],
194
+ "internalDependencies": [
195
+ "button"
196
+ ],
197
+ "files": [
198
+ {
199
+ "path": "src/components/ui/datepicker/DatePicker.tsx",
200
+ "content": "import * as React from 'react';\nimport { Popover as BasePopover } from '@base-ui/react';\nimport { DayPicker, type DateRange } from 'react-day-picker';\nimport { format } from 'date-fns';\nimport { Calendar as CalendarIcon, ChevronDown, Clock } from 'lucide-react';\nimport { tv } from 'tailwind-variants';\nimport * as locales from 'react-day-picker/locale';\n\nimport 'react-day-picker/dist/style.css';\nimport { Button } from '../button/Button';\n\n// ---------- types ----------\n\nexport type TimeFormat = 'HH' | 'HH:mm' | 'HH:mm:ss';\nexport type DatePickerMode = 'single' | 'range' | 'time-only';\nexport type TimePickerStyle = 'input' | 'select';\n\ninterface TimeParts {\n h: string;\n m: string;\n s: string;\n}\n\nexport interface DatePickerProps {\n mode?: DatePickerMode;\n /** single → Date | range → DateRange | time-only → dùng timeValue */\n date?: Date | DateRange;\n onDateChange?: (date: Date | DateRange | undefined) => void;\n onChange?: (date: Date | DateRange | undefined) => void;\n /** Chỉ dùng khi mode='time-only' */\n timeValue?: string;\n onTimeChange?: (time: string) => void;\n label?: string;\n placeholder?: string;\n disablePastDates?: boolean;\n showTime?: boolean;\n timeFormat?: TimeFormat;\n timePickerStyle?: TimePickerStyle;\n disabled?: boolean;\n className?: string;\n description?: string;\n error?: string;\n}\n\n// ---------- helpers ----------\n\nconst DEFAULT_TIME: TimeParts = { h: '00', m: '00', s: '00' };\n\nfunction parseTimeParts(timeStr: string): TimeParts {\n const [h = '00', m = '00', s = '00'] = timeStr.split(':');\n return {\n h: h.padStart(2, '0'),\n m: m.padStart(2, '0'),\n s: s.padStart(2, '0'),\n };\n}\n\nfunction buildTimeString(parts: TimeParts, fmt: TimeFormat): string {\n if (fmt === 'HH') return parts.h;\n if (fmt === 'HH:mm') return `${parts.h}:${parts.m}`;\n return `${parts.h}:${parts.m}:${parts.s}`;\n}\n\nfunction applyTimeToDate(base: Date, parts: TimeParts): Date {\n const d = new Date(base);\n d.setHours(Number(parts.h), Number(parts.m), Number(parts.s), 0);\n return d;\n}\n\nfunction dateToTimeParts(d: Date): TimeParts {\n return {\n h: d.getHours().toString().padStart(2, '0'),\n m: d.getMinutes().toString().padStart(2, '0'),\n s: d.getSeconds().toString().padStart(2, '0'),\n };\n}\n\nfunction formatDateDisplay(d: Date, showTime: boolean, fmt: TimeFormat): string {\n const datePart = format(d, 'dd/MM/yyyy');\n if (!showTime) return datePart;\n if (fmt === 'HH') return `${datePart} ${format(d, 'HH')}h`;\n if (fmt === 'HH:mm') return `${datePart} ${format(d, 'HH:mm')}`;\n return `${datePart} ${format(d, 'HH:mm:ss')}`;\n}\n\nfunction padOptions(count: number) {\n return Array.from({ length: count }, (_, i) => ({\n label: i.toString().padStart(2, '0'),\n value: i.toString().padStart(2, '0'),\n }));\n}\n\nconst hoursOptions = padOptions(24);\nconst minutesOptions = padOptions(60);\nconst secondsOptions = padOptions(60);\n\n// ---------- styles ----------\n\nconst popoverContent = tv({\n base: 'z-50 rounded-xl border border-border bg-background text-foreground shadow-xl outline-none data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\n});\n\n// ---------- sub-components ----------\n\ninterface NativeSelectProps {\n value: string;\n options: { label: string; value: string }[];\n onChange: (val: string) => void;\n 'aria-label'?: string;\n}\n\nconst NativeScrollSelect: React.FC<NativeSelectProps> = ({ value, options, onChange, 'aria-label': ariaLabel }) => (\n <select\n aria-label={ariaLabel}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n className=\"h-9 w-full rounded-md border border-border bg-background px-2 text-sm text-foreground focus:border-primary focus:outline-none\"\n >\n {options.map((o) => (\n <option key={o.value} value={o.value}>{o.label}</option>\n ))}\n </select>\n);\n\ninterface TimePickerProps {\n parts: TimeParts;\n onChange: (parts: TimeParts) => void;\n timeFormat: TimeFormat;\n timePickerStyle: TimePickerStyle;\n}\n\nconst TimePicker: React.FC<TimePickerProps> = ({ parts, onChange, timeFormat, timePickerStyle }) => {\n const showMinutes = timeFormat === 'HH:mm' || timeFormat === 'HH:mm:ss';\n const showSeconds = timeFormat === 'HH:mm:ss';\n\n if (timePickerStyle === 'input') {\n const inputType = timeFormat === 'HH:mm:ss' ? 'time' : 'time';\n\n // Native time input — giả lập với step\n const step = showSeconds ? 1 : 60;\n const rawValue = showSeconds\n ? `${parts.h}:${parts.m}:${parts.s}`\n : `${parts.h}:${parts.m}`;\n\n return (\n <input\n type=\"time\"\n value={rawValue}\n step={step}\n onChange={(e) => {\n const [h = '00', m = '00', s = '00'] = e.target.value.split(':');\n onChange({ h: h.padStart(2, '0'), m: m.padStart(2, '0'), s: s.padStart(2, '0') });\n }}\n className=\"h-9 w-full rounded-md border border-border bg-background px-3 text-sm text-foreground focus:border-primary focus:outline-none\"\n />\n );\n }\n\n return (\n <div className=\"flex items-center gap-1.5\">\n <div className=\"flex-1\">\n <NativeScrollSelect\n aria-label=\"Giờ\"\n value={parts.h}\n options={hoursOptions}\n onChange={(val) => onChange({ ...parts, h: val })}\n />\n </div>\n {showMinutes && (\n <>\n <span className=\"text-sm font-bold text-muted-foreground\">:</span>\n <div className=\"flex-1\">\n <NativeScrollSelect\n aria-label=\"Phút\"\n value={parts.m}\n options={minutesOptions}\n onChange={(val) => onChange({ ...parts, m: val })}\n />\n </div>\n </>\n )}\n {showSeconds && (\n <>\n <span className=\"text-sm font-bold text-muted-foreground\">:</span>\n <div className=\"flex-1\">\n <NativeScrollSelect\n aria-label=\"Giây\"\n value={parts.s}\n options={secondsOptions}\n onChange={(val) => onChange({ ...parts, s: val })}\n />\n </div>\n </>\n )}\n </div>\n );\n};\n\n// ---------- main component ----------\n\nexport const DatePicker = React.forwardRef<HTMLDivElement, DatePickerProps>(({\n mode = 'single',\n date,\n onDateChange,\n onChange,\n timeValue,\n onTimeChange,\n label,\n placeholder = 'Chọn ngày...',\n disablePastDates = false,\n showTime = false,\n timeFormat = 'HH:mm:ss',\n timePickerStyle = 'select',\n disabled = false,\n className,\n description,\n error,\n}, ref) => {\n const [open, setOpen] = React.useState(false);\n const triggerRef = React.useRef<HTMLButtonElement>(null);\n\n // Khởi tạo parts từ date prop hoặc timeValue\n const initParts = React.useMemo<TimeParts>(() => {\n if (mode === 'time-only' && timeValue) return parseTimeParts(timeValue);\n if (date instanceof Date) return dateToTimeParts(date);\n return DEFAULT_TIME;\n }, []);\n\n const [timeParts, setTimeParts] = React.useState<TimeParts>(initParts);\n\n // Sync khi prop thay đổi từ ngoài\n React.useEffect(() => {\n if (mode === 'time-only' && timeValue) {\n setTimeParts(parseTimeParts(timeValue));\n } else if (date instanceof Date) {\n setTimeParts(dateToTimeParts(date));\n }\n }, [date, timeValue, mode]);\n\n // Gọi callback khi timeParts thay đổi\n const handlePartsChange = (newParts: TimeParts) => {\n setTimeParts(newParts);\n if (mode === 'time-only') {\n onTimeChange?.(buildTimeString(newParts, timeFormat));\n return;\n }\n if (date instanceof Date) {\n const newDate = applyTimeToDate(date, newParts);\n onDateChange?.(newDate);\n onChange?.(newDate);\n }\n };\n\n const handleDateSelect = (selectedDate: Date | DateRange | Date[] | undefined) => {\n if (!selectedDate) {\n onDateChange?.(undefined);\n onChange?.(undefined);\n return;\n }\n if (mode === 'single' && showTime && selectedDate instanceof Date) {\n const newDate = applyTimeToDate(selectedDate, timeParts);\n onDateChange?.(newDate);\n onChange?.(newDate);\n } else {\n // Because of our mode checking, we can be confident here\n onDateChange?.(selectedDate as DateRange);\n onChange?.(selectedDate as DateRange);\n }\n };\n\n // ---------- render trigger label ----------\n const triggerLabel = React.useMemo(() => {\n if (mode === 'time-only') {\n const val = timeValue ?? buildTimeString(timeParts, timeFormat);\n if (!val || val === '00' || val === '00:00' || val === '00:00:00')\n return <span className=\"text-muted-foreground\">{placeholder || 'Chọn giờ...'}</span>;\n return <span>{val}</span>;\n }\n\n if (!date) return <span className=\"text-muted-foreground\">{placeholder}</span>;\n\n if (mode === 'single' && date instanceof Date) {\n return <span>{formatDateDisplay(date, showTime, timeFormat)}</span>;\n }\n\n if (mode === 'range') {\n const range = date as DateRange;\n if (range.from && range.to) {\n return (\n <span>\n {format(range.from, 'dd/MM/yyyy')} – {format(range.to, 'dd/MM/yyyy')}\n </span>\n );\n }\n if (range.from) return <span>{format(range.from, 'dd/MM/yyyy')} –</span>;\n }\n\n return <span className=\"text-muted-foreground\">{placeholder}</span>;\n }, [date, mode, showTime, timeFormat, timeValue, timeParts, placeholder]);\n\n const isTimeMode = mode === 'time-only';\n const needsTimePicker = isTimeMode || (mode === 'single' && showTime);\n\n return (\n <div ref={ref} className={`flex flex-col gap-1.5 w-full ${className || ''}`}>\n {label && (\n <label className=\"text-sm font-medium text-foreground leading-none\">\n {label}\n </label>\n )}\n\n <BasePopover.Root open={open} onOpenChange={disabled ? undefined : setOpen}>\n <BasePopover.Trigger\n render={\n <button\n ref={triggerRef}\n type=\"button\"\n disabled={disabled}\n className={[\n 'flex h-10 w-full items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm',\n 'ring-offset-background transition-shadow',\n 'hover:border-primary focus:border-primary focus:outline-none',\n 'disabled:cursor-not-allowed disabled:opacity-50',\n error ? 'border-danger focus:border-danger' : 'border-border',\n 'group',\n ].join(' ')}\n >\n {isTimeMode ? (\n <Clock className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n ) : (\n <CalendarIcon className=\"h-4 w-4 shrink-0 text-muted-foreground\" />\n )}\n <div className=\"flex-1 truncate text-left\">{triggerLabel}</div>\n <ChevronDown className=\"h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200 group-data-open:rotate-180\" />\n </button>\n }\n />\n\n <BasePopover.Portal>\n <BasePopover.Positioner anchor={triggerRef} sideOffset={6} className=\"z-50\">\n <BasePopover.Popup className={popoverContent()}>\n {!isTimeMode && mode === 'single' && (\n <div className=\"p-2 flex justify-center\">\n <DayPicker\n mode=\"single\"\n locale={locales.vi}\n selected={date as Date | undefined}\n onSelect={(d) => handleDateSelect(d)}\n disabled={disablePastDates ? [{ before: new Date() }] : undefined}\n className=\"rdp-custom\"\n />\n </div>\n )}\n {!isTimeMode && mode === 'range' && (\n <div className=\"p-2 flex justify-center\">\n <DayPicker\n mode=\"range\"\n locale={locales.vi}\n selected={date as DateRange | undefined}\n onSelect={(d) => handleDateSelect(d)}\n disabled={disablePastDates ? [{ before: new Date() }] : undefined}\n className=\"rdp-custom\"\n />\n </div>\n )}\n\n {/* Time picker */}\n {needsTimePicker && (\n <div className={`border-t border-border p-3 flex flex-col gap-2 ${isTimeMode ? 'border-t-0' : ''}`}>\n <div className=\"flex items-center gap-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wide\">\n <Clock className=\"w-3.5 h-3.5\" />\n <span>\n {timeFormat === 'HH' ? 'Chọn giờ' : timeFormat === 'HH:mm' ? 'Giờ : Phút' : 'Giờ : Phút : Giây'}\n </span>\n </div>\n <TimePicker\n parts={timeParts}\n onChange={handlePartsChange}\n timeFormat={timeFormat}\n timePickerStyle={timePickerStyle}\n />\n </div>\n )}\n\n {/* Footer actions */}\n <div className=\"flex items-center justify-between gap-2 p-3 border-t border-border\">\n <button\n type=\"button\"\n onClick={() => {\n if (mode === 'time-only') {\n setTimeParts(DEFAULT_TIME);\n onTimeChange?.('');\n } else {\n onDateChange?.(undefined);\n }\n }}\n className=\"text-xs text-muted-foreground hover:text-foreground transition-colors underline-offset-2 hover:underline\"\n >\n Xóa\n </button>\n <Button size=\"sm\" onClick={() => setOpen(false)}>\n Xác nhận\n </Button>\n </div>\n </BasePopover.Popup>\n </BasePopover.Positioner>\n </BasePopover.Portal>\n </BasePopover.Root>\n {description && !error && (\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\n )}\n {error && (\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\n )}\n </div>\n );\n});\n\nDatePicker.displayName = \"DatePicker\";\n"
201
+ }
202
+ ]
203
+ },
204
+ "dialog": {
205
+ "name": "dialog",
206
+ "dependencies": [
207
+ "@base-ui/react",
208
+ "lucide-react",
209
+ "tailwind-variants"
210
+ ],
211
+ "internalDependencies": [],
212
+ "files": [
213
+ {
214
+ "path": "src/components/ui/dialog/Dialog.tsx",
215
+ "content": "import * as React from 'react';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst dialogVariants = tv({\r\n slots: {\r\n overlay: 'fixed inset-0! z-50 bg-black/30 backdrop-blur-sm data-starting:animate-in data-ending:animate-out data-ending:fade-out-0 data-starting:fade-in-0',\r\n content: 'fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background p-6 shadow-lg duration-200 data-starting:animate-in data-ending:animate-out data-ending:fade-out-0 data-starting:fade-in-0 data-ending:zoom-out-95 data-starting:zoom-in-95 ',\r\n header: 'flex flex-col space-y-1.5 text-center sm:text-left',\r\n footer: 'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 mt-auto',\r\n title: 'text-lg font-semibold leading-none tracking-tight',\r\n description: 'text-sm text-muted-foreground',\r\n close: 'absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:pointer-events-none data-starting:bg-accent data-starting:text-muted-foreground',\r\n },\r\n variants: {\r\n size: {\r\n default: {\r\n content: 'max-w-lg sm:rounded-lg',\r\n },\r\n fullScreen: {\r\n content: 'inset-0 left-0 top-0 translate-x-0 translate-y-0 max-w-none h-full rounded-none border-none',\r\n },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'default',\r\n },\r\n});\r\n\r\nconst { overlay, content, header, footer, title, description, close } = dialogVariants();\r\n\r\nexport interface DialogProps extends React.ComponentPropsWithoutRef<typeof BaseDialog.Root>, VariantProps<typeof dialogVariants> {\r\n trigger?: React.ReactNode;\r\n headerTitle?: string;\r\n headerDescription?: string;\r\n children?: React.ReactNode;\r\n footerContent?: React.ReactNode;\r\n contentClassName?: string;\r\n}\r\n\r\nconst Dialog = React.forwardRef<React.ElementRef<typeof BaseDialog.Root>, DialogProps>(\r\n ({ trigger, headerTitle, headerDescription, children, footerContent, size, contentClassName, ...props }, ref) => {\r\n const slots = dialogVariants({ size });\r\n\r\n return (\r\n <BaseDialog.Root {...props}>\r\n {trigger && <BaseDialog.Trigger render={trigger as React.ReactElement} />}\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup className={slots.content({ className: contentClassName })}>\r\n {(headerTitle || headerDescription) && (\r\n <div className={slots.header()}>\r\n {headerTitle && <BaseDialog.Title className={slots.title()}>{headerTitle}</BaseDialog.Title>}\r\n {headerDescription && <BaseDialog.Description className={slots.description()}>{headerDescription}</BaseDialog.Description>}\r\n </div>\r\n )}\r\n {children}\r\n {footerContent && <div className={slots.footer()}>{footerContent}</div>}\r\n <BaseDialog.Close className={slots.close()}>\r\n <X className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Close</span>\r\n </BaseDialog.Close>\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n </BaseDialog.Root>\r\n );\r\n }\r\n);\r\n\r\nDialog.displayName = 'Dialog';\r\n\r\nexport { Dialog };\r\n"
216
+ }
217
+ ]
218
+ },
219
+ "drawer": {
220
+ "name": "drawer",
221
+ "dependencies": [
222
+ "tailwind-variants",
223
+ "@base-ui/react",
224
+ "lucide-react"
225
+ ],
226
+ "internalDependencies": [],
227
+ "files": [
228
+ {
229
+ "path": "src/components/ui/drawer/Drawer.tsx",
230
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Dialog as BaseDialog } from '@base-ui/react';\r\nimport { X } from 'lucide-react';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst drawerVariants = tv({\r\n slots: {\r\n overlay: 'fixed inset-0 z-50 bg-black/40 backdrop-blur-sm data-starting:animate-in data-ending:animate-out data-ending:fade-out-0 data-starting:fade-in-0',\r\n panel: [\r\n 'fixed z-50 bg-background shadow-2xl flex flex-col',\r\n 'data-starting:animate-in data-ending:animate-out duration-300',\r\n 'outline-none overflow-hidden m-0 p-0 max-w-full max-h-full border-none',\r\n ],\r\n header: 'flex items-center justify-between px-6 py-4 border-b border-border/50 shrink-0',\r\n title: 'text-base font-semibold text-foreground',\r\n description: 'text-sm text-muted-foreground mt-0.5',\r\n body: 'flex-1 overflow-y-auto px-6 py-4',\r\n footer: 'px-6 py-4 border-t border-border/50 shrink-0',\r\n close: 'rounded-sm opacity-70 hover:opacity-100 transition-opacity ring-offset-background focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2',\r\n },\r\n variants: {\r\n direction: {\r\n left: {\r\n panel: 'inset-y-0 left-0 h-full data-[starting-style]:-translate-x-full data-[ending-style]:-translate-x-full transition-transform',\r\n },\r\n right: {\r\n panel: 'inset-y-0 right-0 h-full data-[starting-style]:translate-x-full data-[ending-style]:translate-x-full transition-transform',\r\n },\r\n top: {\r\n panel: 'inset-x-0 top-0 w-full data-[starting-style]:-translate-y-full data-[ending-style]:-translate-y-full transition-transform',\r\n },\r\n bottom: {\r\n panel: 'inset-x-0 bottom-0 w-full data-[starting-style]:translate-y-full data-[ending-style]:translate-y-full transition-transform',\r\n },\r\n },\r\n size: {\r\n sm: {},\r\n md: {},\r\n lg: {},\r\n full: {},\r\n },\r\n },\r\n compoundVariants: [\r\n { direction: 'left', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'left', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'left', size: 'lg', class: { panel: 'w-[480px]' } },\r\n { direction: 'left', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'right', size: 'sm', class: { panel: 'w-64' } },\r\n { direction: 'right', size: 'md', class: { panel: 'w-80' } },\r\n { direction: 'right', size: 'lg', class: { panel: 'w-[480px]' } },\r\n { direction: 'right', size: 'full', class: { panel: 'w-full' } },\r\n { direction: 'top', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'top', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'top', size: 'lg', class: { panel: 'h-[480px]' } },\r\n { direction: 'top', size: 'full', class: { panel: 'h-full' } },\r\n { direction: 'bottom', size: 'sm', class: { panel: 'h-48' } },\r\n { direction: 'bottom', size: 'md', class: { panel: 'h-64' } },\r\n { direction: 'bottom', size: 'lg', class: { panel: 'h-[480px]' } },\r\n { direction: 'bottom', size: 'full', class: { panel: 'h-full' } },\r\n ],\r\n defaultVariants: {\r\n direction: 'right',\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface DrawerProps extends VariantProps<typeof drawerVariants> {\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n trigger?: React.ReactNode;\r\n title?: string;\r\n description?: string;\r\n children?: React.ReactNode;\r\n footerContent?: React.ReactNode;\r\n hideClose?: boolean;\r\n className?: string;\r\n}\r\n\r\nconst Drawer = React.forwardRef<HTMLDivElement, DrawerProps>(({\r\n open: controlledOpen,\r\n onOpenChange,\r\n trigger,\r\n title,\r\n description,\r\n children,\r\n footerContent,\r\n direction = 'right',\r\n size = 'md',\r\n hideClose = false,\r\n className,\r\n}, ref) => {\r\n const [internalOpen, setInternalOpen] = React.useState(false);\r\n const isControlled = controlledOpen !== undefined;\r\n const isOpen = isControlled ? controlledOpen : internalOpen;\r\n\r\n const handleOpenChange = (val: boolean) => {\r\n if (!isControlled) setInternalOpen(val);\r\n onOpenChange?.(val);\r\n };\r\n\r\n const slots = drawerVariants({ direction, size });\r\n\r\n return (\r\n <BaseDialog.Root open={isOpen} onOpenChange={handleOpenChange}>\r\n {trigger && (\r\n <BaseDialog.Trigger\r\n render={<span style={{ display: 'contents' }}>{trigger}</span>}\r\n />\r\n )}\r\n\r\n <BaseDialog.Portal>\r\n <BaseDialog.Backdrop className={slots.overlay()} />\r\n <BaseDialog.Popup ref={ref} className={cn(slots.panel({ className }))}>\r\n {/* Header */}\r\n {(title || description || !hideClose) && (\r\n <div className={slots.header()}>\r\n <div>\r\n {title && <BaseDialog.Title className={slots.title()}>{title}</BaseDialog.Title>}\r\n {description && <BaseDialog.Description className={slots.description()}>{description}</BaseDialog.Description>}\r\n </div>\r\n {!hideClose && (\r\n <BaseDialog.Close\r\n className={slots.close()}\r\n aria-label=\"Đóng\"\r\n >\r\n <X className=\"h-4 w-4\" />\r\n </BaseDialog.Close>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Body */}\r\n <div className={slots.body()}>{children}</div>\r\n\r\n {/* Footer */}\r\n {footerContent && (\r\n <div className={slots.footer()}>{footerContent}</div>\r\n )}\r\n </BaseDialog.Popup>\r\n </BaseDialog.Portal>\r\n </BaseDialog.Root>\r\n );\r\n});\r\n\r\nDrawer.displayName = 'Drawer';\r\n\r\nexport { Drawer };\r\n"
231
+ }
232
+ ]
233
+ },
234
+ "form": {
235
+ "name": "form",
236
+ "dependencies": [
237
+ "react-hook-form"
238
+ ],
239
+ "internalDependencies": [],
240
+ "files": [
241
+ {
242
+ "path": "src/components/ui/form/Form.tsx",
243
+ "content": "import * as React from 'react';\nimport { cn } from '@lib/utils/cn';\nimport {\n Controller,\n FormProvider,\n useFormContext,\n} from 'react-hook-form';\nimport type {\n ControllerProps,\n FieldPath,\n FieldValues,\n} from 'react-hook-form';\n\nconst Form = FormProvider;\n\ntype FormFieldContextValue<\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n> = {\n name: TName;\n};\n\nconst FormFieldContext = React.createContext<FormFieldContextValue | null>(null);\n\nconst FormField = <\n TFieldValues extends FieldValues = FieldValues,\n TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>\n>({\n ...props\n}: ControllerProps<TFieldValues, TName>) => {\n return (\n <FormFieldContext.Provider value={{ name: props.name }}>\n <Controller {...props} />\n </FormFieldContext.Provider>\n );\n};\n\nconst useFormField = () => {\n const fieldContext = React.useContext(FormFieldContext);\n const itemContext = React.useContext(FormItemContext);\n const { getFieldState, formState } = useFormContext();\n\n if (!fieldContext) {\n throw new Error('useFormField must be used within <FormField>');\n }\n\n const fieldState = getFieldState(fieldContext.name, formState);\n\n if (!itemContext) {\n throw new Error('useFormField should be used within <FormItem>');\n }\n\n const { id } = itemContext;\n\n return {\n id,\n name: fieldContext.name,\n formItemId: `${id}-form-item`,\n formDescriptionId: `${id}-form-item-description`,\n formMessageId: `${id}-form-item-message`,\n ...fieldState,\n };\n};\n\ntype FormItemContextValue = {\n id: string;\n};\n\nconst FormItemContext = React.createContext<FormItemContextValue | null>(null);\n\nconst FormItem = React.forwardRef<\n HTMLDivElement,\n React.HTMLAttributes<HTMLDivElement>\n>(({ className, ...props }, ref) => {\n const id = React.useId();\n\n return (\n <FormItemContext.Provider value={{ id }}>\n <div ref={ref} className={cn('space-y-2', className)} {...props} />\n </FormItemContext.Provider>\n );\n});\nFormItem.displayName = 'FormItem';\n\nconst FormLabel = React.forwardRef<\n HTMLLabelElement,\n React.LabelHTMLAttributes<HTMLLabelElement>\n>(({ className, ...props }, ref) => {\n const { formItemId } = useFormField();\n\n return (\n <label\n ref={ref}\n htmlFor={formItemId}\n className={cn('text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70', className)}\n {...props}\n />\n );\n});\nFormLabel.displayName = 'FormLabel';\n\nconst FormControl = React.forwardRef<\n React.ElementRef<'div'>,\n React.HTMLAttributes<HTMLDivElement>\n>(({ ...props }, ref) => {\n const { error, formItemId, formDescriptionId, formMessageId } = useFormField();\n\n return (\n <div\n ref={ref}\n id={formItemId}\n aria-describedby={\n !error\n ? `${formDescriptionId}`\n : `${formDescriptionId} ${formMessageId}`\n }\n aria-invalid={!!error}\n {...props}\n />\n );\n});\nFormControl.displayName = 'FormControl';\n\nconst FormDescription = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, ...props }, ref) => {\n const { formDescriptionId } = useFormField();\n\n return (\n <p\n ref={ref}\n id={formDescriptionId}\n className={cn('text-[0.8rem] text-muted-foreground', className)}\n {...props}\n />\n );\n});\nFormDescription.displayName = 'FormDescription';\n\nconst FormMessage = React.forwardRef<\n HTMLParagraphElement,\n React.HTMLAttributes<HTMLParagraphElement>\n>(({ className, children, ...props }, ref) => {\n const { error, formMessageId } = useFormField();\n const body = error ? String(error?.message) : children;\n\n if (!body) {\n return null;\n }\n\n return (\n <p\n ref={ref}\n id={formMessageId}\n className={cn('text-[0.8rem] font-medium text-danger', className)}\n {...props}\n >\n {body}\n </p>\n );\n});\nFormMessage.displayName = 'FormMessage';\n\nexport {\n useFormField,\n Form,\n FormItem,\n FormLabel,\n FormControl,\n FormDescription,\n FormMessage,\n FormField,\n};\n"
244
+ }
245
+ ]
246
+ },
247
+ "input": {
248
+ "name": "input",
249
+ "dependencies": [
250
+ "@base-ui/react",
251
+ "tailwind-variants"
252
+ ],
253
+ "internalDependencies": [
254
+ "toggle"
255
+ ],
256
+ "files": [
257
+ {
258
+ "path": "src/components/ui/input/Input.tsx",
259
+ "content": "import * as React from 'react';\r\nimport { Input as BaseInput, Field as BaseField } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\nimport * as Icon from \"@components/ui/icons\";\r\nimport { Toggle } from '@/components/ui/toggle/Toggle';\r\n\r\nconst inputVariants = tv({\r\n base: 'flex h-10 w-full rounded-md border border-border bg-background px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:border-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow',\r\n variants: {\r\n variant: {\r\n default: '',\r\n filled: 'bg-accent border-transparent focus:border-primary',\r\n flushed: 'border-b-2 border-transparent border-b-border rounded-none px-0 focus:outline-none focus:ring-0 focus:border-transparent focus:border-b-primary bg-transparent',\r\n }\r\n },\r\n defaultVariants: {\r\n variant: 'default'\r\n }\r\n});\r\n\r\nexport interface InputProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseInput>, 'className'>, VariantProps<typeof inputVariants> {\r\n label?: string;\r\n error?: string;\r\n description?: string;\r\n icon?: React.ReactNode;\r\n endIcon?: React.ReactNode;\r\n placeholder?: string;\r\n className?: string;\r\n}\r\n\r\nconst Input = React.forwardRef<React.ElementRef<typeof BaseInput>, InputProps>(\r\n ({ className, variant, label, error, description, icon, endIcon, id, type, ...props }, ref) => {\r\n const defaultId = React.useId();\r\n const inputId = id || defaultId;\r\n const [showPassword, setShowPassword] = React.useState(false);\r\n\r\n const isPassword = type === 'password';\r\n const inputType = isPassword ? (showPassword ? 'text' : 'password') : type;\r\n\r\n return (\r\n <BaseField.Root className=\"flex flex-col gap-1.5 w-full\">\r\n {label && (\r\n <BaseField.Label htmlFor={inputId} className=\"text-sm font-medium text-foreground leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\r\n {label}\r\n </BaseField.Label>\r\n )}\r\n <div className=\"relative\">\r\n {icon && (\r\n <div className=\"absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground\">\r\n {icon}\r\n </div>\r\n )}\r\n <BaseField.Control render={<BaseInput\r\n ref={ref}\r\n id={inputId}\r\n type={inputType}\r\n className={cn(\r\n inputVariants({ variant }),\r\n icon && 'pl-9',\r\n (isPassword || endIcon) && 'pr-10',\r\n error && 'border-danger focus:border-danger',\r\n className\r\n )}\r\n {...props}\r\n />} />\r\n {isPassword ? (\r\n <Toggle\r\n type=\"button\"\r\n variant=\"ghost\"\r\n size=\"icon\"\r\n className=\"absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 text-muted-foreground hover:text-foreground\"\r\n pressed={showPassword}\r\n onPressedChange={setShowPassword}\r\n aria-label={showPassword ? 'Ẩn mật khẩu' : 'Hiện mật khẩu'}\r\n >\r\n {showPassword ? <Icon.EyeOff className=\"h-4 w-4\" /> : <Icon.Eye className=\"h-4 w-4\" />}\r\n </Toggle>\r\n ) : endIcon ? (\r\n <div className=\"absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground\">\r\n {endIcon}\r\n </div>\r\n ) : null}\r\n </div>\r\n {description && !error && (\r\n <BaseField.Description className=\"text-[0.8rem] text-muted-foreground\">\r\n {description}\r\n </BaseField.Description>\r\n )}\r\n {error && (\r\n <p className=\"text-[0.8rem] font-medium text-danger\">\r\n {error}\r\n </p>\r\n )}\r\n </BaseField.Root>\r\n );\r\n }\r\n);\r\nInput.displayName = 'Input';\r\n\r\nexport { Input };\r\n"
260
+ }
261
+ ]
262
+ },
263
+ "layout": {
264
+ "name": "layout",
265
+ "dependencies": [],
266
+ "internalDependencies": [],
267
+ "files": [
268
+ {
269
+ "path": "src/components/ui/layout/components/Route.tsx",
270
+ "content": ""
271
+ },
272
+ {
273
+ "path": "src/components/ui/layout/DashboardLayout.tsx",
274
+ "content": "import * as React from 'react';\r\nimport { Outlet, useLocation, useNavigate } from 'react-router-dom';\r\nimport { ComboBox } from '../combobox/ComboBox';\r\nimport { flattenSearchableRoutes } from '../../../../routes';\r\n\r\nimport * as Icon from \"@components/ui/icons\";\r\nimport { cn } from '@lib/utils/cn';\r\nimport { ThemeToggle } from '../ThemeToggle';\r\nimport { Tooltip } from '../tooltip/Tooltip';\r\nimport {\r\n SidebarProvider,\r\n Sidebar,\r\n SidebarRail,\r\n SidebarHeader,\r\n SidebarContent,\r\n SidebarFooter,\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarNavLink,\r\n SidebarMenuCollapsible,\r\n SidebarSeparator,\r\n SidebarTrigger,\r\n SidebarInset,\r\n UserMenuPopover,\r\n UserMenuItem,\r\n useSidebar,\r\n} from '../sidebar/Sidebar';\r\nimport { ROUTES } from '../../../../routes';\r\n\r\n// ─── SidebarNavItem ──────────────────────────────────────────────────────────\r\n\r\ninterface SidebarNavItemProps {\r\n route: any;\r\n parentPath?: string;\r\n isCollapsed: boolean;\r\n}\r\n\r\nconst SidebarNavItem: React.FC<SidebarNavItemProps> = ({ route, parentPath = '', isCollapsed }) => {\r\n const location = useLocation();\r\n \r\n // Tính toán path tuyệt đối\r\n const absolutePath = [parentPath, route.prefix, route.path]\r\n .filter(Boolean)\r\n .join('/')\r\n .replace(/\\/+/g, '/');\r\n\r\n const hasChildren = route.children && route.children.length > 0;\r\n\r\n // Kiểm tra xem có node con nào đang active không (để tự động mở group)\r\n const isAnyChildActive = React.useMemo(() => {\r\n if (!hasChildren) return false;\r\n const checkActive = (items: any[]): boolean => {\r\n return items.some(item => {\r\n const itemAbsPath = [absolutePath, item.prefix, item.path]\r\n .filter(Boolean)\r\n .join('/')\r\n .replace(/\\/+/g, '/');\r\n if (location.pathname === itemAbsPath) return true;\r\n if (item.children) return checkActive(item.children);\r\n return false;\r\n });\r\n };\r\n return checkActive(route.children);\r\n }, [hasChildren, route.children, absolutePath, location.pathname]);\r\n\r\n if (hasChildren) {\r\n return (\r\n <SidebarMenuItem>\r\n <SidebarMenuCollapsible\r\n id={route.label}\r\n icon={route.icon}\r\n label={route.label}\r\n isChildActive={isAnyChildActive}\r\n >\r\n {route.children.map((child: any, index: number) => (\r\n <SidebarNavItem \r\n key={index} \r\n route={child} \r\n parentPath={absolutePath} \r\n isCollapsed={isCollapsed} \r\n />\r\n ))}\r\n </SidebarMenuCollapsible>\r\n </SidebarMenuItem>\r\n );\r\n }\r\n\r\n // Nếu không có children nhưng có element -> Render Link\r\n if (route.element) {\r\n return (\r\n <SidebarMenuItem>\r\n <SidebarNavLink\r\n to={absolutePath === '' ? '/' : absolutePath}\r\n icon={route.icon}\r\n label={route.label}\r\n end={route.end}\r\n size=\"sm\"\r\n badge={\r\n route.badge && !isCollapsed ? (\r\n <span className=\"text-[10px] bg-primary text-primary-foreground rounded px-1.5 py-0.5 font-medium leading-none\">\r\n {route.badge}\r\n </span>\r\n ) : undefined\r\n }\r\n />\r\n </SidebarMenuItem>\r\n );\r\n }\r\n\r\n return null;\r\n};\r\n\r\n// ─── App Sidebar ──────────────────────────────────────────────────────────────\r\n\r\nconst AppSidebar: React.FC = () => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n const location = useLocation();\r\n\r\n return (\r\n <Sidebar collapsible=\"icon\">\r\n <SidebarRail />\r\n {/* Header */}\r\n <SidebarHeader className=\"border-b border-sidebar-border\">\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <div\r\n className={cn(\r\n 'flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200',\r\n isCollapsed && 'justify-center'\r\n )}\r\n >\r\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm\">\r\n <span className=\"text-primary-foreground font-bold text-sm select-none\">UI</span>\r\n </div>\r\n {!isCollapsed && (\r\n <div className=\"overflow-hidden min-w-0\">\r\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">UI Library</p>\r\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">Component Showcase</p>\r\n </div>\r\n )}\r\n </div>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarHeader>\r\n\r\n {/* Content */}\r\n <SidebarContent>\r\n {['overview', 'general', 'forms', 'complex', 'overlays'].map((cat) => {\r\n const catRoutes = ROUTES.filter(r => r.category === cat);\r\n if (catRoutes.length === 0) return null;\r\n\r\n return (\r\n <SidebarGroup key={cat}>\r\n <SidebarGroupLabel className=\"capitalize\">{cat}</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n {catRoutes.map((route, idx) => (\r\n <SidebarNavItem \r\n key={idx} \r\n route={route} \r\n isCollapsed={isCollapsed} \r\n />\r\n ))}\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n <SidebarSeparator className=\"mt-2 opacity-50\" />\r\n </SidebarGroup>\r\n );\r\n })}\r\n </SidebarContent>\r\n\r\n {/* Footer — User menu */}\r\n <SidebarFooter className=\"border-t border-sidebar-border pb-2\">\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <UserMenuPopover\r\n name=\"admin2\"\r\n email=\"admin@example.com\"\r\n avatar=\"https://i.pravatar.cc/100\"\r\n >\r\n <UserMenuItem icon={<Icon.Sparkles className=\"w-4 h-4\" />}>\r\n Upgrade to Pro\r\n </UserMenuItem>\r\n <div className=\"h-px bg-border/50 my-1\" />\r\n <UserMenuItem icon={<Icon.BadgeCheck className=\"w-4 h-4\" />}>\r\n Account\r\n </UserMenuItem>\r\n <UserMenuItem icon={<Icon.BillingIcon className=\"w-4 h-4\" />}>\r\n Billing\r\n </UserMenuItem>\r\n <UserMenuItem icon={<Icon.BellIcon className=\"w-4 h-4\" />}>\r\n Notifications\r\n </UserMenuItem>\r\n <div className=\"h-px bg-border/50 my-1\" />\r\n <UserMenuItem icon={<Icon.LogOut className=\"w-4 h-4\" />} destructive>\r\n Log out\r\n </UserMenuItem>\r\n </UserMenuPopover>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarFooter>\r\n </Sidebar>\r\n );\r\n};\r\n\r\n// ─── Header Search ───────────────────────────────────────────────────────────\r\n\r\nconst HeaderSearch: React.FC = () => {\r\n const navigate = useNavigate();\r\n const searchItems = React.useMemo(() => flattenSearchableRoutes(), []);\r\n const inputRef = React.useRef<HTMLInputElement>(null);\r\n\r\n // Ctrl + K to focus search\r\n React.useEffect(() => {\r\n const handleKeyDown = (e: KeyboardEvent) => {\r\n if ((e.ctrlKey || e.metaKey) && e.key === 'k') {\r\n e.preventDefault();\r\n inputRef.current?.focus();\r\n }\r\n };\r\n window.addEventListener('keydown', handleKeyDown);\r\n return () => window.removeEventListener('keydown', handleKeyDown);\r\n }, []);\r\n\r\n return (\r\n <div className=\"flex-1 max-w-sm mx-4 relative group hidden md:block\">\r\n <ComboBox\r\n ref={inputRef}\r\n options={searchItems}\r\n placeholder=\"Tìm kiếm component... (Ctrl + K)\"\r\n leftIcon={<Icon.Search className=\"w-4 h-4\" />}\r\n className=\"w-full h-9 min-h-[36px]\" // Tinh chỉnh chiều cao cho gọn\r\n onValueChange={(val) => {\r\n if (typeof val === 'string') {\r\n navigate(val);\r\n }\r\n }}\r\n />\r\n\r\n <div className=\"absolute right-10 top-1/2 -translate-y-1/2 flex items-center gap-1 px-1.5 py-0.5 rounded border border-border bg-muted/50 text-[10px] font-medium text-muted-foreground pointer-events-none group-focus-within:opacity-0 transition-opacity\">\r\n <span className=\"text-[8px]\">⌘</span>K\r\n </div>\r\n </div>\r\n );\r\n};\r\n\r\n// ─── Header ───────────────────────────────────────────────────────────────────\r\n\r\nconst Header: React.FC = () => {\r\n const location = useLocation();\r\n const segments = location.pathname.replace(/^\\//, '').split('/').filter(Boolean);\r\n\r\n return (\r\n <header className=\"h-[60px] bg-background/95 backdrop-blur-sm border-b border-border/50 flex items-center px-4 gap-3 sticky top-0 z-30\">\r\n <SidebarTrigger />\r\n <div className=\"h-4 w-px bg-border/60\" />\r\n <nav className=\"flex items-center gap-1 text-sm flex-1 min-w-0\">\r\n <span className=\"text-muted-foreground hover:text-foreground transition-colors cursor-default\">\r\n Home\r\n </span>\r\n {segments.map((seg, i) => (\r\n <React.Fragment key={i}>\r\n <Icon.ChevronRight className=\"w-3.5 h-3.5 text-muted-foreground/40 shrink-0\" />\r\n <span\r\n className={\r\n i === segments.length - 1\r\n ? 'text-foreground font-medium capitalize truncate'\r\n : 'text-muted-foreground capitalize'\r\n }\r\n >\r\n {seg.replace(/-/g, ' ')}\r\n </span>\r\n </React.Fragment>\r\n ))}\r\n </nav>\r\n\r\n <HeaderSearch />\r\n\r\n <div className=\"flex items-center gap-2 shrink-0\">\r\n <ThemeToggle />\r\n <img\r\n src=\"https://i.pravatar.cc/100\"\r\n alt=\"avatar\"\r\n className=\"w-8 h-8 rounded-full object-cover border border-border cursor-pointer\"\r\n />\r\n </div>\r\n </header>\r\n );\r\n};\r\n\r\n// ─── DashboardLayout ──────────────────────────────────────────────────────────\r\n\r\nexport const DashboardLayout = React.forwardRef<HTMLDivElement, { children?: React.ReactNode }>(({ children }, ref) => {\r\n return (\r\n <div ref={ref} className=\"h-full w-full\">\r\n <SidebarProvider>\r\n <AppSidebar />\r\n <SidebarInset>\r\n <Header />\r\n <main className=\"flex-1 overflow-y-auto bg-muted/10\">\r\n <div className=\"p-6 h-[calc(100vh-60px)] overflow-auto\">\r\n {children ? children : <Outlet />}\r\n </div>\r\n </main>\r\n </SidebarInset>\r\n </SidebarProvider>\r\n </div>\r\n );\r\n});\r\n\r\nDashboardLayout.displayName = \"DashboardLayout\";\r\n"
275
+ },
276
+ {
277
+ "path": "src/components/ui/layout/LayoutSample.tsx",
278
+ "content": "import * as React from 'react';\r\nimport { Outlet, useLocation } from 'react-router-dom';\r\n\r\nimport * as Icon from \"@components/ui/icons\";\r\nimport { cn } from '@lib/utils/cn';\r\nimport { ThemeToggle } from '../ThemeToggle';\r\nimport { Tooltip } from '../tooltip/Tooltip';\r\nimport {\r\n SidebarProvider,\r\n Sidebar,\r\n SidebarRail,\r\n SidebarHeader,\r\n SidebarContent,\r\n SidebarFooter,\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarNavLink,\r\n SidebarMenuCollapsible,\r\n SidebarSeparator,\r\n SidebarTrigger,\r\n SidebarInset,\r\n UserMenuPopover,\r\n UserMenuItem,\r\n useSidebar,\r\n} from '../sidebar/Sidebar';\r\nimport { ROUTES } from '../../../../routes';\r\n\r\n// ─── Nav Config ───────────────────────────────────────────────────────────────\r\n\r\nconst COLLAPSIBLE_GROUPS = [\r\n { id: 'general', label: 'General', icon: <Icon.BookOpen className=\"w-4 h-4\" />, defaultOpen: true },\r\n { id: 'forms', label: 'Forms', icon: <Icon.Users className=\"w-4 h-4\" />, defaultOpen: false },\r\n { id: 'complex', label: 'Complex', icon: <Icon.CreditCard className=\"w-4 h-4\" />, defaultOpen: false },\r\n { id: 'overlays', label: 'Overlays', icon: <Icon.ShieldCheck className=\"w-4 h-4\" />, defaultOpen: false },\r\n] as const;\r\n\r\n// ─── App Sidebar ──────────────────────────────────────────────────────────────\r\n\r\nconst AppSidebar: React.FC = () => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n const location = useLocation();\r\n\r\n // ─── Memoized Nav Config ───────────────────────────────────────────────────\r\n const navOverview = React.useMemo(() => \r\n ROUTES.filter((r) => r.category === 'overview').map((r) => ({\r\n to: r.path,\r\n end: r.end,\r\n icon: r.icon,\r\n label: r.label,\r\n })), \r\n []);\r\n\r\n const navCollapsibles = React.useMemo(() => \r\n COLLAPSIBLE_GROUPS.map((group) => ({\r\n id: group.id,\r\n icon: group.icon,\r\n label: group.label,\r\n defaultOpen: group.defaultOpen,\r\n items: ROUTES.filter((r) => r.category === group.id).map((r) => ({\r\n to: r.path,\r\n icon: r.icon,\r\n label: r.label,\r\n badge: r.badge,\r\n })),\r\n })),\r\n []);\r\n\r\n return (\r\n <Sidebar collapsible=\"icon\">\r\n <SidebarRail />\r\n {/* Header */}\r\n <SidebarHeader className=\"border-b border-sidebar-border\">\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <div\r\n className={cn(\r\n 'flex items-center gap-3 px-2 py-2 rounded-md transition-all duration-200',\r\n isCollapsed && 'justify-center'\r\n )}\r\n >\r\n <div className=\"w-8 h-8 bg-primary rounded-lg flex items-center justify-center shrink-0 shadow-sm\">\r\n <span className=\"text-primary-foreground font-bold text-sm select-none\">UI</span>\r\n </div>\r\n {!isCollapsed && (\r\n <div className=\"overflow-hidden min-w-0\">\r\n <p className=\"text-sm font-semibold text-foreground truncate leading-tight\">UI Library</p>\r\n <p className=\"text-xs text-muted-foreground truncate leading-tight\">Component Showcase</p>\r\n </div>\r\n )}\r\n </div>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarHeader>\r\n\r\n {/* Content */}\r\n <SidebarContent>\r\n {/* Overview */}\r\n <SidebarGroup>\r\n <SidebarGroupLabel>Overview</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n {navOverview.map((item) => (\r\n <SidebarMenuItem key={item.to}>\r\n <SidebarNavLink to={item.to} end={item.end} icon={item.icon} label={item.label} />\r\n </SidebarMenuItem>\r\n ))}\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </SidebarGroup>\r\n\r\n <SidebarSeparator />\r\n\r\n {/* Components Collapsible Groups */}\r\n <SidebarGroup>\r\n <SidebarGroupLabel>Components</SidebarGroupLabel>\r\n <SidebarGroupContent>\r\n <SidebarMenu>\r\n {navCollapsibles.map((group) => {\r\n // Detect xem có child nào đang active không\r\n const isChildActive = group.items.some((item) =>\r\n location.pathname === item.to || location.pathname.startsWith(item.to + '/')\r\n );\r\n\r\n return (\r\n <SidebarMenuItem key={group.id}>\r\n <SidebarMenuCollapsible\r\n id={group.id}\r\n icon={group.icon}\r\n label={group.label}\r\n defaultOpen={group.defaultOpen}\r\n isChildActive={isChildActive}\r\n >\r\n {group.items.map((item) => (\r\n <SidebarNavLink\r\n key={item.to}\r\n to={item.to}\r\n icon={item.icon}\r\n label={item.label}\r\n size=\"sm\"\r\n badge={\r\n 'badge' in item && item.badge && !isCollapsed ? (\r\n <span className=\"text-[10px] bg-primary text-primary-foreground rounded px-1.5 py-0.5 font-medium leading-none\">\r\n {item.badge}\r\n </span>\r\n ) : undefined\r\n }\r\n />\r\n ))}\r\n </SidebarMenuCollapsible>\r\n </SidebarMenuItem>\r\n );\r\n })}\r\n </SidebarMenu>\r\n </SidebarGroupContent>\r\n </SidebarGroup>\r\n </SidebarContent>\r\n\r\n {/* Footer — User menu */}\r\n <SidebarFooter className=\"border-t border-sidebar-border pb-2\">\r\n <SidebarMenu>\r\n <SidebarMenuItem>\r\n <UserMenuPopover\r\n name=\"admin2\"\r\n email=\"admin@example.com\"\r\n avatar=\"https://i.pravatar.cc/100\"\r\n >\r\n <UserMenuItem icon={<Icon.Sparkles className=\"w-4 h-4\" />}>\r\n Upgrade to Pro\r\n </UserMenuItem>\r\n <div className=\"h-px bg-border/50 my-1\" />\r\n <UserMenuItem icon={<Icon.BadgeCheck className=\"w-4 h-4\" />}>\r\n Account\r\n </UserMenuItem>\r\n <UserMenuItem icon={<Icon.BillingIcon className=\"w-4 h-4\" />}>\r\n Billing\r\n </UserMenuItem>\r\n <UserMenuItem icon={<Icon.BellIcon className=\"w-4 h-4\" />}>\r\n Notifications\r\n </UserMenuItem>\r\n <div className=\"h-px bg-border/50 my-1\" />\r\n <UserMenuItem icon={<Icon.LogOut className=\"w-4 h-4\" />} destructive>\r\n Log out\r\n </UserMenuItem>\r\n </UserMenuPopover>\r\n </SidebarMenuItem>\r\n </SidebarMenu>\r\n </SidebarFooter>\r\n </Sidebar>\r\n );\r\n};\r\n\r\n// ─── Header ───────────────────────────────────────────────────────────────────\r\n\r\nconst Header: React.FC = () => {\r\n const location = useLocation();\r\n const segments = location.pathname.replace(/^\\//, '').split('/').filter(Boolean);\r\n\r\n return (\r\n <header className=\"h-[60px] bg-background/95 backdrop-blur-sm border-b border-border/50 flex items-center px-4 gap-3 sticky top-0 z-30\">\r\n <SidebarTrigger />\r\n <div className=\"h-4 w-px bg-border/60\" />\r\n <nav className=\"flex items-center gap-1 text-sm flex-1 min-w-0\">\r\n <span className=\"text-muted-foreground hover:text-foreground transition-colors cursor-default\">\r\n Home\r\n </span>\r\n {segments.map((seg, i) => (\r\n <React.Fragment key={i}>\r\n <Icon.ChevronRight className=\"w-3.5 h-3.5 text-muted-foreground/40 shrink-0\" />\r\n <span\r\n className={\r\n i === segments.length - 1\r\n ? 'text-foreground font-medium capitalize truncate'\r\n : 'text-muted-foreground capitalize'\r\n }\r\n >\r\n {seg.replace(/-/g, ' ')}\r\n </span>\r\n </React.Fragment>\r\n ))}\r\n </nav>\r\n <div className=\"flex items-center gap-2 shrink-0\">\r\n <ThemeToggle />\r\n <img\r\n src=\"https://i.pravatar.cc/100\"\r\n alt=\"avatar\"\r\n className=\"w-8 h-8 rounded-full object-cover border border-border cursor-pointer\"\r\n />\r\n </div>\r\n </header>\r\n );\r\n};\r\n\r\n// ─── DashboardLayout ──────────────────────────────────────────────────────────\r\n\r\nexport const LayoutSample = React.forwardRef<HTMLDivElement, { children?: React.ReactNode }>(({ children }, ref) => {\r\n return (\r\n <div ref={ref} className=\"h-full w-full\">\r\n <SidebarProvider>\r\n <AppSidebar />\r\n <SidebarInset>\r\n <Header />\r\n <main className=\"flex-1 overflow-y-auto bg-muted/10\">\r\n <div className=\"p-6 h-[calc(100vh-60px)] overflow-auto\">\r\n {children ? children : <Outlet />}\r\n </div>\r\n </main>\r\n </SidebarInset>\r\n </SidebarProvider>\r\n </div>\r\n );\r\n});\r\n\r\nLayoutSample.displayName = \"LayoutSample\";\r\n"
279
+ }
280
+ ]
281
+ },
282
+ "popover": {
283
+ "name": "popover",
284
+ "dependencies": [
285
+ "@base-ui/react",
286
+ "tailwind-variants"
287
+ ],
288
+ "internalDependencies": [],
289
+ "files": [
290
+ {
291
+ "path": "src/components/ui/popover/Popover.tsx",
292
+ "content": "import * as React from 'react';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst popoverVariants = tv({\r\n slots: {\r\n popup: 'z-50 w-72 rounded-md border border-border bg-background p-4 text-popover-foreground shadow-md outline-none animate-in fade-in-0 zoom-in-95 data-ending:animate-out data-ending:fade-out-0 data-ending:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n arrow: 'fill-popover stroke-border stroke-[1px]',\r\n }\r\n});\r\n\r\nconst { popup, arrow } = popoverVariants();\r\n\r\nexport interface PopoverProps extends React.ComponentPropsWithoutRef<typeof BasePopover.Root> {\r\n trigger: React.ReactNode;\r\n children: React.ReactNode;\r\n className?: string;\r\n}\r\n\r\nconst Popover = React.forwardRef<HTMLDivElement, PopoverProps>(({ trigger, children, className, ...props }, ref) => {\r\n return (\r\n <BasePopover.Root {...props}>\r\n <BasePopover.Trigger nativeButton={false} render={<div ref={ref} className=\"inline-block\">{trigger}</div>} />\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner sideOffset={4}>\r\n <BasePopover.Popup className={popup({ className })}>\r\n <BasePopover.Arrow className={arrow()} />\r\n {children}\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n </BasePopover.Root>\r\n );\r\n}\r\n);\r\nPopover.displayName = \"Popover\";\r\n\r\nexport { Popover };\r\n"
293
+ }
294
+ ]
295
+ },
296
+ "pretty-code": {
297
+ "name": "pretty-code",
298
+ "dependencies": [
299
+ "shiki",
300
+ "unified",
301
+ "rehype-parse",
302
+ "rehype-pretty-code",
303
+ "rehype-react"
304
+ ],
305
+ "internalDependencies": [],
306
+ "files": [
307
+ {
308
+ "path": "src/components/ui/pretty-code/PrettyCode.tsx",
309
+ "content": "import React, { useState, useEffect } from 'react';\r\nimport { createHighlighter, type Highlighter } from 'shiki';\r\nimport { unified } from 'unified';\r\nimport rehypeParse from 'rehype-parse';\r\nimport rehypePrettyCode from 'rehype-pretty-code';\r\nimport rehypeReact from 'rehype-react';\r\nimport * as prod from 'react/jsx-runtime';\r\n\r\n// Singleton highlighter to avoid reloading\r\nlet globalHighlighter: Highlighter | null = null;\r\n\r\nconst getHighlighter = async () => {\r\n if (globalHighlighter) return globalHighlighter;\r\n globalHighlighter = await createHighlighter({\r\n themes: ['nord'],\r\n langs: ['tsx', 'typescript', 'javascript', 'bash', 'json'],\r\n });\r\n return globalHighlighter;\r\n};\r\n\r\ninterface PrettyCodeProps {\r\n code: string;\r\n lang?: string;\r\n className?: string;\r\n}\r\n\r\nexport const PrettyCode: React.FC<PrettyCodeProps> = ({ code, lang = 'tsx', className }) => {\r\n const [nodes, setNodes] = useState<React.ReactNode>(null);\r\n const [loading, setLoading] = useState(true);\r\n\r\n useEffect(() => {\r\n let isMounted = true;\r\n\r\n const highlight = async () => {\r\n try {\r\n const highlighter = await getHighlighter();\r\n \r\n // Use shiki directly to generate HTML\r\n const html = highlighter.codeToHtml(code, { \r\n lang, \r\n theme: 'nord' \r\n });\r\n\r\n const file = await unified()\r\n .use(rehypeParse, { fragment: true })\r\n .use(rehypeReact, { \r\n ...prod,\r\n })\r\n .process(html);\r\n\r\n if (isMounted) {\r\n setNodes(file.result as React.ReactNode);\r\n setLoading(false);\r\n }\r\n } catch (error) {\r\n console.error('Failed to highlight code:', error);\r\n if (isMounted) setLoading(false);\r\n }\r\n };\r\n\r\n highlight();\r\n return () => { isMounted = false; };\r\n }, [code, lang]);\r\n\r\n if (loading) {\r\n return (\r\n <pre className={`p-6 bg-zinc-950 text-zinc-100/50 text-xs animate-pulse ${className}`}>\r\n <code>{code}</code>\r\n </pre>\r\n );\r\n }\r\n\r\n return (\r\n <div className={`pretty-code-wrapper ${className}`}>\r\n {nodes}\r\n </div>\r\n );\r\n};\r\n"
310
+ }
311
+ ]
312
+ },
313
+ "preview-card": {
314
+ "name": "preview-card",
315
+ "dependencies": [
316
+ "@base-ui/react",
317
+ "tailwind-variants"
318
+ ],
319
+ "internalDependencies": [
320
+ "button"
321
+ ],
322
+ "files": [
323
+ {
324
+ "path": "src/components/ui/preview-card/PreviewCard.tsx",
325
+ "content": "import * as React from 'react';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Button } from '../button/Button';\r\n\r\nconst previewCardVariants = tv({\r\n slots: {\r\n popup: [\r\n 'z-50 w-72 rounded-xl border border-border bg-background shadow-xl outline-none',\r\n 'data-starting:animate-in data-ending:animate-out',\r\n 'data-ending:fade-out-0 data-starting:fade-in-0',\r\n 'data-ending:zoom-out-95 data-starting:zoom-in-95',\r\n 'data-side-bottom:slide-in-from-top-2',\r\n 'data-side-left:slide-in-from-right-2',\r\n 'data-side-right:slide-in-from-left-2',\r\n 'data-side-top:slide-in-from-bottom-2',\r\n ],\r\n cover: 'w-full overflow-hidden rounded-t-xl',\r\n body: 'p-4 space-y-2',\r\n title: 'font-semibold text-sm text-foreground leading-tight',\r\n description: 'text-xs text-muted-foreground leading-relaxed',\r\n footer: 'px-4 pb-4 pt-0 border-t border-border/50 mt-2 pt-3',\r\n },\r\n});\r\n\r\nexport type PreviewCardSide = 'top' | 'right' | 'bottom' | 'left';\r\nexport type PreviewCardAlign = 'start' | 'center' | 'end';\r\n\r\nexport interface PreviewCardProps {\r\n trigger: React.ReactNode;\r\n title?: string;\r\n description?: string;\r\n coverImage?: string;\r\n coverAlt?: string;\r\n coverHeight?: number;\r\n children?: React.ReactNode;\r\n footerContent?: React.ReactNode;\r\n side?: PreviewCardSide;\r\n align?: PreviewCardAlign;\r\n sideOffset?: number;\r\n openOnHover?: boolean;\r\n width?: number;\r\n className?: string;\r\n}\r\n\r\nconst PreviewCard = React.forwardRef<HTMLSpanElement, PreviewCardProps>(({\r\n trigger,\r\n title,\r\n description,\r\n coverImage,\r\n coverAlt = '',\r\n coverHeight = 120,\r\n children,\r\n footerContent,\r\n side = 'bottom',\r\n align = 'start',\r\n sideOffset = 8,\r\n openOnHover = false,\r\n width = 288,\r\n className,\r\n}, ref) => {\r\n const [open, setOpen] = React.useState(false);\r\n const slots = previewCardVariants();\r\n\r\n const triggerProps = openOnHover\r\n ? {\r\n onMouseEnter: () => setOpen(true),\r\n onMouseLeave: () => setOpen(false),\r\n }\r\n : {};\r\n\r\n return (\r\n <BasePopover.Root open={open} onOpenChange={setOpen}>\r\n <BasePopover.Trigger\r\n nativeButton={false}\r\n render={\r\n <span\r\n ref={ref}\r\n className=\"inline-block cursor-pointer\"\r\n {...triggerProps}\r\n >\r\n {trigger}\r\n </span>\r\n }\r\n />\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner side={side} align={align} sideOffset={sideOffset}>\r\n <BasePopover.Popup\r\n className={slots.popup({ className })}\r\n style={{ width }}\r\n >\r\n {coverImage && (\r\n <div className={slots.cover()} style={{ height: coverHeight }}>\r\n <img\r\n src={coverImage}\r\n alt={coverAlt}\r\n className=\"w-full h-full object-cover\"\r\n />\r\n </div>\r\n )}\r\n {(title || description || children) && (\r\n <div className={slots.body()}>\r\n {title && <p className={slots.title()}>{title}</p>}\r\n {description && <p className={slots.description()}>{description}</p>}\r\n {children}\r\n </div>\r\n )}\r\n {footerContent && (\r\n <div className={slots.footer()}>{footerContent}</div>\r\n )}\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n </BasePopover.Root>\r\n );\r\n});\r\n\r\nPreviewCard.displayName = 'PreviewCard';\r\n\r\nexport { PreviewCard };\r\n"
326
+ }
327
+ ]
328
+ },
329
+ "progress": {
330
+ "name": "progress",
331
+ "dependencies": [
332
+ "@base-ui/react",
333
+ "tailwind-variants"
334
+ ],
335
+ "internalDependencies": [],
336
+ "files": [
337
+ {
338
+ "path": "src/components/ui/progress/Progress.tsx",
339
+ "content": "import * as React from 'react';\nimport { Progress as BaseProgress } from '@base-ui/react';\nimport { tv, type VariantProps } from 'tailwind-variants';\n\nconst progressVariants = tv({\n slots: {\n base: 'flex flex-col gap-1.5 w-full',\n labelContainer: 'flex justify-between items-center text-sm font-medium',\n root: 'relative w-full overflow-hidden rounded-full bg-secondary',\n indicator: 'rounded-full h-full w-full flex-1 transition-all duration-500 ease-in-out relative overflow-hidden flex items-center justify-end',\n innerLabel: 'text-[10px] font-bold text-white drop-shadow-md pr-2',\n },\n variants: {\n size: {\n sm: { root: 'h-3', innerLabel: 'text-[10px] pr-1' },\n md: { root: 'h-4', innerLabel: 'text-[10px] pr-1' },\n lg: { root: 'h-6', innerLabel: 'text-xs pr-3' },\n },\n variant: {\n default: { indicator: 'bg-primary' },\n success: { indicator: 'bg-success' },\n warning: { indicator: 'bg-warning' },\n danger: { indicator: 'bg-danger' },\n gradient: { indicator: 'bg-gradient-to-r from-primary to-indigo-400' },\n },\n striped: {\n true: { \n indicator: 'bg-[linear-gradient(45deg,rgba(255,255,255,0.15)_25%,transparent_25%,transparent_50%,rgba(255,255,255,0.15)_50%,rgba(255,255,255,0.15)_75%,transparent_75%,transparent)] bg-[length:1rem_1rem]' \n }\n },\n animated: {\n true: {\n indicator: 'animate-progress-stripes'\n }\n }\n },\n defaultVariants: {\n size: 'md',\n variant: 'default',\n }\n});\n\nexport interface ProgressProps \n extends Omit<React.ComponentPropsWithoutRef<typeof BaseProgress.Root>, 'value'>, \n VariantProps<typeof progressVariants> {\n className?: string;\n value?: number | null;\n showLabel?: boolean;\n labelPosition?: 'inside' | 'outside' | 'none';\n label?: string;\n}\n\nconst Progress = React.forwardRef<React.ElementRef<typeof BaseProgress.Root>, ProgressProps>(\n ({ className, value, size, variant, striped, animated, showLabel = false, labelPosition = 'none', label, ...props }, ref) => {\n const { base, root, indicator, labelContainer, innerLabel } = progressVariants({ size, variant, striped, animated });\n \n // Auto-enable striped if string animated is true, unless explicitly turned off\n const isStriped = striped !== undefined ? striped : animated;\n const { indicator: finalIndicator } = progressVariants({ size, variant, striped: isStriped, animated });\n\n const displayValue = value ?? 0;\n\n return (\n <div className={base({ className })}>\n {(labelPosition === 'outside' || label) && (\n <div className={labelContainer()}>\n {label && <span>{label}</span>}\n {labelPosition === 'outside' && <span>{Math.round(displayValue)}%</span>}\n </div>\n )}\n <BaseProgress.Root\n ref={ref}\n className={root()}\n value={value ?? null}\n {...props}\n >\n <BaseProgress.Indicator \n className={finalIndicator()} \n style={{ transform: `translateX(-${100 - displayValue}%)` }} \n >\n {labelPosition === 'inside' && displayValue > 5 && (\n <span className={innerLabel()}>{Math.round(displayValue)}%</span>\n )}\n </BaseProgress.Indicator>\n </BaseProgress.Root>\n </div>\n )\n }\n)\nProgress.displayName = 'Progress';\n\nexport { Progress };\n"
340
+ }
341
+ ]
342
+ },
343
+ "radio": {
344
+ "name": "radio",
345
+ "dependencies": [
346
+ "@base-ui/react",
347
+ "tailwind-variants"
348
+ ],
349
+ "internalDependencies": [],
350
+ "files": [
351
+ {
352
+ "path": "src/components/ui/radio/Radio.tsx",
353
+ "content": "import * as React from 'react';\r\nimport { Radio as BaseRadio } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst radioVariants = tv({\r\n slots: {\r\n root: 'group flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border bg-background transition-all outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-checked:border-primary',\r\n indicator: 'flex items-center justify-center',\r\n dot: 'h-2 w-2 rounded-full bg-primary',\r\n },\r\n variants: {\r\n size: {\r\n sm: { root: 'h-4 w-4', dot: 'h-1.5 w-1.5' },\r\n md: { root: 'h-5 w-5', dot: 'h-2 w-2' },\r\n lg: { root: 'h-6 w-6', dot: 'h-2.5 w-2.5' },\r\n }\r\n },\r\n defaultVariants: {\r\n size: 'md'\r\n }\r\n});\r\n\r\nexport interface RadioProps\r\n extends Omit<BaseRadio.Root.Props, 'className'>,\r\n VariantProps<typeof radioVariants> {\r\n label?: string;\r\n className?: string;\r\n}\r\n\r\nconst Radio = React.forwardRef<React.ElementRef<typeof BaseRadio.Root>, RadioProps>(\r\n ({ className, size, label, id, ...props }, ref) => {\r\n const defaultId = React.useId();\r\n const radioId = id || defaultId;\r\n\r\n const { root, indicator, dot } = radioVariants({ size });\r\n\r\n return (\r\n <div className=\"flex items-center gap-2 w-fit\">\r\n <BaseRadio.Root\r\n ref={ref}\r\n id={radioId}\r\n className={root({ className })}\r\n {...props}\r\n >\r\n <BaseRadio.Indicator className={indicator()}>\r\n <div className={dot()} />\r\n </BaseRadio.Indicator>\r\n </BaseRadio.Root>\r\n {label && (\r\n <label\r\n htmlFor={radioId}\r\n className=\"text-sm font-medium leading-none cursor-pointer peer-disabled:cursor-not-allowed peer-disabled:opacity-70\"\r\n >\r\n {label}\r\n </label>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nRadio.displayName = 'Radio';\r\n\r\nexport { Radio };\r\n"
354
+ }
355
+ ]
356
+ },
357
+ "radio-group": {
358
+ "name": "radio-group",
359
+ "dependencies": [
360
+ "@base-ui/react"
361
+ ],
362
+ "internalDependencies": [],
363
+ "files": [
364
+ {
365
+ "path": "src/components/ui/radio-group/RadioGroup.tsx",
366
+ "content": "import * as React from 'react';\r\nimport { RadioGroup as BaseRadioGroup } from '@base-ui/react';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nexport interface RadioGroupProps extends React.ComponentPropsWithoutRef<typeof BaseRadioGroup> {\r\n className?: string;\r\n}\r\n\r\nconst RadioGroup = React.forwardRef<React.ElementRef<typeof BaseRadioGroup>, RadioGroupProps>(\r\n ({ className, ...props }, ref) => {\r\n return (\r\n <BaseRadioGroup\r\n ref={ref}\r\n className={cn('grid gap-2', className)}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\n\r\nRadioGroup.displayName = 'RadioGroup';\r\n\r\nexport { RadioGroup };\r\n"
367
+ }
368
+ ]
369
+ },
370
+ "rate": {
371
+ "name": "rate",
372
+ "dependencies": [
373
+ "tailwind-variants",
374
+ "lucide-react"
375
+ ],
376
+ "internalDependencies": [],
377
+ "files": [
378
+ {
379
+ "path": "src/components/ui/rate/Rate.tsx",
380
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { Star } from 'lucide-react';\r\n\r\nconst rateVariants = tv({\r\n slots: {\r\n root: 'inline-flex items-center gap-0.5',\r\n star: 'relative cursor-pointer transition-transform duration-100 hover:scale-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary rounded-sm',\r\n starIcon: 'transition-colors duration-150',\r\n },\r\n variants: {\r\n size: {\r\n sm: { star: 'w-4 h-4', starIcon: 'w-4 h-4' },\r\n md: { star: 'w-6 h-6', starIcon: 'w-6 h-6' },\r\n lg: { star: 'w-8 h-8', starIcon: 'w-8 h-8' },\r\n xl: { star: 'w-10 h-10', starIcon: 'w-10 h-10' },\r\n },\r\n readonly: {\r\n true: { star: 'cursor-default hover:scale-100' },\r\n },\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface RateProps extends VariantProps<typeof rateVariants> {\r\n value?: number;\r\n defaultValue?: number;\r\n onChange?: (value: number) => void;\r\n count?: number;\r\n allowHalf?: boolean;\r\n allowClear?: boolean;\r\n readonly?: boolean;\r\n disabled?: boolean;\r\n character?: React.ReactNode;\r\n activeColor?: string;\r\n inactiveColor?: string;\r\n className?: string;\r\n 'aria-label'?: string;\r\n}\r\n\r\nconst Rate = React.forwardRef<HTMLDivElement, RateProps>(({\r\n value: controlledValue,\r\n defaultValue = 0,\r\n onChange,\r\n count = 5,\r\n allowHalf = false,\r\n allowClear = true,\r\n readonly = false,\r\n disabled = false,\r\n character,\r\n activeColor = 'text-amber-400',\r\n inactiveColor = 'text-muted-foreground/30',\r\n size,\r\n className,\r\n 'aria-label': ariaLabel,\r\n}, ref) => {\r\n const isControlled = controlledValue !== undefined;\r\n const [internalValue, setInternalValue] = React.useState(defaultValue);\r\n const [hoverValue, setHoverValue] = React.useState<number | null>(null);\r\n\r\n const value = isControlled ? controlledValue! : internalValue;\r\n\r\n const handleChange = (newVal: number) => {\r\n if (readonly || disabled) return;\r\n const next = allowClear && newVal === value ? 0 : newVal;\r\n if (!isControlled) setInternalValue(next);\r\n onChange?.(next);\r\n };\r\n\r\n const getStarFraction = (starIndex: number, displayValue: number): number => {\r\n const full = starIndex + 1;\r\n const half = starIndex + 0.5;\r\n if (displayValue >= full) return 1;\r\n if (allowHalf && displayValue >= half) return 0.5;\r\n return 0;\r\n };\r\n\r\n const slots = rateVariants({ size, readonly: readonly || disabled });\r\n const display = hoverValue ?? value;\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n className={slots.root({ className })}\r\n role=\"radiogroup\"\r\n aria-label={ariaLabel || 'Đánh giá'}\r\n >\r\n {Array.from({ length: count }, (_, i) => {\r\n const fraction = getStarFraction(i, display);\r\n const full = i + 1;\r\n const half = i + 0.5;\r\n\r\n const renderStar = (frac: number) => {\r\n if (character) {\r\n if (frac === 0.5) {\r\n return (\r\n <span className=\"relative inline-flex items-center justify-center w-full h-full\">\r\n <span className={`absolute inset-0 overflow-hidden ${inactiveColor}`}>{character}</span>\r\n <span className=\"absolute inset-0 overflow-hidden w-1/2\" style={{ color: 'inherit' }}>\r\n <span className={activeColor}>{character}</span>\r\n </span>\r\n </span>\r\n );\r\n }\r\n return <span className={frac === 1 ? activeColor : inactiveColor}>{character}</span>;\r\n }\r\n\r\n if (frac === 0.5) {\r\n return (\r\n <span className=\"relative inline-flex items-center justify-center w-full h-full\">\r\n <Star className={`${slots.starIcon()} ${inactiveColor}`} fill=\"currentColor\" />\r\n <span\r\n className=\"absolute inset-0 overflow-hidden\"\r\n style={{ width: '50%' }}\r\n aria-hidden=\"true\"\r\n >\r\n <Star className={`${slots.starIcon()} ${activeColor}`} fill=\"currentColor\" />\r\n </span>\r\n </span>\r\n );\r\n }\r\n\r\n return (\r\n <Star\r\n className={`${slots.starIcon()} ${frac === 1 ? activeColor : inactiveColor}`}\r\n fill=\"currentColor\"\r\n />\r\n );\r\n };\r\n\r\n return (\r\n <button\r\n key={i}\r\n type=\"button\"\r\n role=\"radio\"\r\n aria-checked={full <= value}\r\n aria-label={`${full} sao`}\r\n disabled={disabled}\r\n className={slots.star()}\r\n onMouseMove={(e) => {\r\n if (readonly || disabled) return;\r\n if (allowHalf) {\r\n const rect = e.currentTarget.getBoundingClientRect();\r\n const isLeft = e.clientX - rect.left < rect.width / 2;\r\n setHoverValue(isLeft ? half : full);\r\n } else {\r\n setHoverValue(full);\r\n }\r\n }}\r\n onMouseLeave={() => setHoverValue(null)}\r\n onClick={(e) => {\r\n if (readonly || disabled) return;\r\n if (allowHalf) {\r\n const rect = e.currentTarget.getBoundingClientRect();\r\n const isLeft = e.clientX - rect.left < rect.width / 2;\r\n handleChange(isLeft ? half : full);\r\n } else {\r\n handleChange(full);\r\n }\r\n }}\r\n onKeyDown={(e) => {\r\n if (readonly || disabled) return;\r\n if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {\r\n e.preventDefault();\r\n handleChange(Math.min(count, value + (allowHalf ? 0.5 : 1)));\r\n } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {\r\n e.preventDefault();\r\n handleChange(Math.max(0, value - (allowHalf ? 0.5 : 1)));\r\n }\r\n }}\r\n >\r\n {renderStar(fraction)}\r\n </button>\r\n );\r\n })}\r\n </div>\r\n );\r\n});\r\n\r\nRate.displayName = 'Rate';\r\n\r\nexport { Rate };\r\n"
381
+ }
382
+ ]
383
+ },
384
+ "select": {
385
+ "name": "select",
386
+ "dependencies": [
387
+ "@base-ui/react",
388
+ "tailwind-variants",
389
+ "lucide-react"
390
+ ],
391
+ "internalDependencies": [],
392
+ "files": [
393
+ {
394
+ "path": "src/components/ui/select/Select.tsx",
395
+ "content": "import * as React from 'react';\nimport { Select as BaseSelect } from '@base-ui/react';\nimport { tv } from 'tailwind-variants';\nimport { cn } from '@lib/utils/cn';\nimport { ChevronDown, Check, X } from 'lucide-react';\n\nconst selectVariants = tv({\n slots: {\n trigger: 'flex h-10 w-full items-center justify-between rounded-md border border-border bg-background px-3 py-2 text-sm ring-offset-background focus:border-primary focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow group cursor-pointer',\n content: 'relative z-50 max-h-[300px] min-w-[var(--anchor-width)] overflow-hidden rounded-md border border-border bg-background text-foreground shadow-md data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\n viewport: 'p-1',\n item: 'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-muted focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-muted data-highlighted:text-foreground',\n icon: 'h-4 w-4 opacity-50 transition-transform duration-200 group-data-open:rotate-180',\n }\n});\n\nconst { trigger, content, viewport, item, icon } = selectVariants();\n\nexport interface SelectProps extends Omit<React.ComponentPropsWithoutRef<typeof BaseSelect.Root>, 'value' | 'defaultValue'> {\n label?: string;\n description?: string;\n error?: string;\n placeholder?: string;\n options: { label: string; value: string }[];\n id?: string;\n className?: string;\n value?: string;\n defaultValue?: string;\n clearable?: boolean;\n onChange?: (value: string) => void;\n emptyText?: string;\n clearLabel?: string;\n}\n\nconst Select = React.forwardRef<React.ElementRef<typeof BaseSelect.Trigger>, SelectProps>(\n ({ label, description, error, placeholder = 'Chọn...', options, id, className, clearable = true, onChange, onValueChange, value, defaultValue, emptyText = 'Không tìm thấy kết quả.', clearLabel = 'Xóa lựa chọn', ...props }, ref) => {\n const triggerRef = React.useRef<HTMLButtonElement>(null);\n\n const [selectedValue, setSelectedValue] = React.useState<string>(value ?? defaultValue ?? '');\n\n React.useEffect(() => {\n if (value !== undefined) setSelectedValue(value);\n }, [value]);\n\n const handleValueChange = (val: unknown) => {\n const strVal = val as string;\n setSelectedValue(strVal);\n onChange?.(strVal);\n // @ts-expect-error Base UI type mapping for onValueChange expects strict context which is internal\n onValueChange?.(strVal, {\n event: new Event('change')\n });\n };\n\n const handleClear = (e: React.MouseEvent) => {\n e.preventDefault();\n e.stopPropagation();\n setSelectedValue('');\n onChange?.('');\n // @ts-expect-error Base UI type mapping for onValueChange expects strict context which is internal\n onValueChange?.('', {\n event: new Event('change')\n });\n };\n\n const selectedLabel = options.find((o) => o.value === selectedValue)?.label;\n\n return (\n <div className=\"flex flex-col gap-1.5 w-full\">\n {label && (\n <label className=\"text-sm font-medium text-foreground leading-none\">\n {label}\n </label>\n )}\n\n {/*\n * Wrapper relative: nút X nằm NGOÀI BaseSelect.Trigger (absolute)\n * → click X không bao giờ bubble lên Trigger → popup không mở\n */}\n <div className=\"relative w-full\">\n <BaseSelect.Root\n value={value}\n defaultValue={defaultValue}\n onValueChange={handleValueChange}\n {...props}\n >\n <BaseSelect.Trigger\n ref={(node) => {\n triggerRef.current = node;\n if (typeof ref === 'function') ref(node);\n else if (ref) (ref as React.MutableRefObject<HTMLButtonElement | null>).current = node;\n }}\n className={trigger({ className: cn(className, error ? 'border-danger focus:border-danger' : '') })}\n id={id}\n >\n <span className={selectedLabel ? 'text-foreground' : 'text-muted-foreground'}>\n {selectedLabel ?? placeholder}\n </span>\n <BaseSelect.Icon>\n <ChevronDown className={icon()} />\n </BaseSelect.Icon>\n </BaseSelect.Trigger>\n <BaseSelect.Portal>\n <BaseSelect.Positioner anchor={triggerRef} className=\"z-50\" sideOffset={4}>\n <BaseSelect.Popup className={content()}>\n <div className={viewport()}>\n {options.length === 0 ? (\n <div className=\"py-2 px-8 text-sm text-muted-foreground italic text-center\">\n {emptyText}\n </div>\n ) : (\n options.map((option) => (\n <BaseSelect.Item key={option.value} value={option.value} className={item()}>\n <BaseSelect.ItemIndicator className=\"absolute left-2 flex h-3.5 w-3.5 items-center justify-center\">\n <Check className=\"h-4 w-4\" />\n </BaseSelect.ItemIndicator>\n <BaseSelect.ItemText>{option.label}</BaseSelect.ItemText>\n </BaseSelect.Item>\n ))\n )}\n </div>\n </BaseSelect.Popup>\n </BaseSelect.Positioner>\n </BaseSelect.Portal>\n </BaseSelect.Root>\n\n {/* Nút X đặt NGOÀI Trigger, absolute position — click không bubble lên Trigger */}\n {clearable && selectedValue && (\n <button\n type=\"button\"\n aria-label={clearLabel}\n onMouseDown={handleClear}\n className=\"cursor-pointer absolute right-8 top-1/2 -translate-y-1/2 flex h-5 w-5 items-center justify-center rounded-full text-muted-foreground hover:bg-red-50 hover:text-red-500 transition-colors z-10\"\n >\n <X className=\"h-3 w-3\" />\n </button>\n )}\n </div>\n\n {description && !error && (\n <p className=\"text-[0.8rem] text-muted-foreground\">{description}</p>\n )}\n {error && (\n <p className=\"text-[0.8rem] font-medium text-danger\">{error}</p>\n )}\n </div>\n );\n }\n);\n\nSelect.displayName = 'Select';\n\nexport { Select };\n"
396
+ }
397
+ ]
398
+ },
399
+ "sidebar": {
400
+ "name": "sidebar",
401
+ "dependencies": [
402
+ "react-router-dom",
403
+ "lucide-react",
404
+ "tailwind-variants",
405
+ "@base-ui/react"
406
+ ],
407
+ "internalDependencies": [
408
+ "tooltip"
409
+ ],
410
+ "files": [
411
+ {
412
+ "path": "src/components/ui/sidebar/Sidebar.tsx",
413
+ "content": "import * as React from 'react';\r\nimport { NavLink } from 'react-router-dom';\r\nimport { PanelLeft, ChevronRight, ChevronsUpDown } from 'lucide-react';\r\nimport { tv } from 'tailwind-variants';\r\nimport { Popover as BasePopover } from '@base-ui/react';\r\nimport { Tooltip } from '../tooltip/Tooltip';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\n// ─── Constants ────────────────────────────────────────────────────────────────\r\n\r\nconst SIDEBAR_WIDTH_DEFAULT = 256; // px\r\nconst SIDEBAR_WIDTH_MIN = 160; // px\r\nconst SIDEBAR_WIDTH_MAX = 480; // px\r\nconst SIDEBAR_WIDTH_ICON = '4rem';\r\nconst MOBILE_BREAKPOINT = 768;\r\n\r\n// ─── Context ──────────────────────────────────────────────────────────────────\r\n\r\ntype SidebarState = 'expanded' | 'collapsed';\r\n\r\ninterface SidebarContextValue {\r\n state: SidebarState;\r\n open: boolean;\r\n setOpen: (open: boolean) => void;\r\n toggleSidebar: () => void;\r\n isMobile: boolean;\r\n openMobile: boolean;\r\n setOpenMobile: (open: boolean) => void;\r\n sidebarWidth: number;\r\n setSidebarWidth: (w: number) => void;\r\n}\r\n\r\nconst SidebarContext = React.createContext<SidebarContextValue | null>(null);\r\n\r\nexport function useSidebar() {\r\n const ctx = React.useContext(SidebarContext);\r\n if (!ctx) throw new Error('useSidebar must be used within SidebarProvider');\r\n return ctx;\r\n}\r\n\r\n// ─── Provider ─────────────────────────────────────────────────────────────────\r\n\r\nexport interface SidebarProviderProps {\r\n children: React.ReactNode;\r\n defaultOpen?: boolean;\r\n open?: boolean;\r\n onOpenChange?: (open: boolean) => void;\r\n className?: string;\r\n style?: React.CSSProperties;\r\n}\r\n\r\nconst SidebarProvider = React.forwardRef<HTMLDivElement, SidebarProviderProps>(\r\n ({ children, defaultOpen = true, open: controlledOpen, onOpenChange, className, style }, ref) => {\r\n const [isMobile, setIsMobile] = React.useState(false);\r\n const [openMobile, setOpenMobile] = React.useState(false);\r\n const [internalOpen, setInternalOpen] = React.useState(defaultOpen);\r\n const [sidebarWidth, setSidebarWidth] = React.useState(SIDEBAR_WIDTH_DEFAULT);\r\n\r\n const isControlled = controlledOpen !== undefined;\r\n const open = isControlled ? controlledOpen! : internalOpen;\r\n\r\n React.useEffect(() => {\r\n const check = () => setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);\r\n check();\r\n window.addEventListener('resize', check);\r\n return () => window.removeEventListener('resize', check);\r\n }, []);\r\n\r\n const setOpen = React.useCallback(\r\n (val: boolean) => {\r\n if (!isControlled) setInternalOpen(val);\r\n onOpenChange?.(val);\r\n },\r\n [isControlled, onOpenChange]\r\n );\r\n\r\n const toggleSidebar = React.useCallback(() => {\r\n if (isMobile) setOpenMobile((v) => !v);\r\n else setOpen(!open);\r\n }, [isMobile, open, setOpen]);\r\n\r\n React.useEffect(() => {\r\n const onKey = (e: KeyboardEvent) => {\r\n if ((e.metaKey || e.ctrlKey) && e.key === 'b') {\r\n e.preventDefault();\r\n toggleSidebar();\r\n }\r\n };\r\n window.addEventListener('keydown', onKey);\r\n return () => window.removeEventListener('keydown', onKey);\r\n }, [toggleSidebar]);\r\n\r\n const state: SidebarState = open ? 'expanded' : 'collapsed';\r\n\r\n return (\r\n <SidebarContext.Provider\r\n value={{ state, open, setOpen, toggleSidebar, isMobile, openMobile, setOpenMobile, sidebarWidth, setSidebarWidth }}\r\n >\r\n <div\r\n ref={ref}\r\n data-sidebar-state={state}\r\n style={\r\n {\r\n '--sidebar-width': `${sidebarWidth}px`,\r\n '--sidebar-width-icon': SIDEBAR_WIDTH_ICON,\r\n ...style,\r\n } as React.CSSProperties\r\n }\r\n className={cn('group/sidebar-wrapper flex min-h-screen w-full has-data-[variant=inset]:bg-muted/30', className)}\r\n >\r\n {children}\r\n </div>\r\n </SidebarContext.Provider>\r\n );\r\n }\r\n);\r\nSidebarProvider.displayName = 'SidebarProvider';\r\n\r\n// ─── SidebarTrigger ───────────────────────────────────────────────────────────\r\n\r\nconst SidebarTrigger = React.forwardRef<\r\n HTMLButtonElement,\r\n React.ButtonHTMLAttributes<HTMLButtonElement>\r\n>(({ className, onClick, ...props }, ref) => {\r\n const { toggleSidebar } = useSidebar();\r\n return (\r\n <button\r\n ref={ref}\r\n type=\"button\"\r\n data-sidebar=\"trigger\"\r\n onClick={(e) => {\r\n toggleSidebar();\r\n onClick?.(e);\r\n }}\r\n className={cn('inline-flex items-center justify-center h-8 w-8 rounded-md text-muted-foreground hover:bg-muted hover:text-foreground transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary', className)}\r\n title=\"Toggle Sidebar (⌘B)\"\r\n {...props}\r\n >\r\n <PanelLeft className=\"h-4 w-4\" />\r\n <span className=\"sr-only\">Toggle Sidebar</span>\r\n </button>\r\n );\r\n});\r\nSidebarTrigger.displayName = 'SidebarTrigger';\r\n\r\n// ─── Sidebar ──────────────────────────────────────────────────────────────────\r\n\r\nexport interface SidebarProps extends React.HTMLAttributes<HTMLElement> {\r\n side?: 'left' | 'right';\r\n variant?: 'sidebar' | 'floating' | 'inset';\r\n collapsible?: 'offcanvas' | 'icon' | 'none';\r\n}\r\n\r\nconst Sidebar = React.forwardRef<HTMLElement, SidebarProps>(\r\n ({ side = 'left', variant = 'sidebar', collapsible = 'icon', className, children, ...props }, ref) => {\r\n const { state, isMobile, openMobile, setOpenMobile, sidebarWidth } = useSidebar();\r\n\r\n if (collapsible === 'none') {\r\n return (\r\n <aside\r\n ref={ref}\r\n className={cn('flex h-screen flex-col bg-sidebar border-r border-sidebar-border', className)}\r\n style={{ width: SIDEBAR_WIDTH_DEFAULT }}\r\n {...props}\r\n >\r\n {children}\r\n </aside>\r\n );\r\n }\r\n\r\n if (isMobile) {\r\n return (\r\n <>\r\n {openMobile && (\r\n <div\r\n className=\"fixed inset-0 z-40 bg-black/50 backdrop-blur-sm\"\r\n onClick={() => setOpenMobile(false)}\r\n />\r\n )}\r\n <aside\r\n ref={ref}\r\n className={cn(\r\n 'fixed inset-y-0 z-50 flex flex-col bg-sidebar border-r border-sidebar-border shadow-xl',\r\n 'transition-transform duration-300 ease-in-out',\r\n side === 'left' ? 'left-0' : 'right-0',\r\n openMobile\r\n ? 'translate-x-0'\r\n : side === 'left'\r\n ? '-translate-x-full'\r\n : 'translate-x-full',\r\n className\r\n )}\r\n style={{ width: sidebarWidth }}\r\n {...props}\r\n >\r\n {children}\r\n </aside>\r\n </>\r\n );\r\n }\r\n\r\n return (\r\n <aside\r\n ref={ref}\r\n data-state={state}\r\n data-collapsible={state === 'collapsed' ? collapsible : ''}\r\n data-variant={variant}\r\n data-side={side}\r\n className={cn(\r\n 'group relative flex h-screen flex-col bg-sidebar text-sidebar-foreground',\r\n 'border-r border-dashed border-sidebar-border',\r\n state === 'collapsed' && 'will-change-[width] transition-[width] duration-300 ease-in-out',\r\n 'overflow-hidden shrink-0',\r\n state === 'collapsed' && collapsible === 'icon' ? 'w-(--sidebar-width-icon)' : 'w-(--sidebar-width)',\r\n className\r\n )}\r\n {...props}\r\n >\r\n {children}\r\n </aside>\r\n );\r\n }\r\n);\r\nSidebar.displayName = 'Sidebar';\r\n\r\n// ─── SidebarRail — drag handle để resize ──────────────────────────────────────\r\n\r\nconst SidebarRail = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const { state, setSidebarWidth, sidebarWidth } = useSidebar();\r\n const isDragging = React.useRef(false);\r\n const startX = React.useRef(0);\r\n const startWidth = React.useRef(0);\r\n\r\n const onMouseDown = React.useCallback(\r\n (e: React.MouseEvent) => {\r\n if (state === 'collapsed') return;\r\n isDragging.current = true;\r\n startX.current = e.clientX;\r\n startWidth.current = sidebarWidth;\r\n document.body.style.cursor = 'col-resize';\r\n document.body.style.userSelect = 'none';\r\n },\r\n [state, sidebarWidth]\r\n );\r\n\r\n React.useEffect(() => {\r\n const onMouseMove = (e: MouseEvent) => {\r\n if (!isDragging.current) return;\r\n const delta = e.clientX - startX.current;\r\n const next = Math.min(SIDEBAR_WIDTH_MAX, Math.max(SIDEBAR_WIDTH_MIN, startWidth.current + delta));\r\n setSidebarWidth(next);\r\n };\r\n\r\n const onMouseUp = () => {\r\n if (!isDragging.current) return;\r\n isDragging.current = false;\r\n document.body.style.cursor = '';\r\n document.body.style.userSelect = '';\r\n };\r\n\r\n window.addEventListener('mousemove', onMouseMove);\r\n window.addEventListener('mouseup', onMouseUp);\r\n return () => {\r\n window.removeEventListener('mousemove', onMouseMove);\r\n window.removeEventListener('mouseup', onMouseUp);\r\n };\r\n }, [setSidebarWidth]);\r\n\r\n if (state === 'collapsed') return null;\r\n\r\n return (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"rail\"\r\n aria-label=\"Resize sidebar\"\r\n onMouseDown={onMouseDown}\r\n className={cn(\r\n 'absolute inset-y-0 right-0 z-20 w-1 cursor-col-resize',\r\n 'group/rail flex items-center justify-center',\r\n 'after:absolute after:inset-y-0 after:right-0 after:w-1',\r\n 'hover:after:bg-primary/50 transition-colors duration-150',\r\n className\r\n )}\r\n {...props}\r\n >\r\n </div>\r\n );\r\n }\r\n);\r\nSidebarRail.displayName = 'SidebarRail';\r\n\r\n// ─── SidebarInset ─────────────────────────────────────────────────────────────\r\n\r\nconst SidebarInset = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n className={cn('relative flex flex-1 flex-col overflow-hidden min-w-0 bg-background', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarInset.displayName = 'SidebarInset';\r\n\r\n// ─── Layout: Header / Content / Footer ───────────────────────────────────────\r\n\r\nconst SidebarHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"header\"\r\n className={cn('flex flex-col gap-2 p-2 shrink-0', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarHeader.displayName = 'SidebarHeader';\r\n\r\nconst SidebarFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const { isMobile } = useSidebar();\r\n if (isMobile) {\r\n return null;\r\n }\r\n return (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"footer\"\r\n className={cn('flex flex-col gap-2 p-2 mt-auto shrink-0', className)}\r\n {...props}\r\n />\r\n )\r\n}\r\n);\r\nSidebarFooter.displayName = 'SidebarFooter';\r\n\r\nconst SidebarContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"content\"\r\n className={cn('flex flex-1 flex-col gap-2 overflow-y-auto overflow-x-hidden py-2', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarContent.displayName = 'SidebarContent';\r\n\r\nconst SidebarSeparator = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"separator\"\r\n className={cn('mx-2 h-px border-t border-sidebar-border', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarSeparator.displayName = 'SidebarSeparator';\r\n\r\n// ─── Group ────────────────────────────────────────────────────────────────────\r\n\r\nconst SidebarGroup = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"group\"\r\n className={cn('relative flex flex-col w-full min-w-0 px-2 py-1', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarGroup.displayName = 'SidebarGroup';\r\n\r\nconst SidebarGroupLabel = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n const { state } = useSidebar();\r\n return (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"group-label\"\r\n className={cn(\r\n 'flex h-8 shrink-0 items-center rounded-md px-2',\r\n 'text-xs font-semibold text-sidebar-foreground/50 uppercase tracking-wider',\r\n 'transition-all duration-200 overflow-hidden whitespace-nowrap select-none',\r\n state === 'collapsed' ? 'opacity-0 h-0 mb-0 hidden' : 'opacity-100',\r\n className\r\n )}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nSidebarGroupLabel.displayName = 'SidebarGroupLabel';\r\n\r\nconst SidebarGroupContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"group-content\"\r\n className={cn('w-full', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarGroupContent.displayName = 'SidebarGroupContent';\r\n\r\n// ─── Menu ─────────────────────────────────────────────────────────────────────\r\n\r\nconst SidebarMenu = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\r\n ({ className, ...props }, ref) => (\r\n <ul\r\n ref={ref}\r\n data-sidebar=\"menu\"\r\n className={cn('flex flex-col gap-0.5 list-none m-0 p-0 w-full', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarMenu.displayName = 'SidebarMenu';\r\n\r\nconst SidebarMenuItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\r\n ({ className, ...props }, ref) => (\r\n <li\r\n ref={ref}\r\n data-sidebar=\"menu-item\"\r\n className={cn('group/menu-item relative', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarMenuItem.displayName = 'SidebarMenuItem';\r\n\r\n// ─── SidebarMenuButton ────────────────────────────────────────────────────────\r\n\r\nconst menuButtonVariants = tv({\r\n base: [\r\n 'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md',\r\n 'text-sm font-medium outline-none ring-sidebar-ring transition-all duration-150',\r\n 'hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',\r\n 'focus-visible:ring-2 active:bg-sidebar-accent/80',\r\n 'disabled:pointer-events-none disabled:opacity-50',\r\n 'group-has-data-[sidebar=menu-action]/menu-item:pr-8',\r\n // Data state active\r\n 'data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[active=true]:font-semibold',\r\n ],\r\n variants: {\r\n size: {\r\n sm: 'h-7 text-xs px-2',\r\n md: 'h-9 px-2',\r\n lg: 'h-11 text-base px-3',\r\n },\r\n collapsed: {\r\n true: 'justify-center px-0',\r\n false: 'justify-start',\r\n },\r\n },\r\n defaultVariants: { size: 'md', collapsed: false },\r\n});\r\n\r\nexport interface SidebarMenuButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n asChild?: boolean;\r\n isActive?: boolean;\r\n tooltip?: string;\r\n size?: 'sm' | 'md' | 'lg';\r\n}\r\n\r\nconst SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonProps>(\r\n ({ className, isActive = false, tooltip, size = 'md', children, ...props }, ref) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n\r\n const button = (\r\n <button\r\n ref={ref}\r\n type=\"button\"\r\n data-sidebar=\"menu-button\"\r\n data-active={isActive}\r\n data-size={size}\r\n className={menuButtonVariants({ size, collapsed: isCollapsed, className })}\r\n {...props}\r\n >\r\n {isCollapsed\r\n ? React.Children.toArray(children)[0]\r\n : children}\r\n </button>\r\n );\r\n\r\n if (isCollapsed && tooltip) {\r\n return (\r\n <Tooltip content={tooltip} side=\"right\">\r\n {button}\r\n </Tooltip>\r\n );\r\n }\r\n\r\n return button;\r\n }\r\n);\r\nSidebarMenuButton.displayName = 'SidebarMenuButton';\r\n\r\n// ─── SidebarNavLink — wraps React Router NavLink ─────────────────────────────\r\n\r\nexport interface SidebarNavLinkProps {\r\n to: string;\r\n icon?: React.ReactNode;\r\n label: string;\r\n end?: boolean;\r\n badge?: React.ReactNode;\r\n size?: 'sm' | 'md' | 'lg';\r\n className?: string;\r\n}\r\n\r\nconst SidebarNavLink: React.FC<SidebarNavLinkProps> = ({\r\n to,\r\n icon,\r\n label,\r\n end = false,\r\n badge,\r\n size = 'md',\r\n className,\r\n}) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n\r\n const link = (\r\n <NavLink\r\n to={to}\r\n end={end}\r\n className={({ isActive }) => cn(\r\n menuButtonVariants({ size, collapsed: isCollapsed, className }),\r\n isActive ? 'bg-sidebar-accent text-sidebar-accent-foreground font-semibold' : 'text-sidebar-foreground/70'\r\n )}\r\n >\r\n {icon && (\r\n <span className=\"shrink-0 flex h-4 w-4 items-center justify-center\">\r\n {icon}\r\n </span>\r\n )}\r\n {!isCollapsed && (\r\n <>\r\n <span className=\"flex-1 truncate\">{label}</span>\r\n {badge && <span className=\"ml-auto shrink-0\">{badge}</span>}\r\n </>\r\n )}\r\n </NavLink>\r\n );\r\n\r\n if (isCollapsed && label) {\r\n return (\r\n <Tooltip content={label} side=\"right\">\r\n <span className=\"block\">{link}</span>\r\n </Tooltip>\r\n );\r\n }\r\n\r\n return link;\r\n};\r\nSidebarNavLink.displayName = 'SidebarNavLink';\r\n\r\n// ─── SidebarMenuCollapsible — nhóm có sub-items ───────────────────────────────\r\n\r\nexport interface SidebarMenuCollapsibleProps {\r\n id: string;\r\n icon: React.ReactNode;\r\n label: string;\r\n children: React.ReactNode;\r\n defaultOpen?: boolean;\r\n isChildActive?: boolean;\r\n}\r\n\r\nconst SidebarMenuCollapsible: React.FC<SidebarMenuCollapsibleProps> = ({\r\n icon,\r\n label,\r\n children,\r\n defaultOpen = false,\r\n isChildActive = false,\r\n}) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n\r\n const [isOpen, setIsOpen] = React.useState(defaultOpen || isChildActive);\r\n const prevOpenRef = React.useRef(isOpen);\r\n\r\n // Khi sidebar collapse → đóng tất cả sub-menu, ghi nhớ state\r\n // Khi sidebar expand → khôi phục state cũ\r\n React.useEffect(() => {\r\n if (isCollapsed) {\r\n prevOpenRef.current = isOpen;\r\n setIsOpen(false);\r\n } else {\r\n setIsOpen(prevOpenRef.current);\r\n }\r\n // eslint-disable-next-line react-hooks/exhaustive-deps\r\n }, [isCollapsed]);\r\n\r\n // Khi có child active, mở group\r\n React.useEffect(() => {\r\n if (isChildActive && !isCollapsed) {\r\n setIsOpen(true);\r\n prevOpenRef.current = true;\r\n }\r\n }, [isChildActive, isCollapsed]);\r\n\r\n const trigger = (\r\n <button\r\n type=\"button\"\r\n aria-expanded={isOpen}\r\n data-active={isChildActive && isCollapsed}\r\n onClick={() => {\r\n if (!isCollapsed) {\r\n const next = !isOpen;\r\n setIsOpen(next);\r\n prevOpenRef.current = next;\r\n }\r\n }}\r\n className={menuButtonVariants({\r\n collapsed: isCollapsed,\r\n className:\r\n isChildActive && isCollapsed\r\n ? 'text-sidebar-accent-foreground'\r\n : 'text-sidebar-foreground/70',\r\n })}\r\n >\r\n <span className=\"shrink-0 flex h-4 w-4 items-center justify-center\">{icon}</span>\r\n {!isCollapsed && (\r\n <>\r\n <span className=\"flex-1 truncate text-left\">{label}</span>\r\n <ChevronRight\r\n className={cn(\r\n 'ml-auto h-3.5 w-3.5 shrink-0 text-sidebar-foreground/40',\r\n 'transition-transform duration-200',\r\n isOpen && 'rotate-90'\r\n )}\r\n />\r\n </>\r\n )}\r\n </button>\r\n );\r\n\r\n return (\r\n <>\r\n {isCollapsed ? (\r\n <Tooltip content={label} side=\"right\">\r\n <span className=\"block\">{trigger}</span>\r\n </Tooltip>\r\n ) : (\r\n trigger\r\n )}\r\n\r\n {/* Sub-items với animation mượt - Sử dụng SidebarMenuSub (ul) để hợp lệ HTML */}\r\n <SidebarMenuSub\r\n className={cn(\r\n 'overflow-hidden transition-all duration-200 ease-in-out',\r\n !isCollapsed && isOpen ? 'max-h-[800px] opacity-100' : 'max-h-0 opacity-0'\r\n )}\r\n >\r\n {children}\r\n </SidebarMenuSub>\r\n </>\r\n );\r\n};\r\nSidebarMenuCollapsible.displayName = 'SidebarMenuCollapsible';\r\n\r\n// ─── SidebarMenuSub ───────────────────────────────────────────────────────────\r\n\r\nconst SidebarMenuSub = React.forwardRef<HTMLUListElement, React.HTMLAttributes<HTMLUListElement>>(\r\n ({ className, ...props }, ref) => {\r\n const { state } = useSidebar();\r\n if (state === 'collapsed') return null;\r\n return (\r\n <ul\r\n ref={ref}\r\n data-sidebar=\"menu-sub\"\r\n className={cn('mx-3.5 flex min-w-0 flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 list-none', className)}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nSidebarMenuSub.displayName = 'SidebarMenuSub';\r\n\r\nconst SidebarMenuSubItem = React.forwardRef<HTMLLIElement, React.HTMLAttributes<HTMLLIElement>>(\r\n (props, ref) => <li ref={ref} {...props} />\r\n);\r\nSidebarMenuSubItem.displayName = 'SidebarMenuSubItem';\r\n\r\n// ─── Badge & Skeleton ─────────────────────────────────────────────────────────\r\n\r\nconst SidebarMenuBadge = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => (\r\n <div\r\n ref={ref}\r\n data-sidebar=\"menu-badge\"\r\n className={cn('ml-auto flex h-5 min-w-5 items-center justify-center rounded-full bg-primary/10 px-1 text-xs font-medium text-primary', className)}\r\n {...props}\r\n />\r\n )\r\n);\r\nSidebarMenuBadge.displayName = 'SidebarMenuBadge';\r\n\r\nconst SidebarMenuSkeleton: React.FC<{ showIcon?: boolean }> = ({ showIcon = true }) => (\r\n <div className=\"flex h-9 items-center gap-2 rounded-md px-2\">\r\n {showIcon && <div className=\"h-4 w-4 rounded bg-sidebar-accent animate-pulse shrink-0\" />}\r\n <div className=\"h-4 flex-1 rounded bg-sidebar-accent animate-pulse\" />\r\n </div>\r\n);\r\n\r\n// ─── User Menu Popover (shadcn style) ─────────────────────────────────────────\r\n\r\ninterface UserMenuPopoverProps {\r\n name: string;\r\n email: string;\r\n avatar?: string;\r\n children?: React.ReactNode;\r\n}\r\n\r\nconst UserMenuPopover: React.FC<UserMenuPopoverProps> = ({ name, email, avatar, children }) => {\r\n const { state } = useSidebar();\r\n const isCollapsed = state === 'collapsed';\r\n const [open, setOpen] = React.useState(false);\r\n\r\n const trigger = (\r\n <BasePopover.Trigger\r\n render={\r\n <button\r\n type=\"button\"\r\n data-active={open}\r\n className={cn(\r\n menuButtonVariants({ size: 'lg', collapsed: isCollapsed }),\r\n 'data-[active=true]:bg-sidebar-accent'\r\n )}\r\n >\r\n <img\r\n src={avatar || 'https://i.pravatar.cc/100'}\r\n alt={name}\r\n className=\"w-8 h-8 rounded-lg shrink-0 object-cover border border-sidebar-border\"\r\n />\r\n {!isCollapsed && (\r\n <>\r\n <div className=\"flex-1 text-left overflow-hidden grid\">\r\n <span className=\"text-sm font-semibold truncate leading-tight\">{name}</span>\r\n <span className=\"text-xs text-sidebar-foreground/50 truncate leading-tight\">{email}</span>\r\n </div>\r\n <ChevronsUpDown className=\"ml-auto h-4 w-4 shrink-0 text-sidebar-foreground/40\" />\r\n </>\r\n )}\r\n </button>\r\n }\r\n />\r\n );\r\n\r\n return (\r\n <BasePopover.Root open={open} onOpenChange={setOpen}>\r\n {isCollapsed ? (\r\n <Tooltip content={name} side=\"right\">\r\n <span className=\"block\">{trigger}</span>\r\n </Tooltip>\r\n ) : (\r\n trigger\r\n )}\r\n <BasePopover.Portal>\r\n <BasePopover.Positioner side=\"right\" align=\"end\" sideOffset={8}>\r\n <BasePopover.Popup\r\n className={cn(\r\n 'z-50 w-64 rounded-xl border border-border bg-popover shadow-xl outline-none p-1',\r\n 'data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95',\r\n 'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95'\r\n )}\r\n >\r\n {/* User info header */}\r\n <div className=\"flex items-center gap-3 p-3 pb-2 border-b border-border/50\">\r\n <img\r\n src={avatar || 'https://i.pravatar.cc/100'}\r\n alt={name}\r\n className=\"w-10 h-10 rounded-lg object-cover border border-border\"\r\n />\r\n <div className=\"flex-1 overflow-hidden\">\r\n <p className=\"text-sm font-semibold truncate\">{name}</p>\r\n <p className=\"text-xs text-muted-foreground truncate\">{email}</p>\r\n </div>\r\n </div>\r\n\r\n {/* Menu items */}\r\n <div className=\"py-1\">{children}</div>\r\n </BasePopover.Popup>\r\n </BasePopover.Positioner>\r\n </BasePopover.Portal>\r\n </BasePopover.Root>\r\n );\r\n};\r\n\r\n// ─── UserMenuItem (item trong popover) ───────────────────────────────────────\r\n\r\ninterface UserMenuItemProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {\r\n icon?: React.ReactNode;\r\n destructive?: boolean;\r\n}\r\n\r\nconst UserMenuItem: React.FC<UserMenuItemProps> = ({ icon, children, destructive, className, ...props }) => (\r\n <button\r\n type=\"button\"\r\n className={cn(\r\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',\r\n 'hover:bg-muted outline-none focus-visible:bg-muted',\r\n destructive ? 'text-destructive hover:text-destructive' : 'text-foreground',\r\n className\r\n )}\r\n {...props}\r\n >\r\n {icon && <span className=\"shrink-0 h-4 w-4 flex items-center justify-center\">{icon}</span>}\r\n {children}\r\n </button>\r\n);\r\n\r\n// ─── Exports ──────────────────────────────────────────────────────────────────\r\n\r\nexport {\r\n SidebarProvider,\r\n SidebarTrigger,\r\n Sidebar,\r\n SidebarRail,\r\n SidebarInset,\r\n SidebarHeader,\r\n SidebarFooter,\r\n SidebarContent,\r\n SidebarSeparator,\r\n SidebarGroup,\r\n SidebarGroupLabel,\r\n SidebarGroupContent,\r\n SidebarMenu,\r\n SidebarMenuItem,\r\n SidebarMenuButton,\r\n SidebarNavLink,\r\n SidebarMenuCollapsible,\r\n SidebarMenuSub,\r\n SidebarMenuSubItem,\r\n SidebarMenuBadge,\r\n SidebarMenuSkeleton,\r\n UserMenuPopover,\r\n UserMenuItem,\r\n};\r\n"
414
+ }
415
+ ]
416
+ },
417
+ "skeleton": {
418
+ "name": "skeleton",
419
+ "dependencies": [],
420
+ "internalDependencies": [],
421
+ "files": [
422
+ {
423
+ "path": "src/components/ui/skeleton/Skeleton.tsx",
424
+ "content": "import * as React from 'react';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst Skeleton = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(\r\n ({ className, ...props }, ref) => {\r\n return (\r\n <div\r\n ref={ref}\r\n className={cn('animate-pulse rounded-md bg-secondary', className)}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nSkeleton.displayName = 'Skeleton';\r\n\r\nexport { Skeleton };\r\n"
425
+ }
426
+ ]
427
+ },
428
+ "slider": {
429
+ "name": "slider",
430
+ "dependencies": [
431
+ "@base-ui/react",
432
+ "tailwind-variants"
433
+ ],
434
+ "internalDependencies": [],
435
+ "files": [
436
+ {
437
+ "path": "src/components/ui/slider/Slider.tsx",
438
+ "content": "import * as React from 'react';\r\nimport { Slider as BaseSlider } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst sliderVariants = tv({\r\n slots: {\r\n root: 'relative flex w-full touch-none select-none items-center py-4 data-disabled:opacity-50 data-disabled:cursor-not-allowed',\r\n control: 'relative flex w-full items-center', \r\n track: 'relative h-1.5 w-full grow overflow-hidden rounded-full bg-secondary data-disabled:bg-muted',\r\n indicator: 'absolute h-full bg-primary data-disabled:bg-muted-foreground/30',\r\n thumb: 'block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 data-disabled:border-muted-foreground data-disabled:bg-muted data-disabled:pointer-events-none',\r\n }\r\n});\r\n\r\nconst { root, control, track, indicator, thumb } = sliderVariants();\r\n\r\nexport interface SliderProps extends React.ComponentPropsWithoutRef<typeof BaseSlider.Root> {\r\n className?: string;\r\n}\r\n\r\nconst Slider = React.forwardRef<React.ElementRef<typeof BaseSlider.Root>, SliderProps>(\r\n ({ className, ...props }, ref) => (\r\n <BaseSlider.Root\r\n ref={ref}\r\n className={root({ className })}\r\n {...props}\r\n >\r\n {/* 1. Thêm BaseSlider.Control bọc ngoài */}\r\n <BaseSlider.Control className={control()}>\r\n \r\n <BaseSlider.Track className={track()}>\r\n <BaseSlider.Indicator className={indicator()} />\r\n </BaseSlider.Track>\r\n\r\n {/* 2. Đưa Thumb ra ngoài Track, đứng ngang hàng */}\r\n <BaseSlider.Thumb className={thumb()} />\r\n \r\n </BaseSlider.Control>\r\n </BaseSlider.Root>\r\n )\r\n)\r\nSlider.displayName = 'Slider';\r\n\r\nexport { Slider };"
439
+ }
440
+ ]
441
+ },
442
+ "spinner": {
443
+ "name": "spinner",
444
+ "dependencies": [
445
+ "tailwind-variants"
446
+ ],
447
+ "internalDependencies": [],
448
+ "files": [
449
+ {
450
+ "path": "src/components/ui/spinner/Spinner.tsx",
451
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst spinnerVariants = tv({\r\n base: 'animate-spin rounded-full border-2 border-current border-t-transparent',\r\n variants: {\r\n size: {\r\n xs: 'h-3 w-3 border-[1.5px]',\r\n sm: 'h-4 w-4 border-2',\r\n md: 'h-6 w-6 border-2',\r\n lg: 'h-8 w-8 border-3',\r\n xl: 'h-12 w-12 border-4',\r\n },\r\n variant: {\r\n primary: 'text-primary',\r\n secondary: 'text-secondary',\r\n white: 'text-white',\r\n muted: 'text-muted-foreground',\r\n }\r\n },\r\n defaultVariants: {\r\n size: 'md',\r\n variant: 'primary'\r\n }\r\n});\r\n\r\nexport interface SpinnerProps\r\n extends React.HTMLAttributes<HTMLDivElement>,\r\n VariantProps<typeof spinnerVariants> {}\r\n\r\nconst Spinner = React.forwardRef<HTMLDivElement, SpinnerProps>(\r\n ({ className, size, variant, ...props }, ref) => {\r\n return (\r\n <div\r\n ref={ref}\r\n className={spinnerVariants({ size, variant, className })}\r\n role=\"status\"\r\n aria-label=\"Loading\"\r\n {...props}\r\n >\r\n <span className=\"sr-only\">Loading...</span>\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nSpinner.displayName = 'Spinner';\r\n\r\nexport { Spinner };\r\n"
452
+ }
453
+ ]
454
+ },
455
+ "switch": {
456
+ "name": "switch",
457
+ "dependencies": [
458
+ "@base-ui/react",
459
+ "tailwind-variants"
460
+ ],
461
+ "internalDependencies": [],
462
+ "files": [
463
+ {
464
+ "path": "src/components/ui/switch/Switch.tsx",
465
+ "content": "import * as React from 'react';\r\nimport { Switch as BaseSwitch } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@/lib/utils/cn';\r\n\r\nconst switchVariants = tv({\r\n slots: {\r\n root: 'group inline-flex h-6 w-11 shrink-0 items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-checked:bg-primary data-unchecked:bg-switch-background',\r\n thumb: 'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-checked:translate-x-5 data-unchecked:translate-x-0',\r\n },\r\n variants: {\r\n size: {\r\n sm: {\r\n root: 'h-5 w-9',\r\n thumb: 'h-4 w-4 data-checked:translate-x-4',\r\n },\r\n md: {\r\n root: 'h-6 w-11',\r\n thumb: 'h-5 w-5 data-checked:translate-x-5',\r\n }\r\n }\r\n },\r\n defaultVariants: {\r\n size: 'md'\r\n }\r\n});\r\n\r\n\r\n\r\nexport interface SwitchProps\r\n extends Omit<BaseSwitch.Root.Props, 'className'>,\r\n VariantProps<typeof switchVariants> {\r\n label?: string;\r\n className?: string;\r\n}\r\n\r\nconst Switch = React.forwardRef<React.ElementRef<typeof BaseSwitch.Root>, SwitchProps>(\r\n ({ className, size, label, id, ...props }, ref) => {\r\n const defaultId = React.useId();\r\n const switchId = id || defaultId;\r\n\r\n const { root, thumb } = switchVariants({ size });\r\n\r\n return (\r\n <div className={cn(\" flex items-center gap-2 w-fit\", props.disabled && \"opacity-40 cursor-not-allowed\")}>\r\n <BaseSwitch.Root\r\n ref={ref}\r\n id={switchId}\r\n className={root({ className: cn(!props.disabled && \"cursor-pointer\", className) })}\r\n {...props}\r\n >\r\n <BaseSwitch.Thumb className={thumb()} />\r\n </BaseSwitch.Root>\r\n {label && (\r\n <label\r\n htmlFor={switchId}\r\n className={cn(\"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\", props.disabled && \"cursor-not-allowed\")}\r\n >\r\n {label}\r\n </label>\r\n )}\r\n </div>\r\n );\r\n }\r\n);\r\n\r\nSwitch.displayName = 'Switch';\r\n\r\nexport { Switch };\r\n"
466
+ }
467
+ ]
468
+ },
469
+ "table": {
470
+ "name": "table",
471
+ "dependencies": [
472
+ "@tanstack/react-table",
473
+ "lucide-react"
474
+ ],
475
+ "internalDependencies": [
476
+ "button",
477
+ "checkbox",
478
+ "spinner"
479
+ ],
480
+ "files": [
481
+ {
482
+ "path": "src/components/ui/table/Table.tsx",
483
+ "content": "import React, { useEffect, useState } from 'react';\r\nimport {\r\n useReactTable,\r\n getCoreRowModel,\r\n getSortedRowModel,\r\n getPaginationRowModel,\r\n getFilteredRowModel,\r\n getExpandedRowModel,\r\n flexRender,\r\n type ColumnDef,\r\n type SortingState,\r\n type PaginationState,\r\n type RowSelectionState,\r\n type RowData,\r\n type ColumnResizeMode,\r\n} from '@tanstack/react-table';\r\n\r\ndeclare module '@tanstack/react-table' {\r\n interface ColumnMeta<TData extends RowData, TValue> {\r\n align?: 'left' | 'center' | 'right';\r\n }\r\n}\r\nimport { cn } from '@lib/utils/cn';\r\nimport { ChevronDown, ChevronUp, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';\r\nimport { Button } from '../button/Button';\r\nimport { Checkbox } from '../checkbox/Checkbox';\r\nimport { Spinner } from '../spinner/Spinner';\r\n\r\n// ─── Pagination Config ───────────────────────────────────────────────────────\r\n\r\nexport interface PaginationConfig {\r\n /** Trang hiện tại, 1-based (dùng để controlled ở server mode) */\r\n current?: number;\r\n /** Số dòng mỗi trang, default 10 */\r\n pageSize?: number;\r\n /** Tổng bản ghi từ BE — có giá trị → tự động bật server mode */\r\n total?: number;\r\n /** Danh sách page size options, default [5,10,20,50,100] */\r\n pageSizeOptions?: number[];\r\n /** Hiển thị info tổng. (total, range) => ReactNode */\r\n showTotal?: (total: number, range: [number, number]) => React.ReactNode;\r\n /** Ẩn/hiện page size selector, default true */\r\n showSizeChanger?: boolean;\r\n /**\r\n * Callback khi đổi trang / pageSize.\r\n * - Client mode: tuỳ chọn, chỉ để notify\r\n * - Server mode: bắt buộc, dùng để fetch API\r\n */\r\n onChange?: (page: number, pageSize: number) => void;\r\n}\r\n\r\nexport interface TableProps<TData, TValue = unknown> {\r\n data: TData[];\r\n columns: ColumnDef<TData, TValue>[];\r\n isLoading?: boolean;\r\n enableSorting?: boolean;\r\n enableRowSelection?: boolean;\r\n enableExpanding?: boolean;\r\n renderSubComponent?: (props: { row: TData }) => React.ReactNode;\r\n getRowCanExpand?: (row: TData) => boolean;\r\n onSelectionChange?: (selectedRows: TData[]) => void;\r\n className?: string;\r\n enableColumnResizing?: boolean;\r\n columnResizeMode?: ColumnResizeMode;\r\n /**\r\n * Cấu hình pagination.\r\n * - false / không truyền: tắt pagination\r\n * - {} object: bật pagination (client-side mặc định)\r\n * - { total }: bật server-side mode\r\n */\r\n pagination?: PaginationConfig | false;\r\n emptyText?: string;\r\n}\r\n\r\nexport function Table<TData, TValue = unknown>({\r\n data,\r\n columns,\r\n isLoading = false,\r\n enableSorting = true,\r\n enableRowSelection = false,\r\n enableExpanding = false,\r\n renderSubComponent,\r\n getRowCanExpand,\r\n onSelectionChange,\r\n className,\r\n enableColumnResizing = false,\r\n columnResizeMode = 'onChange',\r\n pagination: paginationProp = {},\r\n emptyText = 'Không có dữ liệu',\r\n}: TableProps<TData>) {\r\n // Xác định có bật pagination không\r\n const paginationEnabled = paginationProp !== false;\r\n const cfg = paginationEnabled ? (paginationProp as PaginationConfig) : {};\r\n\r\n // Server mode khi có cfg.total\r\n const isServerMode = paginationEnabled && cfg.total !== undefined;\r\n const pageSizeOptions = cfg.pageSizeOptions ?? [5, 10, 20, 50, 100];\r\n\r\n // Internal pagination state (0-based pageIndex cho tanstack)\r\n const [page, setPage] = useState(cfg.current ?? 1); // 1-based\r\n const [pageSize, setPageSize] = useState(cfg.pageSize ?? 10);\r\n\r\n // Sync controlled current từ ngoài vào (server mode)\r\n useEffect(() => {\r\n if (cfg.current !== undefined) setPage(cfg.current);\r\n }, [cfg.current]);\r\n\r\n // Derived\r\n const pageIndex = page - 1; // 0-based cho tanstack\r\n const totalRows = isServerMode ? cfg.total! : data.length;\r\n const pageCount = isServerMode ? Math.ceil(totalRows / pageSize) : undefined;\r\n\r\n const tanstackPagination: PaginationState = { pageIndex, pageSize };\r\n\r\n const handlePaginationChange = (updater: PaginationState | ((prev: PaginationState) => PaginationState)) => {\r\n const next = typeof updater === 'function' ? updater(tanstackPagination) : updater;\r\n const newPage = next.pageIndex + 1; // convert về 1-based\r\n const newPageSize = next.pageSize;\r\n\r\n if (!isServerMode) {\r\n setPage(newPage);\r\n setPageSize(newPageSize);\r\n }\r\n cfg.onChange?.(newPage, newPageSize);\r\n };\r\n\r\n const [sorting, setSorting] = useState<SortingState>([]);\r\n const [rowSelection, setRowSelection] = useState<RowSelectionState>({});\r\n\r\n const finalColumns = React.useMemo(() => {\r\n const cols = [...columns];\r\n if (enableRowSelection) {\r\n cols.unshift({\r\n id: 'select',\r\n size: 10,\r\n minSize: 5,\r\n maxSize: 10,\r\n meta: { align: 'center' },\r\n header: ({ table }) => (\r\n <div className=\"flex items-center justify-center\">\r\n <Checkbox\r\n size=\"sm\"\r\n checked={table.getIsAllRowsSelected()}\r\n indeterminate={table.getIsSomeRowsSelected()}\r\n onCheckedChange={(checked) => table.toggleAllRowsSelected(!!checked)}\r\n />\r\n </div>\r\n ),\r\n cell: ({ row }) => (\r\n <div className=\"flex items-center justify-center\">\r\n <Checkbox\r\n size=\"sm\"\r\n checked={row.getIsSelected()}\r\n disabled={!row.getCanSelect()}\r\n onCheckedChange={(checked) => row.toggleSelected(!!checked)}\r\n />\r\n </div>\r\n ),\r\n enableSorting: false,\r\n });\r\n }\r\n if (enableExpanding) {\r\n cols.unshift({\r\n id: 'expander',\r\n header: () => null,\r\n size: 10,\r\n minSize: 10,\r\n maxSize: 10,\r\n meta: { align: 'center' },\r\n cell: ({ row }) => row.getCanExpand() ? (\r\n <div className=\"flex items-center justify-center\">\r\n <span\r\n onClick={row.getToggleExpandedHandler()}\r\n className=\"hover:bg-muted text-muted-foreground transition-colors cursor-pointer outline-none focus:ring-2 focus:ring-primary/50 p-1 rounded-md border border-border\"\r\n >\r\n {row.getIsExpanded() ? <ChevronDown className=\"w-3 h-3\" /> : <ChevronRight className=\"w-3 h-3\" />}\r\n </span>\r\n </div>\r\n ) : null,\r\n enableSorting: false,\r\n });\r\n }\r\n return cols;\r\n }, [columns, enableRowSelection, enableExpanding]);\r\n\r\n const table = useReactTable({\r\n data,\r\n columns: finalColumns,\r\n state: { sorting, rowSelection, pagination: tanstackPagination },\r\n onSortingChange: setSorting,\r\n onRowSelectionChange: setRowSelection,\r\n onPaginationChange: handlePaginationChange,\r\n getCoreRowModel: getCoreRowModel(),\r\n getSortedRowModel: enableSorting ? getSortedRowModel() : undefined,\r\n getPaginationRowModel: paginationEnabled ? getPaginationRowModel() : undefined,\r\n getFilteredRowModel: getFilteredRowModel(),\r\n getExpandedRowModel: getExpandedRowModel(),\r\n columnResizeMode,\r\n enableColumnResizing,\r\n enableRowSelection,\r\n manualPagination: isServerMode,\r\n pageCount: isServerMode ? pageCount : undefined,\r\n getRowCanExpand: getRowCanExpand ? (row) => getRowCanExpand(row.original) : () => !!renderSubComponent,\r\n });\r\n\r\n // Input go-to-page\r\n const [inputValue, setInputValue] = useState<string | number>(page);\r\n useEffect(() => { setInputValue(page); }, [page]);\r\n\r\n useEffect(() => {\r\n if (onSelectionChange) {\r\n const selected = table.getSelectedRowModel().rows.map(row => row.original);\r\n onSelectionChange(selected);\r\n }\r\n }, [rowSelection, onSelectionChange, table]);\r\n\r\n // Computed display values\r\n const currentPageIndex = table.getState().pagination.pageIndex;\r\n const currentPageSize = table.getState().pagination.pageSize;\r\n const from = currentPageIndex * currentPageSize + 1;\r\n const to = Math.min((currentPageIndex + 1) * currentPageSize, totalRows);\r\n const totalPageCount = isServerMode ? (pageCount ?? 1) : (table.getPageCount() || 1);\r\n\r\n return (\r\n <div className={cn(\"relative w-full rounded-md border border-border bg-background flex flex-col overflow-hidden\", className)}>\r\n\r\n {/* Loading Overlay */}\r\n {isLoading && (\r\n <div className=\"absolute inset-0 bg-white/60 z-10 flex items-center justify-center backdrop-blur-[0.5px]\">\r\n <Spinner size=\"lg\" variant=\"primary\" />\r\n </div>\r\n )}\r\n\r\n <div className=\"overflow-x-auto w-full\">\r\n <table\r\n className=\"w-full text-sm text-left text-foreground whitespace-nowrap\"\r\n style={{\r\n width: enableColumnResizing ? table.getCenterTotalSize() : undefined,\r\n tableLayout: enableColumnResizing ? 'fixed' : 'auto'\r\n }}\r\n >\r\n <thead className=\"text-xs text-muted-foreground bg-muted/50 border-b border-border\">\r\n {table.getHeaderGroups().map(headerGroup => (\r\n <tr key={headerGroup.id} >\r\n {headerGroup.headers.map(header => {\r\n const meta = header.column.columnDef.meta;\r\n const align = meta?.align || 'left';\r\n const canSort = header.column.getCanSort() && enableSorting && header.column.id !== 'select';\r\n\r\n return (\r\n <th\r\n key={header.id}\r\n colSpan={header.colSpan}\r\n style={{\r\n width: enableColumnResizing ? header.getSize() : header.column.columnDef.size,\r\n position: 'relative'\r\n }}\r\n aria-sort={\r\n canSort\r\n ? header.column.getIsSorted() === 'desc'\r\n ? 'descending'\r\n : header.column.getIsSorted() === 'asc'\r\n ? 'ascending'\r\n : 'none'\r\n : undefined\r\n }\r\n className={cn(\r\n header.column.id === 'select' || header.column.id === 'expander' ? \"px-1\" : \"px-2\",\r\n \"py-3 font-semibold tracking-wide border border-border transition-colors group/header\",\r\n canSort ? \"cursor-pointer select-none hover:bg-muted\" : \"\",\r\n align === 'center' ? \"text-center\" : align === 'right' ? \"text-right\" : \"text-left\"\r\n )}\r\n >\r\n <div\r\n className={cn(\"flex flex-col h-full\")}\r\n onClick={canSort ? header.column.getToggleSortingHandler() : undefined}\r\n >\r\n {header.isPlaceholder ? null : (\r\n <div className={cn(\r\n \"flex items-center gap-2\",\r\n align === 'center' ? \"justify-center\" : align === 'right' ? \"justify-end\" : \"justify-between\"\r\n )}>\r\n <span>{flexRender(header.column.columnDef.header, header.getContext())}</span>\r\n {canSort && (\r\n <span className=\"shrink-0\">\r\n {{\r\n asc: <ChevronUp className=\"w-4 h-4 text-primary\" />,\r\n desc: <ChevronDown className=\"w-4 h-4 text-primary\" />,\r\n }[header.column.getIsSorted() as string] ?? (\r\n <div className=\"flex flex-col opacity-30 -space-y-1 hover:opacity-100 transition-opacity\">\r\n <ChevronUp className=\"w-3 h-3\" />\r\n <ChevronDown className=\"w-3 h-3\" />\r\n </div>\r\n )}\r\n </span>\r\n )}\r\n </div>\r\n )}\r\n </div>\r\n\r\n {/* Resize Handle */}\r\n {enableColumnResizing && header.column.getCanResize() && (\r\n <div\r\n {...{\r\n onMouseDown: header.getResizeHandler(),\r\n onTouchStart: header.getResizeHandler(),\r\n className: cn(\r\n \"absolute right-0 top-0 h-full w-1 cursor-col-resize select-none touch-none hover:bg-primary/50 transition-colors\",\r\n header.column.getIsResizing() ? \"bg-primary w-1.5 z-10\" : \"bg-transparent\"\r\n ),\r\n }}\r\n />\r\n )}\r\n </th>\r\n );\r\n })}\r\n </tr>\r\n ))}\r\n </thead>\r\n <tbody className=\"\">\r\n {!isLoading && data.length === 0 ? (\r\n <tr>\r\n <td colSpan={finalColumns.length} className=\" px-4 py-16 text-center text-muted-foreground\">\r\n <div className=\"flex flex-col items-center justify-center space-y-2\">\r\n <span className=\"text-muted-foreground/50\">\r\n <svg className=\"w-12 h-12\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\">\r\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={1} d=\"M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4\" />\r\n </svg>\r\n </span>\r\n <span>{emptyText}</span>\r\n </div>\r\n </td>\r\n </tr>\r\n ) : (\r\n table.getRowModel().rows.map(row => (\r\n <React.Fragment key={row.id}>\r\n <tr\r\n className={cn(\r\n \"hover:bg-muted/50 transition-colors group\",\r\n row.getIsSelected() ? \"bg-primary/5 hover:bg-primary/10\" : \"\",\r\n row.getIsExpanded() ? \"bg-primary/5\" : \"\"\r\n )}\r\n >\r\n {row.getVisibleCells().map(cell => {\r\n const meta = cell.column.columnDef.meta;\r\n const align = meta?.align || 'left';\r\n return (\r\n <td\r\n key={cell.id}\r\n style={{ width: enableColumnResizing ? cell.column.getSize() : cell.column.columnDef.size }}\r\n className={cn(\r\n cell.column.id === 'select' || cell.column.id === 'expander' ? \"px-1\" : \"px-2\",\r\n \"py-3 border border-border align-middle\",\r\n align === 'center' ? \"text-center\" : align === 'right' ? \"text-right\" : \"text-left\"\r\n )}\r\n >\r\n <div className={cn(\r\n \"flex items-center\",\r\n align === 'center' ? \"justify-center\" : align === 'right' ? \"justify-end\" : \"justify-start\"\r\n )}>\r\n {flexRender(cell.column.columnDef.cell, cell.getContext())}\r\n </div>\r\n </td>\r\n );\r\n })}\r\n </tr>\r\n {row.getIsExpanded() && renderSubComponent && (\r\n <tr>\r\n <td colSpan={row.getVisibleCells().length} className=\"p-0 border-b border-border whitespace-normal\">\r\n <div className=\"bg-muted/50 px-4 py-5 shadow-inner w-full border-l-4 border-l-primary/40 wrap-break-word\">\r\n {renderSubComponent({ row: row.original })}\r\n </div>\r\n </td>\r\n </tr>\r\n )}\r\n </React.Fragment>\r\n ))\r\n )}\r\n </tbody>\r\n </table>\r\n </div>\r\n\r\n {/* Pagination Controls */}\r\n {paginationEnabled && totalPageCount > 0 && (\r\n <div className=\"flex flex-col sm:flex-row items-center justify-between px-3 py-2.5 border-t border-border bg-muted/50 gap-2\">\r\n {/* showTotal info */}\r\n <div className=\"text-xs text-muted-foreground shrink-0 order-2 sm:order-1\">\r\n {cfg.showTotal\r\n ? cfg.showTotal(totalRows, [from, to])\r\n : <>Show <b>{from}</b>–<b>{to}</b> of <b>{totalRows}</b> results</>\r\n }\r\n </div>\r\n\r\n {/* Controls */}\r\n <div className=\"flex items-center gap-2 overflow-x-auto order-1 sm:order-2 w-full sm:w-auto pb-0.5 sm:pb-0\">\r\n {/* Page size */}\r\n {(cfg.showSizeChanger !== false) && (\r\n <select\r\n value={currentPageSize}\r\n onChange={e => table.setPageSize(Number(e.target.value))}\r\n className=\"shrink-0 px-2 py-1 text-xs border border-border rounded-md bg-background text-foreground outline-none focus:border-primary focus:ring-1 focus:ring-primary cursor-pointer\"\r\n >\r\n {pageSizeOptions.map(s => (\r\n <option key={s} value={s}>{s} / trang</option>\r\n ))}\r\n </select>\r\n )}\r\n\r\n {/* Nav buttons */}\r\n <div className=\"flex items-center gap-1 shrink-0\">\r\n <button\r\n className=\"p-1 rounded border border-border text-muted-foreground bg-background hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\r\n onClick={() => table.setPageIndex(0)}\r\n disabled={!table.getCanPreviousPage()}\r\n aria-label=\"First page\"\r\n >\r\n <ChevronsLeft className=\"w-3.5 h-3.5\" />\r\n </button>\r\n <button\r\n className=\"p-1 rounded border border-border text-muted-foreground bg-background hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\r\n onClick={() => table.previousPage()}\r\n disabled={!table.getCanPreviousPage()}\r\n aria-label=\"Previous page\"\r\n >\r\n <ChevronLeft className=\"w-3.5 h-3.5\" />\r\n </button>\r\n\r\n <span className=\"text-xs font-medium px-2.5 py-1 bg-background border border-border rounded shrink-0 min-w-14 text-center text-foreground\">\r\n {currentPageIndex + 1} / {totalPageCount}\r\n </span>\r\n\r\n <button\r\n className=\"p-1 rounded border border-border text-muted-foreground bg-background hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\r\n onClick={() => table.nextPage()}\r\n disabled={!table.getCanNextPage()}\r\n aria-label=\"Next page\"\r\n >\r\n <ChevronRight className=\"w-3.5 h-3.5\" />\r\n </button>\r\n <button\r\n className=\"p-1 rounded border border-border text-muted-foreground bg-background hover:bg-muted disabled:opacity-40 disabled:cursor-not-allowed transition-colors\"\r\n onClick={() => table.setPageIndex(totalPageCount - 1)}\r\n disabled={!table.getCanNextPage()}\r\n aria-label=\"Last page\"\r\n >\r\n <ChevronsRight className=\"w-3.5 h-3.5\" />\r\n </button>\r\n </div>\r\n\r\n {/* Go to page */}\r\n <div className=\"flex items-center gap-1.5 text-xs text-muted-foreground border-l border-border pl-2 ml-1 shrink-0\">\r\n <span className=\"hidden sm:inline\">Trang</span>\r\n <input\r\n type=\"number\"\r\n min={1}\r\n max={totalPageCount}\r\n value={inputValue}\r\n onChange={e => {\r\n const val = e.target.value;\r\n setInputValue(val);\r\n const p = val ? Number(val) - 1 : 0;\r\n if (val && p >= 0 && p < totalPageCount) {\r\n table.setPageIndex(p);\r\n }\r\n }}\r\n onBlur={() => setInputValue(currentPageIndex + 1)}\r\n className=\"w-10 px-1 py-1 text-xs border border-border rounded bg-background text-foreground text-center outline-none focus:border-primary focus:ring-1 focus:ring-primary\"\r\n />\r\n </div>\r\n </div>\r\n </div>\r\n )}\r\n </div>\r\n );\r\n}\r\n"
484
+ }
485
+ ]
486
+ },
487
+ "tabs": {
488
+ "name": "tabs",
489
+ "dependencies": [
490
+ "@base-ui/react",
491
+ "tailwind-variants"
492
+ ],
493
+ "internalDependencies": [],
494
+ "files": [
495
+ {
496
+ "path": "src/components/ui/tabs/Tabs.tsx",
497
+ "content": "import * as React from 'react';\r\nimport { Tabs as BaseTabs } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst tabsVariants = tv({\r\n slots: {\r\n rootSlots: 'flex flex-col w-full',\r\n list: 'relative inline-flex items-center justify-start rounded-lg bg-muted p-1 text-muted-foreground w-fit',\r\n indicator: 'absolute top-1 bottom-1 left-[var(--active-tab-left)] w-[var(--active-tab-width)] rounded-md bg-background shadow-sm transition-all duration-300 ease-out z-0',\r\n trigger: 'relative z-10 inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1.5 text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-active:text-foreground data-active:font-semibold',\r\n panel: 'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',\r\n }\r\n});\r\n\r\nconst { rootSlots, list, indicator, trigger, panel } = tabsVariants();\r\n\r\nexport interface TabsProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.Root> {\r\n}\r\n\r\nconst Tabs = React.forwardRef<React.ElementRef<typeof BaseTabs.Root>, TabsProps>(\r\n ({ className, ...props }, ref) => {\r\n return (\r\n <BaseTabs.Root\r\n ref={ref}\r\n className={cn(rootSlots(), className)}\r\n {...props}\r\n />\r\n );\r\n }\r\n);\r\nTabs.displayName = 'Tabs';\r\n\r\nexport interface TabsListProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.List> {\r\n}\r\n\r\nconst TabsList = React.forwardRef<React.ElementRef<typeof BaseTabs.List>, TabsListProps>(\r\n ({ className, children, ...props }, ref) => {\r\n return (\r\n <BaseTabs.List ref={ref} className={cn(list(), className)} {...props}>\r\n <BaseTabs.Indicator className={indicator()} />\r\n {children}\r\n </BaseTabs.List>\r\n );\r\n }\r\n);\r\nTabsList.displayName = 'TabsList';\r\n\r\nexport interface TabsTriggerProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.Tab> {\r\n}\r\n\r\nconst TabsTrigger = React.forwardRef<React.ElementRef<typeof BaseTabs.Tab>, TabsTriggerProps>(\r\n ({ className, ...props }, ref) => {\r\n return (\r\n <BaseTabs.Tab ref={ref} className={cn(trigger(), className)} {...props} />\r\n );\r\n }\r\n);\r\nTabsTrigger.displayName = 'TabsTrigger';\r\n\r\nexport interface TabsContentProps extends React.ComponentPropsWithoutRef<typeof BaseTabs.Panel> {\r\n}\r\n\r\nconst TabsContent = React.forwardRef<React.ElementRef<typeof BaseTabs.Panel>, TabsContentProps>(\r\n ({ className, ...props }, ref) => {\r\n return (\r\n <BaseTabs.Panel ref={ref} className={cn(panel(), className)} {...props} />\r\n );\r\n }\r\n);\r\nTabsContent.displayName = 'TabsContent';\r\n\r\nexport { Tabs, TabsList, TabsTrigger, TabsContent };\r\n"
498
+ }
499
+ ]
500
+ },
501
+ "textarea": {
502
+ "name": "textarea",
503
+ "dependencies": [
504
+ "@base-ui/react",
505
+ "tailwind-variants"
506
+ ],
507
+ "internalDependencies": [],
508
+ "files": [
509
+ {
510
+ "path": "src/components/ui/textarea/Textarea.test.tsx",
511
+ "content": "import { render, screen } from '@testing-library/react';\nimport { describe, it, expect } from 'vitest';\nimport { Textarea } from './Textarea';\nimport React from 'react';\n\ndescribe('Textarea', () => {\n it('renders correctly', () => {\n render(<Textarea placeholder=\"Type your message\" />);\n const textarea = screen.getByPlaceholderText(/type your message/i);\n expect(textarea).toBeInTheDocument();\n });\n\n it('forwards ref correctly', () => {\n const ref = React.createRef<HTMLTextAreaElement>();\n render(<Textarea ref={ref} />);\n expect(ref.current).toBeInstanceOf(HTMLTextAreaElement);\n });\n\n it('renders label and description correctly', () => {\n render(\n <Textarea \n label=\"Message\" \n description=\"Max 500 characters\" \n id=\"textarea-1\"\n />\n );\n \n expect(screen.getByText('Message')).toBeInTheDocument();\n expect(screen.getByText('Max 500 characters')).toBeInTheDocument();\n });\n\n it('displays error message and applies error styles', () => {\n const { container } = render(<Textarea error=\"This field is required\" />);\n \n expect(screen.getByText('This field is required')).toBeInTheDocument();\n \n const textarea = container.querySelector('textarea');\n expect(textarea?.className).toContain('border-danger');\n });\n});\n"
512
+ },
513
+ {
514
+ "path": "src/components/ui/textarea/Textarea.tsx",
515
+ "content": "import * as React from 'react';\nimport { Field as BaseField } from '@base-ui/react';\nimport { tv, type VariantProps } from 'tailwind-variants';\nimport { cn } from '@lib/utils/cn';\n\nconst textareaVariants = tv({\n base: 'flex min-h-[80px] w-full rounded-md border border-border bg-background px-3 py-2 text-sm focus:border-primary focus:ring-0 placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 transition-shadow',\n variants: {\n variant: {\n default: '',\n filled: 'bg-accent border-transparent focus-visible:border-primary focus-visible:ring-0',\n }\n },\n defaultVariants: {\n variant: 'default'\n }\n});\n\nexport interface TextareaProps \n extends React.TextareaHTMLAttributes<HTMLTextAreaElement>, \n VariantProps<typeof textareaVariants> {\n label?: string;\n error?: string;\n description?: string;\n}\n\nconst Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(\n ({ className, variant, label, error, description, id, ...props }, ref) => {\n const defaultId = React.useId();\n const textareaId = id || defaultId;\n\n return (\n <BaseField.Root className=\"flex flex-col gap-1.5 w-full\">\n {label && (\n <BaseField.Label htmlFor={textareaId} className=\"text-sm font-medium text-foreground leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70\">\n {label}\n </BaseField.Label>\n )}\n \n <BaseField.Control render={\n <textarea\n ref={ref}\n id={textareaId}\n className={cn(\n textareaVariants({ variant }),\n error && 'border-danger focus-visible:ring-danger',\n className\n )}\n {...props}\n />\n } />\n \n {description && !error && (\n <BaseField.Description className=\"text-[0.8rem] text-muted-foreground\">\n {description}\n </BaseField.Description>\n )}\n {error && (\n <p className=\"text-[0.8rem] font-medium text-danger\">\n {error}\n </p>\n )}\n </BaseField.Root>\n );\n }\n);\nTextarea.displayName = 'Textarea';\n\nexport { Textarea };\n"
516
+ }
517
+ ]
518
+ },
519
+ "toast": {
520
+ "name": "toast",
521
+ "dependencies": [
522
+ "sonner"
523
+ ],
524
+ "internalDependencies": [],
525
+ "files": [
526
+ {
527
+ "path": "src/components/ui/toast/Toaster.tsx",
528
+ "content": "import { Toaster as Sonner } from 'sonner';\r\nimport * as React from 'react';\r\n\r\ntype ToasterProps = React.ComponentProps<typeof Sonner>;\r\n\r\nconst Toaster = ({ ...props }: ToasterProps) => {\r\n return (\r\n <Sonner\r\n className=\"toaster group\"\r\n toastOptions={{\r\n classNames: {\r\n toast:\r\n 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',\r\n description: 'group-[.toast]:text-muted-foreground',\r\n actionButton:\r\n 'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',\r\n cancelButton:\r\n 'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',\r\n success: 'group-[.toaster]:text-success group-[.toaster]:border-success/50',\r\n error: 'group-[.toaster]:text-destructive group-[.toaster]:border-destructive/50',\r\n warning: 'group-[.toaster]:text-warning group-[.toaster]:border-warning/50',\r\n info: 'group-[.toaster]:text-blue-500 group-[.toaster]:border-blue-500/50',\r\n },\r\n }}\r\n {...props}\r\n />\r\n );\r\n};\r\n\r\nexport { Toaster };\r\n"
529
+ }
530
+ ]
531
+ },
532
+ "toggle": {
533
+ "name": "toggle",
534
+ "dependencies": [
535
+ "tailwind-variants"
536
+ ],
537
+ "internalDependencies": [],
538
+ "files": [
539
+ {
540
+ "path": "src/components/ui/toggle/Toggle.tsx",
541
+ "content": "import * as React from 'react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\nimport { cn } from '@lib/utils/cn';\r\n\r\nconst toggleVariants = tv({\r\n base: [\r\n 'inline-flex items-center justify-center gap-1.5 rounded-md font-medium text-sm',\r\n 'transition-all duration-150 cursor-pointer select-none',\r\n 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2',\r\n 'disabled:pointer-events-none disabled:opacity-50',\r\n 'border',\r\n ],\r\n variants: {\r\n variant: {\r\n default: [\r\n 'bg-transparent border-border text-muted-foreground',\r\n 'hover:bg-muted hover:text-foreground',\r\n 'data-[state=on]:bg-secondary data-[state=on]:text-foreground data-[state=on]:border-secondary',\r\n ],\r\n outline: [\r\n 'bg-transparent border-border text-muted-foreground',\r\n 'hover:border-primary/50 hover:text-primary',\r\n 'data-[state=on]:bg-primary/10 data-[state=on]:text-primary data-[state=on]:border-primary/50',\r\n ],\r\n solid: [\r\n 'bg-transparent border-transparent text-muted-foreground',\r\n 'hover:bg-muted hover:text-foreground',\r\n 'data-[state=on]:bg-primary data-[state=on]:text-primary-foreground data-[state=on]:border-primary',\r\n ],\r\n ghost: [\r\n 'bg-transparent border-transparent text-muted-foreground',\r\n 'hover:bg-muted hover:text-foreground',\r\n 'data-[state=on]:bg-muted data-[state=on]:text-foreground',\r\n ],\r\n },\r\n size: {\r\n sm: 'h-7 px-2 text-xs',\r\n md: 'h-9 px-3 text-sm',\r\n lg: 'h-11 px-4 text-base',\r\n icon: 'h-9 w-9',\r\n },\r\n },\r\n defaultVariants: {\r\n variant: 'default',\r\n size: 'md',\r\n },\r\n});\r\n\r\nexport interface ToggleProps\r\n extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onChange'>,\r\n VariantProps<typeof toggleVariants> {\r\n pressed?: boolean;\r\n defaultPressed?: boolean;\r\n onPressedChange?: (pressed: boolean) => void;\r\n}\r\n\r\nconst Toggle = React.forwardRef<HTMLButtonElement, ToggleProps>(\r\n (\r\n {\r\n pressed: controlledPressed,\r\n defaultPressed = false,\r\n onPressedChange,\r\n variant,\r\n size,\r\n className,\r\n children,\r\n ...props\r\n },\r\n ref\r\n ) => {\r\n const isControlled = controlledPressed !== undefined;\r\n const [internalPressed, setInternalPressed] = React.useState(defaultPressed);\r\n\r\n const isPressed = isControlled ? controlledPressed! : internalPressed;\r\n\r\n const handleClick = () => {\r\n const next = !isPressed;\r\n if (!isControlled) setInternalPressed(next);\r\n onPressedChange?.(next);\r\n };\r\n\r\n return (\r\n <button\r\n ref={ref}\r\n type=\"button\"\r\n aria-pressed={isPressed}\r\n data-state={isPressed ? 'on' : 'off'}\r\n onClick={handleClick}\r\n className={toggleVariants({ variant, size, className })}\r\n {...props}\r\n >\r\n {children}\r\n </button>\r\n );\r\n }\r\n);\r\n\r\nToggle.displayName = 'Toggle';\r\n\r\n// ─── ToggleGroup ─────────────────────────────────────────────────────────────\r\n\r\ninterface ToggleGroupContextValue {\r\n value: string[];\r\n onValueChange: (value: string[]) => void;\r\n type: 'single' | 'multiple';\r\n variant?: VariantProps<typeof toggleVariants>['variant'];\r\n size?: VariantProps<typeof toggleVariants>['size'];\r\n}\r\n\r\nconst ToggleGroupContext = React.createContext<ToggleGroupContextValue | null>(null);\r\n\r\nexport interface ToggleGroupProps\r\n extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onChange'> {\r\n type?: 'single' | 'multiple';\r\n value?: string[];\r\n defaultValue?: string[];\r\n onValueChange?: (value: string[]) => void;\r\n variant?: VariantProps<typeof toggleVariants>['variant'];\r\n size?: VariantProps<typeof toggleVariants>['size'];\r\n children: React.ReactNode;\r\n disabled?: boolean;\r\n}\r\n\r\nconst ToggleGroup = React.forwardRef<HTMLDivElement, ToggleGroupProps>(\r\n (\r\n {\r\n type = 'single',\r\n value: controlledValue,\r\n defaultValue = [],\r\n onValueChange,\r\n variant = 'default',\r\n size = 'md',\r\n className,\r\n children,\r\n disabled,\r\n ...props\r\n },\r\n ref\r\n ) => {\r\n const isControlled = controlledValue !== undefined;\r\n const [internalValue, setInternalValue] = React.useState<string[]>(defaultValue);\r\n const value = isControlled ? controlledValue! : internalValue;\r\n\r\n const handleValueChange = (newValues: string[]) => {\r\n if (!isControlled) setInternalValue(newValues);\r\n onValueChange?.(newValues);\r\n };\r\n\r\n return (\r\n <ToggleGroupContext.Provider value={{ value, onValueChange: handleValueChange, type, variant, size }}>\r\n <div\r\n ref={ref}\r\n role=\"group\"\r\n className={cn('inline-flex items-center gap-1', className)}\r\n aria-disabled={disabled}\r\n {...props}\r\n >\r\n {children}\r\n </div>\r\n </ToggleGroupContext.Provider>\r\n );\r\n }\r\n);\r\n\r\nToggleGroup.displayName = 'ToggleGroup';\r\n\r\nexport interface ToggleGroupItemProps\r\n extends Omit<ToggleProps, 'pressed' | 'onPressedChange'> {\r\n value: string;\r\n}\r\n\r\nconst ToggleGroupItem = React.forwardRef<HTMLButtonElement, ToggleGroupItemProps>(\r\n ({ value, variant: itemVariant, size: itemSize, children, ...props }, ref) => {\r\n const ctx = React.useContext(ToggleGroupContext);\r\n if (!ctx) throw new Error('ToggleGroupItem must be inside ToggleGroup');\r\n\r\n const { value: groupValue, onValueChange, type, variant: ctxVariant, size: ctxSize } = ctx;\r\n const isPressed = groupValue.includes(value);\r\n\r\n const handlePressedChange = (pressed: boolean) => {\r\n if (type === 'single') {\r\n onValueChange(pressed ? [value] : []);\r\n } else {\r\n onValueChange(\r\n pressed ? [...groupValue, value] : groupValue.filter((v) => v !== value)\r\n );\r\n }\r\n };\r\n\r\n return (\r\n <Toggle\r\n ref={ref}\r\n pressed={isPressed}\r\n onPressedChange={handlePressedChange}\r\n variant={itemVariant ?? ctxVariant}\r\n size={itemSize ?? ctxSize}\r\n {...props}\r\n >\r\n {children}\r\n </Toggle>\r\n );\r\n }\r\n);\r\n\r\nToggleGroupItem.displayName = 'ToggleGroupItem';\r\n\r\nexport { Toggle, ToggleGroup, ToggleGroupItem };\r\n"
542
+ }
543
+ ]
544
+ },
545
+ "tooltip": {
546
+ "name": "tooltip",
547
+ "dependencies": [
548
+ "@base-ui/react",
549
+ "tailwind-variants"
550
+ ],
551
+ "internalDependencies": [],
552
+ "files": [
553
+ {
554
+ "path": "src/components/ui/tooltip/Tooltip.tsx",
555
+ "content": "import * as React from 'react';\r\nimport { Tooltip as BaseTooltip } from '@base-ui/react';\r\nimport { tv, type VariantProps } from 'tailwind-variants';\r\n\r\nconst tooltipVariants = tv({\r\n slots: {\r\n popup: 'z-50 overflow-hidden rounded-md border border-border bg-background px-3 py-1.5 text-sm text-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 data-side-bottom:slide-in-from-top-2 data-side-left:slide-in-from-right-2 data-side-right:slide-in-from-left-2 data-side-top:slide-in-from-bottom-2',\r\n arrow: 'fill-popover',\r\n }\r\n});\r\n\r\nconst { popup, arrow } = tooltipVariants();\r\n\r\nexport interface TooltipProps extends React.ComponentPropsWithoutRef<typeof BaseTooltip.Root> {\r\n content: React.ReactNode;\r\n children: React.ReactNode;\r\n side?: 'top' | 'right' | 'bottom' | 'left';\r\n align?: 'start' | 'center' | 'end';\r\n}\r\n\r\nconst Tooltip = React.forwardRef<React.ElementRef<typeof BaseTooltip.Root>, TooltipProps>(\r\n ({ children, content, side = 'top', align = 'center', ...props }, ref) => {\r\n return (\r\n <BaseTooltip.Root {...props}>\r\n <BaseTooltip.Trigger render={children as React.ReactElement} />\r\n <BaseTooltip.Portal>\r\n <BaseTooltip.Positioner side={side} align={align} sideOffset={4}>\r\n <BaseTooltip.Popup className={popup()}>\r\n <BaseTooltip.Arrow className={arrow()} />\r\n {content}\r\n </BaseTooltip.Popup>\r\n </BaseTooltip.Positioner>\r\n </BaseTooltip.Portal>\r\n </BaseTooltip.Root>\r\n );\r\n }\r\n);\r\n\r\nTooltip.displayName = 'Tooltip';\r\n\r\nexport { Tooltip };\r\n"
556
+ }
557
+ ]
558
+ },
559
+ "vs-code": {
560
+ "name": "vs-code",
561
+ "dependencies": [
562
+ "lucide-react"
563
+ ],
564
+ "internalDependencies": [],
565
+ "files": [
566
+ {
567
+ "path": "src/components/ui/vs-code/ide/ActivityBar.tsx",
568
+ "content": "import React from 'react';\r\nimport { Files, Search, GitBranch, MonitorPlay, Settings, User } from 'lucide-react';\r\n\r\ninterface ActivityBarProps {\r\n activeTab: 'explorer' | 'search' | 'git';\r\n setActiveTab: (tab: 'explorer' | 'search' | 'git') => void;\r\n}\r\n\r\nexport function ActivityBar({ activeTab, setActiveTab }: ActivityBarProps) {\r\n return (\r\n <div className=\"w-[48px] bg-[#151515] border-r border-[#242424] flex flex-col items-center py-3 shrink-0\">\r\n <button className=\"w-full h-12 flex items-center justify-center hover:text-white transition-colors\">\r\n <svg viewBox=\"0 0 100 100\" className=\"w-7 h-7\" fill=\"currentColor\">\r\n <path d=\"M50 0L0 25v50l50 25 50-25V25L50 0zm38.1 29.8L50 48.7 11.9 29.8 50 10.7l38.1 19.1zM50 82.7L18.4 66.9v-28L50 54.8l31.6-15.9v28L50 82.7z\"/>\r\n </svg>\r\n </button>\r\n\r\n <div className=\"w-full h-[1px] bg-[#242424] my-2\" />\r\n\r\n <ActivityIcon \r\n icon={<Files className=\"w-[22px] h-[22px]\" />} \r\n active={activeTab === 'explorer'} \r\n onClick={() => setActiveTab('explorer')}\r\n />\r\n <ActivityIcon \r\n icon={<Search className=\"w-[22px] h-[22px]\" />} \r\n active={activeTab === 'search'} \r\n onClick={() => setActiveTab('search')}\r\n />\r\n <ActivityIcon \r\n icon={<GitBranch className=\"w-[22px] h-[22px]\" />} \r\n active={activeTab === 'git'} \r\n onClick={() => setActiveTab('git')}\r\n />\r\n \r\n <div className=\"flex-1\" />\r\n\r\n <ActivityIcon icon={<User className=\"w-[22px] h-[22px]\" />} />\r\n <ActivityIcon icon={<Settings className=\"w-[22px] h-[22px]\" />} />\r\n </div>\r\n );\r\n}\r\n\r\nfunction ActivityIcon({ icon, active, onClick }: { icon: React.ReactNode; active?: boolean; onClick?: () => void }) {\r\n return (\r\n <button\r\n onClick={onClick}\r\n className={`relative w-full h-[48px] flex items-center justify-center transition-colors ${active ? 'text-white' : 'text-[#666666] hover:text-[#cccccc]'}`}\r\n >\r\n {active && (\r\n <span className=\"absolute left-0 top-[10%] bottom-[10%] w-[2px] bg-blue-500 rounded-r-md\" />\r\n )}\r\n {icon}\r\n </button>\r\n );\r\n}\r\n"
569
+ },
570
+ {
571
+ "path": "src/components/ui/vs-code/ide/FileExplorer.tsx",
572
+ "content": "import React, { useState } from 'react';\r\nimport { useSandpack } from '@codesandbox/sandpack-react';\r\nimport { FilePlus, FolderPlus, Trash2, Edit2, File as FileIcon, ChevronDown, ChevronRight } from 'lucide-react';\r\n\r\nexport function FileExplorer() {\r\n const { sandpack } = useSandpack();\r\n const { files, activeFile, openFile, addFile, deleteFile } = sandpack;\r\n\r\n const [isCreating, setIsCreating] = useState<'file' | 'folder' | null>(null);\r\n const [newName, setNewName] = useState('');\r\n\r\n // Lấy danh sách files và hiển thị phẳng (flat) hoặc nhóm thư mục\r\n // Để tối ưu và tránh quá dài, hiển thị danh sách các path dạng phẳng\r\n const filePaths = Object.keys(files);\r\n\r\n const handleCreate = (e: React.FormEvent) => {\r\n e.preventDefault();\r\n if (!newName) return;\r\n \r\n // Đảm bảo có dấu / phía trước\r\n const path = newName.startsWith('/') ? newName : `${newName}`;\r\n \r\n if (isCreating === 'file') {\r\n addFile(path, '');\r\n openFile(path);\r\n } else {\r\n // Sandpack tự hiển thị folder dựa vào path của file. \r\n // Nhưng vì không có file nào thì folder không tồn tại trong sandpack\r\n // Ta tạo 1 file .gitkeep ẩn để giữ folder.\r\n addFile(`${path}/.gitkeep`, '');\r\n }\r\n \r\n setNewName('');\r\n setIsCreating(null);\r\n };\r\n\r\n return (\r\n <div className=\"flex flex-col h-full bg-[#151515] text-[#cccccc] font-sans\">\r\n {/* Header Toolbar */}\r\n <div className=\"flex items-center justify-between px-3 py-2 text-[11px] font-semibold tracking-wider text-[#999999] shrink-0 uppercase mb-1 group\">\r\n <span>Explorer</span>\r\n <div className=\"flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity\">\r\n <button onClick={() => setIsCreating('file')} className=\"p-1 hover:bg-[#2a2d2e] rounded transition-colors\" title=\"New File\">\r\n <FilePlus className=\"w-3.5 h-3.5\" />\r\n </button>\r\n <button onClick={() => setIsCreating('folder')} className=\"p-1 hover:bg-[#2a2d2e] rounded transition-colors\" title=\"New Folder\">\r\n <FolderPlus className=\"w-3.5 h-3.5\" />\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex-1 overflow-y-auto w-full outline-none\">\r\n {/* Input box for new file/folder */}\r\n {isCreating && (\r\n <div className=\"px-3 py-1 flex items-center\">\r\n {isCreating === 'file' ? <FileIcon className=\"w-3.5 h-3.5 mr-1 text-[#5c98ce]\" /> : <ChevronRight className=\"w-3.5 h-3.5 mr-1 text-[#666666]\" />}\r\n <form onSubmit={handleCreate} className=\"flex-1\">\r\n <input\r\n autoFocus\r\n type=\"text\"\r\n placeholder={isCreating === 'file' ? 'Tên file (vd: /src/App.js)' : 'Tên thư mục (vd: /src/components)'}\r\n className=\"w-full bg-[#3c3c3c] border border-[#007acc] text-[#cccccc] text-[13px] px-1 py-0.5 outline-none font-mono\"\r\n value={newName}\r\n onChange={(e) => setNewName(e.target.value)}\r\n onBlur={() => setIsCreating(null)}\r\n // Stop propagation để click vào file không làm mất focus bị lỗi\r\n onMouseDown={(e) => e.stopPropagation()}\r\n />\r\n </form>\r\n </div>\r\n )}\r\n\r\n {/* Danh sách Files hiện tại */}\r\n {filePaths.sort().map((path) => {\r\n // Bỏ qua hiển thị file ẩn như .gitkeep\r\n if (path.endsWith('.gitkeep')) return null;\r\n\r\n const isActive = path === activeFile;\r\n const fileName = path.split('/').pop() || path;\r\n \r\n return (\r\n <div\r\n key={path}\r\n onClick={() => openFile(path)}\r\n className={`flex items-center justify-between py-1 px-3 cursor-pointer text-[13px] group ${isActive ? 'bg-[#37373d] text-white' : 'hover:bg-[#2a2d2e]'}`}\r\n >\r\n <div className=\"flex items-center gap-1.5 truncate\">\r\n <FileIcon className={`w-3.5 h-3.5 shrink-0 ${isActive ? 'text-[#5c98ce]' : 'text-[#666666]'}`} />\r\n <span className={`truncate ${isActive ? '' : 'text-[#cccccc]'}`}>{fileName}</span>\r\n <span className=\"text-[10px] text-[#666666] ml-2 hidden sm:block truncate opacity-50\">{path}</span>\r\n </div>\r\n \r\n <div className=\"flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity shrink-0\">\r\n <button \r\n onClick={(e) => { e.stopPropagation(); deleteFile(path); }}\r\n className=\"p-0.5 hover:bg-[#4d4d4d] rounded text-[#cccccc] hover:text-red-400 z-10\"\r\n title=\"Delete File\"\r\n >\r\n <Trash2 className=\"w-3 h-3\" />\r\n </button>\r\n </div>\r\n </div>\r\n );\r\n })}\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
573
+ },
574
+ {
575
+ "path": "src/components/ui/vs-code/ide/IdeLayout.tsx",
576
+ "content": "import React, { useState } from 'react';\r\nimport { Group, Panel, Separator } from 'react-resizable-panels';\r\nimport { SandpackCodeEditor, SandpackPreview, SandpackConsole } from '@codesandbox/sandpack-react';\r\nimport { FileExplorer } from './FileExplorer';\r\nimport { TopBar } from './TopBar';\r\nimport { ActivityBar } from './ActivityBar';\r\n\r\nexport function IdeLayout() {\r\n const [activeTab, setActiveTab] = useState<'explorer' | 'search' | 'git'>('explorer');\r\n\r\n return (\r\n <div className=\"flex flex-col h-screen w-screen overflow-hidden bg-[#151515] text-[#999999] font-sans\">\r\n <TopBar />\r\n \r\n <div className=\"flex flex-1 min-h-0\">\r\n <ActivityBar activeTab={activeTab} setActiveTab={setActiveTab} />\r\n \r\n <Group orientation=\"horizontal\" className=\"h-full\">\r\n {/* Cột trái: Explorer / Sidebar */}\r\n <Panel defaultSize={15} minSize={10} className=\"flex flex-col border-r border-[#242424] bg-[#151515]\">\r\n {activeTab === 'explorer' && <FileExplorer />}\r\n {activeTab === 'search' && <div className=\"p-4\">Search...</div>}\r\n {activeTab === 'git' && <div className=\"p-4\">Source Control...</div>}\r\n </Panel>\r\n \r\n <ResizeHandle />\r\n\r\n {/* Cột phải: Main Editor Space */}\r\n <Panel defaultSize={85}>\r\n <Group orientation=\"horizontal\">\r\n {/* Editor + Terminal */}\r\n <Panel defaultSize={60} className=\"flex flex-col min-w-0 border-r border-[#242424]\">\r\n <Group orientation=\"vertical\">\r\n {/* Editor */}\r\n <Panel defaultSize={70} className=\"min-h-0 bg-[#1e1e1e]\">\r\n <SandpackCodeEditor \r\n showTabs \r\n showLineNumbers \r\n showInlineErrors \r\n wrapContent \r\n style={{ height: '100%' }} \r\n />\r\n </Panel>\r\n <ResizeHandle horizontal />\r\n {/* Terminal/Console */}\r\n <Panel defaultSize={30} minSize={10} className=\"flex flex-col bg-[#1e1e1e]\">\r\n <div className=\"h-9 shrink-0 bg-[#151515] border-b border-t border-[#242424] flex items-center px-4 gap-4 text-[11px] font-semibold text-[#cccccc] tracking-wider\">\r\n <span className=\"hover:text-white cursor-pointer transition-colors text-blue-500 border-b border-blue-500 pb-2 translate-y-1\">TERMINAL</span>\r\n <span className=\"hover:text-white cursor-pointer transition-colors\">OUTPUT</span>\r\n <span className=\"hover:text-white cursor-pointer transition-colors\">PROBLEMS</span>\r\n </div>\r\n <div className=\"flex-1 overflow-auto bg-[#151515]\">\r\n <SandpackConsole standalone style={{ height: '100%', background: 'transparent' }} />\r\n </div>\r\n </Panel>\r\n </Group>\r\n </Panel>\r\n\r\n <ResizeHandle />\r\n\r\n {/* Preview */}\r\n <Panel defaultSize={40} className=\"min-w-0 bg-[#ffffff]\">\r\n <SandpackPreview \r\n showNavigator \r\n showRefreshButton \r\n showOpenInCodeSandbox={false} \r\n style={{ height: '100%' }} \r\n />\r\n </Panel>\r\n </Group>\r\n </Panel>\r\n </Group>\r\n </div>\r\n\r\n {/* Status Bar */}\r\n <div className=\"h-[22px] bg-[#007acc] text-white flex items-center px-3 text-[10px] shrink-0 font-medium\">\r\n <span className=\"mr-4 hover:bg-white/20 px-1 cursor-pointer transition-colors rounded\">master*</span>\r\n <span className=\"mr-4 hover:bg-white/20 px-1 cursor-pointer transition-colors rounded\">React Preview</span>\r\n <div className=\"flex-1\" />\r\n <span className=\"hover:bg-white/20 px-1 cursor-pointer transition-colors rounded\">LF</span>\r\n <span className=\"hover:bg-white/20 px-1 ml-2 cursor-pointer transition-colors rounded\">UTF-8</span>\r\n </div>\r\n </div>\r\n );\r\n}\r\n\r\nfunction ResizeHandle({ horizontal }: { horizontal?: boolean }) {\r\n return (\r\n <Separator className={`${horizontal ? 'h-1 hover:bg-[#007acc] hover:cursor-row-resize' : 'w-1 hover:bg-[#007acc] hover:cursor-col-resize'} bg-[#242424] transition-colors`} />\r\n );\r\n}\r\n"
577
+ },
578
+ {
579
+ "path": "src/components/ui/vs-code/ide/TopBar.tsx",
580
+ "content": "import React from 'react';\r\nimport { ArrowLeft, Play, Download } from 'lucide-react';\r\nimport { useNavigate } from 'react-router-dom';\r\n\r\nexport function TopBar() {\r\n const navigate = useNavigate();\r\n\r\n return (\r\n <div className=\"h-[40px] bg-[#151515] border-b border-[#242424] flex items-center px-4 shrink-0 shadow-sm relative text-[#cccccc]\">\r\n <button\r\n onClick={() => navigate(-1)}\r\n className=\"flex items-center gap-1.5 hover:text-white transition-colors mr-4\"\r\n >\r\n <ArrowLeft className=\"w-4 h-4\" />\r\n </button>\r\n\r\n <div className=\"flex items-center gap-2 bg-[#1e1e1e] border border-[#242424] px-3 py-1 rounded-md hover:border-[#3a3a3a] transition-colors\">\r\n <span className=\"text-xs font-semibold\">Sandbox</span>\r\n <span className=\"text-[#666666] text-xs\">/</span>\r\n <span className=\"text-xs text-white\">react-playground</span>\r\n </div>\r\n\r\n <div className=\"flex-1\" />\r\n \r\n {/* Cụm menu ở giữa */}\r\n <div className=\"absolute left-1/2 -translate-x-1/2 flex items-center h-full\">\r\n <div className=\"flex items-center gap-2 bg-[#2a2d2e] rounded-md px-1 py-1 shadow-sm border border-[#3c3c3c]\">\r\n <button className=\"px-3 py-1 hover:bg-[#3c3c3c] rounded text-[11px] font-medium transition-colors flex items-center gap-1\">\r\n <Play className=\"w-3 h-3 text-green-400\" /> Start\r\n </button>\r\n <div className=\"w-[1px] h-3 bg-[#4d4d4d]\" />\r\n <button className=\"px-3 py-1 hover:bg-[#3c3c3c] rounded text-[11px] font-medium transition-colors flex items-center gap-1\">\r\n <Download className=\"w-3 h-3\" /> Export\r\n </button>\r\n </div>\r\n </div>\r\n\r\n <div className=\"flex items-center gap-3 relative z-10\">\r\n <button className=\"bg-[#007acc] hover:bg-[#005f9e] text-white text-[11px] font-semibold px-3 py-1.5 rounded transition-colors hidden sm:block\">\r\n Share\r\n </button>\r\n <button className=\"bg-[#1e1e1e] border border-[#3c3c3c] hover:bg-[#2a2a2a] text-white text-[11px] font-semibold px-3 py-1.5 rounded transition-colors hidden sm:block\">\r\n Sign In\r\n </button>\r\n </div>\r\n </div>\r\n );\r\n}\r\n"
581
+ },
582
+ {
583
+ "path": "src/components/ui/vs-code/initialFiles.ts",
584
+ "content": "export const initialFiles = {\r\n '/public/index.html': `<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n <meta charset=\"UTF-8\">\r\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n <title>Document</title>\r\n</head>\r\n<body>\r\n <div id=\"root\"></div>\r\n</body>\r\n</html>`,\r\n '/App.js': `import React from 'react';\r\nimport \"./styles.css\";\r\n\r\nexport default function App() {\r\n return (\r\n <div className=\"App\">\r\n <h1>Hello Professional Web IDE</h1>\r\n <h2>Start editing to see some magic happen!</h2>\r\n </div>\r\n );\r\n}\r\n`,\r\n '/index.js': `import React, { StrictMode } from \"react\";\r\nimport { createRoot } from \"react-dom/client\";\r\nimport \"./styles.css\";\r\n\r\nimport App from \"./App\";\r\n\r\nconst root = createRoot(document.getElementById(\"root\"));\r\nroot.render(\r\n <StrictMode>\r\n <App />\r\n </StrictMode>\r\n);`,\r\n '/styles.css': `.App {\r\n font-family: sans-serif;\r\n text-align: center;\r\n}\r\n`,\r\n '/package.json': `{\r\n \"name\": \"react-playground\",\r\n \"version\": \"1.0.0\",\r\n \"dependencies\": {\r\n \"react\": \"^18.0.0\",\r\n \"react-dom\": \"^18.0.0\",\r\n \"react-scripts\": \"^5.0.0\"\r\n },\r\n \"main\": \"/index.js\"\r\n}\r\n`\r\n};\r\n"
585
+ },
586
+ {
587
+ "path": "src/components/ui/vs-code/VsCodeIDE.tsx",
588
+ "content": "import React from 'react';\r\nimport { SandpackProvider } from '@codesandbox/sandpack-react';\r\nimport { IdeLayout } from './ide/IdeLayout';\r\n\r\nconst myCodeSandboxTheme = {\r\n colors: {\r\n surface1: '#151515',\r\n surface2: '#151515',\r\n surface3: '#2a2a2a',\r\n clickable: '#999999',\r\n base: '#808080',\r\n disabled: '#4d4d4d',\r\n hover: '#ffffff',\r\n accent: '#007acc',\r\n error: '#ff453a',\r\n errorSurface: '#ffeceb',\r\n },\r\n syntax: {\r\n plain: '#d4d4d4',\r\n comment: { color: '#6a9955', fontStyle: 'italic' as 'italic' },\r\n keyword: '#c586c0',\r\n tag: '#569cd6',\r\n punctuation: '#d4d4d4',\r\n definition: '#dcdcaa',\r\n property: '#9cdcfe',\r\n static: '#b5cea8',\r\n string: '#ce9178',\r\n },\r\n font: {\r\n body: '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"',\r\n mono: '\"Fira Code\", \"Fira Mono\", Consolas, Menlo, Monaco, \"Courier New\", monospace',\r\n size: '13px',\r\n lineHeight: '20px',\r\n },\r\n};\r\n\r\nimport { initialFiles } from './initialFiles';\r\n\r\nexport function VsCodeIDE() {\r\n return (\r\n <SandpackProvider\r\n template=\"react\"\r\n theme={myCodeSandboxTheme}\r\n files={initialFiles}\r\n customSetup={{\r\n dependencies: {\r\n \"react\": \"^18.0.0\",\r\n \"react-dom\": \"^18.0.0\",\r\n \"react-scripts\": \"^5.0.0\"\r\n }\r\n }}\r\n options={{\r\n classes: {\r\n 'sp-wrapper': 'h-full',\r\n 'sp-layout': 'h-full rounded-none border-none bg-transparent',\r\n },\r\n }}\r\n >\r\n <IdeLayout />\r\n </SandpackProvider>\r\n );\r\n}\r\n"
589
+ }
590
+ ]
591
+ }
592
+ }
593
+ }