feat: initial ui redesign from demo
No files matched your search
@@ -1,15 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"style": "radix-lyra",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/index.css",
|
||||
"baseColor": "zinc",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"iconLibrary": "lucide",
|
||||
"rtl": false,
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
@@ -17,5 +19,7 @@
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle",
|
||||
"registries": {}
|
||||
}
|
||||
@@ -77,6 +77,7 @@
|
||||
"@electron/notarize": "^3.1.1",
|
||||
"@electron/rebuild": "^4.0.4",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.12",
|
||||
@@ -140,6 +141,7 @@
|
||||
"lucide-react": "^1.11.0",
|
||||
"next-themes": "^0.4.6",
|
||||
"prettier": "3.8.3",
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19.2.5",
|
||||
"react-cytoscapejs": "^2.0.0",
|
||||
"react-dom": "^19.2.5",
|
||||
@@ -157,12 +159,16 @@
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.8.1",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shadcn": "^4.7.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss": "^4.2.4",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "~6.0.3",
|
||||
"typescript-eslint": "^8.59.0",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-svgr": "^5.2.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"lint-staged": {
|
||||
|
||||
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 39 KiB |
@@ -1,29 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="1024" height="1024">
|
||||
<path d="M0 0 C2.73400004 -0.002639 5.46799221 -0.00826382 8.20198786 -0.01373065 C14.12976744 -0.02399057 20.05751439 -0.02688145 25.98530197 -0.02481842 C34.80090688 -0.02175792 43.61647591 -0.03011562 52.43207382 -0.04056767 C69.09746094 -0.05987554 85.76283572 -0.06553472 102.42823271 -0.06668179 C115.30380751 -0.0676022 128.17937749 -0.07172576 141.05495071 -0.07808304 C144.76625176 -0.07987824 148.47755281 -0.08165787 152.18885386 -0.08343279 C153.11160075 -0.08387489 154.03434764 -0.08431699 154.98505655 -0.08477248 C180.92924305 -0.09707542 206.87342788 -0.10497418 232.81761742 -0.10408592 C233.79511593 -0.10405476 234.77261444 -0.10402361 235.77973412 -0.10399151 C240.73366138 -0.1038253 245.68758864 -0.10362722 250.6415159 -0.10342209 C251.6253128 -0.1033822 252.60910971 -0.10334231 253.62271865 -0.10330121 C255.61219571 -0.10322034 257.60167278 -0.10313674 259.59114985 -0.10305043 C290.52844692 -0.10182554 321.46570011 -0.11682888 352.40298462 -0.14419556 C387.19411554 -0.17495891 421.98521764 -0.19212546 456.77636302 -0.19026911 C460.49080761 -0.19011986 464.20525221 -0.19002244 467.91969681 -0.18996048 C469.29149007 -0.18992552 469.29149007 -0.18992552 470.69099632 -0.18988985 C485.3682461 -0.18982846 500.04545761 -0.20256613 514.72269603 -0.21982501 C529.49125025 -0.23683126 544.25974885 -0.23871645 559.02830748 -0.22560637 C567.78591009 -0.2182501 576.54334301 -0.2217749 585.30092741 -0.24149105 C591.16987765 -0.25357988 597.03870633 -0.25080234 602.90765122 -0.23655005 C606.23912509 -0.22886096 609.57001068 -0.22929279 612.90150099 -0.24346475 C669.26549491 -0.4671585 719.76924989 19.84001892 760.15640259 59.26742554 C772.42844891 71.50006259 782.52075649 85.25415658 791.15640259 100.26742554 C791.50912231 100.87183472 791.86184204 101.4762439 792.22525024 102.09896851 C811.88962254 136.11043906 817.64194919 173.04055363 817.54408264 211.76298523 C817.54660703 214.48240629 817.55362534 217.2018119 817.56040245 219.92122525 C817.57267741 225.79546941 817.57402194 231.66963359 817.56785202 237.54388618 C817.5587013 246.27976136 817.56684044 255.01556352 817.57814326 263.75143313 C817.59774407 279.36124531 817.60017028 294.97102417 817.59494806 310.58084573 C817.59037982 324.24363644 817.59100383 337.90641403 817.59678841 351.56920433 C817.59754529 353.40633989 817.59830127 355.24347545 817.59905635 357.08061102 C817.60020162 359.84946233 817.60135151 362.61831363 817.60251559 365.38716494 C817.61321044 391.1073786 817.61795627 416.82758792 817.61004543 442.54780293 C817.60959967 444.00013607 817.60959967 444.00013607 817.60914491 445.48180929 C817.60669631 453.34299581 817.60408455 461.20418229 817.60146077 469.06536876 C817.5913601 499.72185315 817.59971449 530.37828276 817.62295532 561.03475952 C817.64906362 595.52892829 817.6618664 630.02306408 817.65476757 664.51724309 C817.65405158 668.19956187 817.65337718 671.88188065 817.65272713 675.56419945 C817.65255732 676.47078703 817.65238751 677.37737461 817.65221255 678.31143455 C817.64991573 691.95846782 817.65785694 705.60547034 817.6710186 719.25249672 C817.685971 734.78338018 817.68671805 750.31419187 817.66894769 765.84507386 C817.65942821 774.52050334 817.66072981 783.19576513 817.67818238 791.8711832 C817.68875186 797.68511687 817.68446055 803.49892416 817.66870234 809.31284558 C817.66015583 812.61276916 817.65970357 815.91210367 817.67297166 819.21204952 C817.88436667 875.86359058 797.79480503 926.66414093 758.15640259 967.26742554 C745.93892953 979.52425902 732.17979891 989.68607442 717.15640259 998.26742554 C716.4925354 998.65253296 715.82866821 999.03764038 715.14468384 999.43441772 C695.23288262 1010.86828901 673.71271691 1018.1589933 651.15640259 1022.26742554 C650.28498416 1022.44004335 649.41356574 1022.61266116 648.51574069 1022.79050982 C635.90814607 1024.96518969 623.18430579 1024.55138623 610.43305969 1024.53485107 C607.6999227 1024.53748912 604.9667936 1024.54311399 602.233661 1024.54858172 C596.30466868 1024.55884731 590.375709 1024.56173152 584.44670868 1024.55966949 C575.62928442 1024.55661047 566.81189608 1024.5649635 557.99447882 1024.57541875 C541.32568596 1024.59473226 524.65690543 1024.60038602 507.9881027 1024.60153286 C495.10964357 1024.60245312 482.23118926 1024.60657588 469.3527317 1024.61293411 C465.64116775 1024.61472931 461.92960378 1024.61650893 458.21803981 1024.61828387 C457.29523567 1024.61872596 456.37243152 1024.61916806 455.42166361 1024.61962355 C429.47354925 1024.6319276 403.52543655 1024.63982514 377.57731915 1024.638937 C376.59955462 1024.63890584 375.62179009 1024.63887468 374.61439634 1024.63884258 C369.65907825 1024.63867637 364.70376016 1024.63847829 359.74844208 1024.63827316 C358.76436283 1024.63823327 357.78028358 1024.63819338 356.76638375 1024.63815228 C354.77633502 1024.63807141 352.78628628 1024.63798781 350.79623754 1024.6379015 C319.85239029 1024.63667671 288.9085869 1024.6516778 257.9647522 1024.67904663 C223.16886751 1024.70981007 188.37301164 1024.7269765 153.5771125 1024.72512019 CLine truncated
|
||||
<path d="M0 0 C1.40423182 0.00051035 1.40423182 0.00051035 2.83683196 0.00103101 C3.82595598 -0.0025196 4.81508001 -0.00607022 5.83417749 -0.00972843 C9.19305859 -0.02050902 12.55190967 -0.0240525 15.91080561 -0.02766219 C18.32564927 -0.0338193 20.74049189 -0.04039487 23.15533337 -0.04735598 C29.12496578 -0.0633399 35.09459078 -0.07451946 41.06423627 -0.08363459 C46.02947164 -0.09137043 50.99469774 -0.10144125 55.95992556 -0.11302939 C57.03288597 -0.1155315 57.03288597 -0.1155315 58.12752234 -0.11808417 C59.58627278 -0.12149296 61.04502321 -0.12490448 62.50377363 -0.12831869 C82.24770504 -0.17404715 101.99165615 -0.20448455 121.73561946 -0.23248091 C122.54409117 -0.2336359 123.35256289 -0.23479089 124.18553374 -0.23598088 C156.18819906 -0.28168636 188.19087249 -0.32132586 220.19355964 -0.34800582 C228.12660644 -0.35462936 236.05965303 -0.3615028 243.99269953 -0.36846724 C245.30758824 -0.36961202 245.30758824 -0.36961202 246.64904036 -0.37077993 C275.04965913 -0.39570526 303.45013113 -0.44916217 331.85067361 -0.51848551 C361.0453603 -0.58961736 390.23996808 -0.63467261 419.4347424 -0.64598659 C423.56094367 -0.64759145 427.68714476 -0.64947904 431.81334582 -0.65153113 C432.62541303 -0.65192401 433.43748025 -0.6523169 434.27415556 -0.65272169 C447.33054215 -0.65977305 460.38672234 -0.69395932 473.44302996 -0.73819214 C486.57766984 -0.78206641 499.71208526 -0.79644882 512.8467916 -0.78109888 C520.64442365 -0.77285756 528.4412709 -0.78672339 536.23877507 -0.83281821 C542.07168701 -0.86481521 547.90404453 -0.85466238 553.73700174 -0.8285531 C556.08920992 -0.82449162 558.44148962 -0.83384796 560.79357401 -0.8583189 C579.09200025 -1.03720424 592.84786818 2.26405378 606.57546321 15.14642534 C611.04162409 19.56596659 611.04162409 19.56596659 612.57546321 22.14642534 C612.57546321 23.46642534 612.57546321 24.78642534 612.57546321 26.14642534 C613.23546321 26.14642534 613.89546321 26.14642534 614.57546321 26.14642534 C619.06125557 33.80380806 619.31056144 41.10884428 619.26296321 49.83392534 C619.26159358 50.50217332 619.26022395 51.17042131 619.25881281 51.85891923 C619.18815119 62.55403817 618.32559764 70.52408381 611.57546321 79.14642534 C610.89226008 80.02943315 610.20905696 80.91244096 609.50515071 81.82220659 C604.63307267 87.62979771 599.46895511 91.97760101 592.57546321 95.20892534 C591.05952571 95.93789018 591.05952571 95.93789018 589.51296321 96.68158159 C576.2689793 101.93707205 563.08325643 101.55732802 549.06838313 101.48700151 C546.66384639 101.48608132 544.25930885 101.486485 541.85477248 101.48809728 C535.44511442 101.48877152 529.03562612 101.47144822 522.62600794 101.45026597 C516.60024343 101.43311673 510.57451914 101.43237312 504.54872981 101.43023881 C486.953452 101.41452862 469.35820543 101.3726449 451.76296321 101.33392534 C412.43108821 101.27205034 373.09921321 101.21017534 332.57546321 101.14642534 C332.57546321 234.13642534 332.57546321 367.12642534 332.57546321 504.14642534 C311.12546321 504.14642534 289.67546321 504.14642534 267.57546321 504.14642534 C267.90546321 537.47642534 268.23546321 570.80642534 268.57546321 605.14642534 C300.25546321 605.64142534 300.25546321 605.64142534 332.57546321 606.14642534 C332.67858821 613.61267534 332.78171321 621.07892534 332.88796321 628.77142534 C332.95624323 632.26083696 332.95624323 632.26083696 333.02590266 635.82074174 C333.20342124 651.39816119 331.30646416 663.44206298 321.57546321 676.14642534 C320.89483821 677.11580034 320.21421321 678.08517534 319.51296321 679.08392534 C317.34689201 681.38974306 315.57653653 682.22943071 312.57546321 683.14642534 C312.57546321 683.80642534 312.57546321 684.46642534 312.57546321 685.14642534 C311.91546321 685.14642534 311.25546321 685.14642534 310.57546321 685.14642534 C310.57546321 685.80642534 310.57546321 686.46642534 310.57546321 687.14642534 C309.20261164 687.67042924 309.20261164 687.67042924 307.80202571 688.20501909 C306.61351008 688.66005815 305.42499446 689.11509721 304.20046321 689.58392534 C303.01710383 690.03638627 301.83374446 690.48884721 300.61452571 690.95501909 C297.71510128 692.03914445 297.71510128 692.03914445 295.57546321 693.14642534 C285.24933309 693.80324905 276.05684124 693.82446208 266.57546321 689.14642534 C266.57546321 688.48642534 266.57546321 687.82642534 266.57546321 687.14642534 C265.51005305 687.04201127 265.51005305 687.04201127 264.42311946 686.93548784 C257.70869754 685.07497587 251.36435061 678.07379091 247.88796321 672.14642534 C246.57546321 669.14642534 246.57546321 669.14642534 246.57546321 667.14642534 C245.91546321 667.14642534 245.25546321 667.14642534 244.57546321 667.14642534 C241.81043885 659.58869209 241.42919517 652.37018368 241.45520869 644.4210988 C241.45392705 642.97880046 241.45392705 642.97880046 241.45261952 641.50736478 C241.45097643 638.295317 241.45646692 635.0833218 241.46193781 631.87127885 C241.46221996 629.56485695 241.46206141 627.25843496 241.46149531 624.95201311 C241.46120842 618.69642091 241.46708767 612.44084789 241.47406927 606.18526015 C241.48Line truncated
|
||||
<path d="M0 0 C1.65 0 3.3 0 5 0 C5 0.66 5 1.32 5 2 C5.9590625 2.1546875 5.9590625 2.1546875 6.9375 2.3125 C12.53190183 3.56838613 17.91521936 5.33654348 23 8 C23 8.66 23 9.32 23 10 C23.85851562 10.12246094 24.71703125 10.24492187 25.6015625 10.37109375 C29.00686898 11.00127116 32.10342101 11.93562279 35.375 13.0625 C36.43460937 13.42472656 37.49421875 13.78695313 38.5859375 14.16015625 C39.38257813 14.43730469 40.17921875 14.71445312 41 15 C41 15.66 41 16.32 41 17 C41.61488281 17.03738281 42.22976563 17.07476562 42.86328125 17.11328125 C43.67152344 17.17902344 44.47976563 17.24476563 45.3125 17.3125 C46.11300781 17.37050781 46.91351563 17.42851562 47.73828125 17.48828125 C50.43593563 18.09863138 51.10042243 19.06165554 53 21 C55.49018012 21.99988926 55.49018012 21.99988926 58.1875 22.6875 C59.08855469 22.93886719 59.98960937 23.19023437 60.91796875 23.44921875 C61.60503906 23.63097656 62.29210937 23.81273437 63 24 C63 24.66 63 25.32 63 26 C64.65 26 66.3 26 68 26 C68 26.66 68 27.32 68 28 C69.216875 28.12375 70.43375 28.2475 71.6875 28.375 C75.06479059 28.78891306 77.89756695 29.54573451 81 31 C81 31.66 81 32.32 81 33 C81.928125 33.0825 82.85625 33.165 83.8125 33.25 C89.12534498 33.98242925 94.02778798 36.06686151 99 38 C99 38.99 99 39.98 99 41 C101.97 41.33 104.94 41.66 108 42 C108 42.66 108 43.32 108 44 C109.216875 44.103125 110.43375 44.20625 111.6875 44.3125 C115.50184935 44.63574995 117.7522667 46.02730273 121 48 C123.14968745 48.8169898 125.31856158 49.58451998 127.5 50.3125 C133.53502516 52.40422076 139.22759376 54.89590242 145 57.640625 C148.92721265 59.42014323 152.8789933 60.74274372 157 62 C157 62.66 157 63.32 157 64 C158.010625 64.0825 159.02125 64.165 160.0625 64.25 C163.53239519 64.7397864 165.87249712 66.03039413 168.8515625 67.890625 C172.65571858 69.85495287 176.78979197 70.8552536 180.88671875 72.05078125 C184.4123549 73.12572427 187.6626102 74.44772568 191 76 C191 76.66 191 77.32 191 78 C191.71542969 77.95875 192.43085938 77.9175 193.16796875 77.875 C196.30819586 78.01360313 198.43211348 78.77103509 201.3125 80 C202.19550781 80.37125 203.07851563 80.7425 203.98828125 81.125 C204.65214844 81.41375 205.31601562 81.7025 206 82 C206 82.66 206 83.32 206 84 C208.64 84.33 211.28 84.66 214 85 C214 85.66 214 86.32 214 87 C215.2375 87.226875 216.475 87.45375 217.75 87.6875 C223.81734976 89.02193294 229.325946 91.5275072 235 94 C236.66414626 94.67293346 238.32999182 95.3417486 240 96 C240 96.66 240 97.32 240 98 C241.32 98.33 242.64 98.66 244 99 C244 99.66 244 100.32 244 101 C245.5159375 100.9071875 245.5159375 100.9071875 247.0625 100.8125 C251.39947898 101.01902281 254.07744088 102.21701858 258 104 C259.98821313 104.39764263 261.98732362 104.75455166 264 105 C264 105.66 264 106.32 264 107 C264.639375 107.0825 265.27875 107.165 265.9375 107.25 C268.50012926 107.87758268 270.44458009 108.59426495 272.8125 109.6875 C279.70969554 112.86630951 286.87764831 115.37827545 294 118 C294 118.66 294 119.32 294 120 C294.886875 120.12375 295.77375 120.2475 296.6875 120.375 C303.69767442 121.69767442 303.69767442 121.69767442 306 124 C306.09529612 127.01350238 306.12551904 130.00404106 306.11352539 133.01782227 C306.11367142 133.96406464 306.11381744 134.91030701 306.1139679 135.88522339 C306.11326836 139.02897392 306.10547409 142.17266601 306.09765625 145.31640625 C306.09579221 147.48974068 306.09436824 149.66307552 306.09336853 151.83641052 C306.08954221 157.56901195 306.07971097 163.30158823 306.06866455 169.03417969 C306.05845287 174.87890279 306.05387143 180.72362993 306.04882812 186.56835938 C306.0380888 198.045584 306.02101102 209.52278991 306 221 C303.09399739 222.25664978 301.20395416 223 298 223 C298 223.66 298 224.32 298 225 C295.03 225.66 292.06 226.32 289 227 C289 227.66 289 228.32 289 229 C285.26028463 230.44339892 281.62167556 231.83262581 277.75 232.875 C273.2496418 234.22510746 269.05993179 236.07094172 264.76953125 237.9765625 C262 239 262 239 258 239 C258 239.66 258 240.32 258 241 C257.1646875 241.27263672 257.1646875 241.27263672 256.3125 241.55078125 C248.58964059 244.09319952 248.58964059 244.09319952 241 247 C240.67 247.66 240.34 248.32 240 249 C238.11105677 249.74208484 236.21777695 250.47493307 234.3046875 251.15234375 C232.02624136 251.99034852 229.8353074 252.93460251 227.625 253.9375 C224.32911372 255.41693595 221.02373953 256.86357447 217.6875 258.25 C216.88699219 258.58515625 216.08648437 258.9203125 215.26171875 259.265625 C213 260 213 260 209 260 C209 260.66 209 261.32 209 262 C207.88625 262.4125 206.7725 262.825 205.625 263.25 C202.39899591 264.45832662 199.19572477 265.71379498 196 267 C193.01202799 268.20247654 190.01974967 269.37977028 187 270.5 C178.56870095 273.62992779 170.2626406 277.10032586 161.9375 280.5 C161.00873047 280.87769531 160.07996094 281.25539063 159.12304688 281.64453125 C154.04116178 283.72008011 148.99851079 285.85665669 143.99926758 288.12451172 C140.71811867 289.56134161 137.36265982 290.76702473 134 292 C133.113125 292.391875 132.22625 292.78375 131.3125 2Line truncated
|
||||
<path d="M0 0 C1.32 0 2.64 0 4 0 C4 0.66 4 1.32 4 2 C4.59425781 2.17015625 5.18851562 2.3403125 5.80078125 2.515625 C14.03088043 4.92193705 22.0506053 7.78884415 30 11 C30 11.66 30 12.32 30 13 C30.763125 13.0825 31.52625 13.165 32.3125 13.25 C41.03712852 15.02450072 49.04834601 18.83619356 57.0546875 22.6328125 C61.74551929 24.81025431 65.8589058 26.28263802 71 27 C71 27.66 71 28.32 71 29 C71.5775 29.144375 72.155 29.28875 72.75 29.4375 C75.875 30.21875 75.875 30.21875 79 31 C79 31.66 79 32.32 79 33 C79.845625 33.0825 80.69125 33.165 81.5625 33.25 C84.94816006 33.98868947 87.1679512 35.23316228 90.08984375 37.03125 C92.20090329 38.1018896 94.05027523 38.51702804 96.375 38.9375 C100.78658503 39.76888529 104.00591761 41.5746091 107.8359375 43.8515625 C110.36891446 45.19577771 112.89143377 45.70830903 115.68359375 46.29296875 C118.75695192 47.23104266 121.40817621 48.6884795 124.25 50.1875 C130.92469726 53.70757728 137.81399628 56.18579238 144.98339844 58.5144043 C148.81489863 59.78075405 152.3200305 61.06832861 155.7890625 63.1484375 C158.76224988 64.86290321 160.59810208 65.53188262 163.9375 65.9375 C168.43366109 66.69933062 171.342726 68.21955324 175.07421875 70.76953125 C178.8471118 73.18020328 183.08817864 74.39052675 187.328125 75.7421875 C188.49601562 76.11730469 188.49601562 76.11730469 189.6875 76.5 C190.39261719 76.7165625 191.09773437 76.933125 191.82421875 77.15625 C194 78 194 78 196.37109375 79.43359375 C202.28631102 82.95811399 209.20600207 85.36493689 215.875 87.0625 C219 88 219 88 222.3125 89.9375 C226.84415671 92.6470648 226.84415671 92.6470648 236 95 C236 112.16 236 129.32 236 147 C228.13477975 151.49441157 228.13477975 151.49441157 224.3125 152.1875 C223.58675781 152.32542969 222.86101562 152.46335937 222.11328125 152.60546875 C220.67602299 152.87379238 219.23564618 153.12581399 217.79296875 153.36328125 C217.20128906 153.57339844 216.60960937 153.78351563 216 154 C215.67 154.99 215.34 155.98 215 157 C212.41796875 158.09765625 212.41796875 158.09765625 209.1875 159.0625 C205.59909404 160.15082288 202.32570343 161.27582837 199 163 C197.38773015 163.47024537 195.77452619 163.93757854 194.15625 164.38671875 C185.85630305 166.74739207 178.06683002 169.72029401 170.33984375 173.5378418 C167.30328348 175.0185775 164.37352911 176.23370456 161.11328125 177.0859375 C159.57220703 177.53839844 159.57220703 177.53839844 158 178 C157.67 178.66 157.34 179.32 157 180 C154.52984421 181.04037849 152.08512956 181.98065187 149.5625 182.875 C148.08819029 183.41049192 146.61423048 183.94694816 145.140625 184.484375 C144.42455078 184.74412109 143.70847656 185.00386719 142.97070312 185.27148438 C140.55887129 186.16307335 138.18543043 187.13496942 135.8125 188.125 C131.00122041 190.08696003 126.06950487 191.5404007 121.078125 192.97265625 C117.91500582 193.94520247 117.91500582 193.94520247 114.796875 195.62109375 C112 197 112 197 109 197 C108.67 197.99 108.34 198.98 108 200 C106.21484375 200.625 106.21484375 200.625 103.9375 201 C99.34708416 201.88135984 95.30113259 203.15665746 91 205 C90.67 205.66 90.34 206.32 90 207 C88.12988281 207.9296875 88.12988281 207.9296875 85.703125 208.875 C84.8265625 209.22046875 83.95 209.5659375 83.046875 209.921875 C82.12390625 210.27765625 81.2009375 210.6334375 80.25 211 C79.3425 211.35578125 78.435 211.7115625 77.5 212.078125 C75.79712731 212.74533243 74.09313765 213.40970098 72.38769531 214.0703125 C70.7198096 214.71973058 69.05809239 215.38511668 67.40136719 216.0625 C65 217 65 217 61.75 217.875 C57.96339034 219.0109829 54.57204208 220.49171411 51.015625 222.1953125 C49 223 49 223 46 223 C46 223.66 46 224.32 46 225 C25.78125 232.90625 25.78125 232.90625 19 234 C19 234.66 19 235.32 19 236 C17.15047952 236.82302143 15.29511436 237.63291366 13.4375 238.4375 C12.40496094 238.88996094 11.37242187 239.34242188 10.30859375 239.80859375 C6.84845419 241.0545708 3.63685493 241.5300587 0 242 C-0.06943556 234.21156603 -0.12277821 226.42320423 -0.15543652 218.63453865 C-0.17110901 215.0181296 -0.19237553 211.40193192 -0.22631836 207.78564453 C-0.25885631 204.29730012 -0.27684108 200.80915932 -0.28463173 197.3206749 C-0.29289269 195.33856231 -0.31632087 193.35653191 -0.33988953 191.37454224 C-0.34011612 190.17344818 -0.34034271 188.97235413 -0.34057617 187.73486328 C-0.34723801 186.67341095 -0.35389984 185.61195862 -0.36076355 184.51834106 C-0.24171158 183.68728851 -0.12265961 182.85623596 0 182 C3 180 3 180 6 180 C6 179.34 6 178.68 6 178 C7.32451172 177.65388672 7.32451172 177.65388672 8.67578125 177.30078125 C15.48290775 175.45093914 21.89314175 173.34586589 28.2734375 170.296875 C35.35610061 166.92804383 42.45740949 164.13469543 50 162 C52.31944799 160.97928402 54.60816608 159.91361012 56.88671875 158.8046875 C59 158 59 158 63 158 C63 157.34 63 156.68 63 156 C63.60328125 155.86722656 64.2065625 155.73445312 64.828125 155.59765625 C66.02695312 155.33275391 66.02695312 155.33275391 67.25 155.0625 C68.03890625 154.88847656 68.8278125 154.71445312 69.640625 154.53515625 C70.80851562 154.270Line truncated
|
||||
<path d="M0 0 C12.01719911 0.01365142 24.03440239 0.01874096 36.05160737 0.02450979 C54.80253918 0.03372043 73.55344276 0.05369526 92.30437303 0.07202661 C92.30437303 34.06202661 92.30437303 68.05202661 92.30437303 103.07202661 C51.07884714 103.11232964 51.07884714 103.11232964 33.31999803 103.12085474 C21.23047528 103.12670618 9.14095977 103.13347291 -2.94855666 103.14746606 C-74.27785064 103.22842372 -74.27785064 103.22842372 -103.14972854 102.84497583 C-103.88451289 102.83521793 -104.61929724 102.82546003 -105.37634778 102.81540644 C-119.81699678 102.61855942 -134.2560097 102.33467791 -148.69562697 102.07202661 C-148.69562697 68.74202661 -148.69562697 35.41202661 -148.69562697 1.07202661 C-114.45674119 0.44923393 -114.45674119 0.44923393 -103.14972854 0.29907739 C-102.35742103 0.28855474 -101.56511353 0.27803209 -100.7487967 0.26719058 C-67.16855577 -0.16690708 -33.58227694 -0.03887021 0 0 Z " fill="#14191F" transform="translate(641.6956269741058,696.9279733896255)"/>
|
||||
<path d="M0 0 C56.43 0 112.86 0 171 0 C171 10.56 171 21.12 171 32 C114.57 32 58.14 32 0 32 C0 21.44 0 10.88 0 0 Z " fill="#727272" transform="translate(528,733)"/>
|
||||
<path d="M0 0 C79.86 0 159.72 0 242 0 C242 0.33 242 0.66 242 1 C162.8 1 83.6 1 2 1 C2 133.99 2 266.98 2 404 C-19.45 404 -40.9 404 -63 404 C-62.67 437.33 -62.34 470.66 -62 505 C-30.32 505.495 -30.32 505.495 2 506 C2.04125 513.6725 2.0825 521.345 2.125 529.25 C2.14320801 531.66731445 2.16141602 534.08462891 2.18017578 536.57519531 C2.18601743 538.49283628 2.19111377 540.41047973 2.1953125 542.328125 C2.20554443 543.32247559 2.21577637 544.31682617 2.22631836 545.34130859 C2.22668091 546.77192627 2.22668091 546.77192627 2.22705078 548.23144531 C2.231492 549.05751282 2.23593323 549.88358032 2.24050903 550.73468018 C2 553 2 553 0 557 C0.33 540.5 0.66 524 1 507 C-20.45 507 -41.9 507 -64 507 C-64 472.35 -64 437.7 -64 402 C-42.88 402 -21.76 402 0 402 C0 269.34 0 136.68 0 0 Z " fill="#A0A0A1" transform="translate(556,294)"/>
|
||||
<path d="M0 0 C57.42 0 114.84 0 174 0 C174 11.55 174 23.1 174 35 C151.46087147 35.02254502 128.92175202 35.040978 106.38261509 35.05181217 C95.91807719 35.05697564 85.45354677 35.06401929 74.98901367 35.07543945 C65.87160565 35.08538476 56.75420304 35.09185306 47.63678974 35.09408849 C42.80606124 35.09539586 37.97534825 35.09848336 33.14462471 35.10573006 C28.60326313 35.11248785 24.06192353 35.11461746 19.5205574 35.11310768 C17.84875431 35.11338412 16.17695052 35.11539066 14.50515175 35.11920547 C12.23274147 35.12413898 9.96041554 35.12299958 7.68800354 35.12025452 C6.41316384 35.12117631 5.13832415 35.1220981 3.82485294 35.12304783 C1 35 1 35 0 34 C0 22.78 0 11.56 0 0 Z M2 1 C2 11.56 2 22.12 2 33 C58.43 33 114.86 33 173 33 C173 22.44 173 11.88 173 1 C116.57 1 60.14 1 2 1 Z " fill="#3D4044" transform="translate(526,732)"/>
|
||||
<path d="M0 0 C80.19 0 160.38 0 243 0 C243.33 55.77 243.66 111.54 244 169 C245.98 169.99 247.96 170.98 250 172 C250 172.33 250 172.66 250 173 C248.35 173 246.7 173 245 173 C245 172.34 245 171.68 245 171 C244.01 170.67 243.02 170.34 242 170 C242 114.23 242 58.46 242 1 C162.14 1 82.28 1 0 1 C0 0.67 0 0.34 0 0 Z " fill="#4B4B4B" transform="translate(224,295)"/>
|
||||
<path d="M0 0 C1.485 0.99 1.485 0.99 3 2 C3.25616216 5.05290084 3.31849805 7.89702233 3.2487793 10.94775391 C3.23946381 11.87463516 3.23014832 12.80151642 3.22055054 13.75648499 C3.1860853 16.83020963 3.13020737 19.90316065 3.07421875 22.9765625 C3.04619629 25.10361061 3.01950476 27.23067664 2.99409485 29.35775757 C2.9235311 34.96399488 2.83482971 40.56980591 2.74243164 46.17572021 C2.63537976 52.89774556 2.55071052 59.62008508 2.46231842 66.34237671 C2.32541953 76.56192943 2.15997827 86.78066503 2 97 C21.8 97 41.6 97 62 97 C62 97.33 62 97.66 62 98 C42.2 98 22.4 98 2 98 C2 98.66 2 99.32 2 100 C1.34 100 0.68 100 0 100 C0 67 0 34 0 0 Z " fill="#0A0E15" transform="translate(494,699)"/>
|
||||
<path d="M0 0 C0.33 0 0.66 0 1 0 C1.33 55.77 1.66 111.54 2 169 C3.98 169.99 5.96 170.98 8 172 C8 172.33 8 172.66 8 173 C6.35 173 4.7 173 3 173 C3 172.34 3 171.68 3 171 C2.01 170.67 1.02 170.34 0 170 C0 113.9 0 57.8 0 0 Z " fill="#717171" transform="translate(466,295)"/>
|
||||
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C1.67 20.14 1.34 39.28 1 59 C3.64 58.01 6.28 57.02 9 56 C10.32 56.33 11.64 56.66 13 57 C11.23141309 57.67260351 9.46003984 58.33788395 7.6875 59 C6.70136719 59.37125 5.71523438 59.7425 4.69921875 60.125 C2 61 2 61 -1 61 C-1.02312792 53.19424805 -1.04091738 45.38850405 -1.05181217 37.58272648 C-1.05704002 33.9585404 -1.06413587 30.3343777 -1.07543945 26.71020508 C-1.08627184 23.21540025 -1.09227758 19.72061796 -1.09487724 16.22579765 C-1.09763551 14.23760611 -1.10544688 12.24942369 -1.11329651 10.26124573 C-1.11337204 9.06007614 -1.11344757 7.85890656 -1.11352539 6.62133789 C-1.115746 5.55766495 -1.11796661 4.493992 -1.12025452 3.39808655 C-1 1 -1 1 0 0 Z " fill="#049652" transform="translate(261,599)"/>
|
||||
<path d="M0 0 C1.32 0 2.64 0 4 0 C4 0.66 4 1.32 4 2 C4.66 2.33 5.32 2.66 6 3 C5.01 3.33 4.02 3.66 3 4 C2.67 23.14 2.34 42.28 2 62 C1.34 62 0.68 62 0 62 C0 41.54 0 21.08 0 0 Z " fill="#049853" transform="translate(260,418)"/>
|
||||
<path d="M0 0 C-1.65 0.33 -3.3 0.66 -5 1 C-5 1.66 -5 2.32 -5 3 C-6.65 3 -8.3 3 -10 3 C-10 3.66 -10 4.32 -10 5 C-10.9178125 5.32613281 -11.835625 5.65226563 -12.78125 5.98828125 C-14.62203125 6.64376953 -14.62203125 6.64376953 -16.5 7.3125 C-17.1384082 7.5396167 -17.77681641 7.7667334 -18.43457031 8.00073242 C-21.88861021 9.23312895 -25.32784181 10.49433536 -28.75 11.8125 C-32 13 -32 13 -34 13 C-34 12.34 -34 11.68 -34 11 C-32.58603098 10.49242138 -31.16868271 9.99425075 -29.75 9.5 C-28.96109375 9.2215625 -28.1721875 8.943125 -27.359375 8.65625 C-25 8 -25 8 -21 8 C-21 7.34 -21 6.68 -21 6 C-17.49223216 4.39644899 -14.85645121 3.77963136 -11 4 C-11 3.34 -11 2.68 -11 2 C-9.54494642 1.47007308 -8.08581873 0.9513211 -6.625 0.4375 C-5.81289063 0.14746094 -5.00078125 -0.14257812 -4.1640625 -0.44140625 C-2 -1 -2 -1 0 0 Z " fill="#25272A" transform="translate(309,544)"/>
|
||||
<path d="M0 0 C0 0.66 0 1.32 0 2 C-0.89460938 2.31453125 -1.78921875 2.6290625 -2.7109375 2.953125 C-8.35592004 4.95113588 -13.96926436 6.94975521 -19.5 9.25 C-22.98649532 10.67681657 -26.22664243 11.60280447 -30 12 C-30 11.34 -30 10.68 -30 10 C-28.60974609 9.58427734 -28.60974609 9.58427734 -27.19140625 9.16015625 C-25.99386719 8.79792969 -24.79632813 8.43570313 -23.5625 8.0625 C-21.77005859 7.52302734 -21.77005859 7.52302734 -19.94140625 6.97265625 C-17.14132455 6.29110858 -17.14132455 6.29110858 -16 5 C-14.33382885 4.95936168 -12.66611905 4.957279 -11 5 C-11 4.34 -11 3.68 -11 3 C-9.54293274 2.49632243 -8.08429904 1.99717444 -6.625 1.5 C-5.81289062 1.2215625 -5.00078125 0.943125 -4.1640625 0.65625 C-2 0 -2 0 0 0 Z " fill="#25272A" transform="translate(257,563)"/>
|
||||
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C-2.99893918 3.7266941 -7.31292741 5.43542946 -12.89453125 6.4921875 C-15.28068151 6.98141752 -15.28068151 6.98141752 -18.3125 8.6875 C-21.33484121 10.1635271 -21.94813661 9.96956854 -25 9 C-17.71998049 6.15325357 -10.44504388 3.38499241 -3 1 C-2.01 0.67 -1.02 0.34 0 0 Z " fill="#087F48" transform="translate(394,606)"/>
|
||||
<path d="M0 0 C0.66 0.33 1.32 0.66 2 1 C1.360625 1.28875 0.72125 1.5775 0.0625 1.875 C-2.15206079 2.82823451 -2.15206079 2.82823451 -3 5 C-8.04706509 7.17545909 -12.5487185 8.43215818 -18 9 C-16.44464191 5.88928381 -13.88562714 5.66683615 -10.6875 4.5 C-6.10637354 2.84526208 -6.10637354 2.84526208 -1.70703125 0.78125 C-1.14371094 0.5234375 -0.58039063 0.265625 0 0 Z " fill="#0B5C3A" transform="translate(448,583)"/>
|
||||
<path d="M0 0 C0 0.66 0 1.32 0 2 C-1.65 2 -3.3 2 -5 2 C-5 2.66 -5 3.32 -5 4 C-6.4117783 4.53394179 -7.82970244 5.05165104 -9.25 5.5625 C-10.03890625 5.85253906 -10.8278125 6.14257812 -11.640625 6.44140625 C-14.19103452 7.04522837 -15.55322229 6.8473932 -18 6 C-12.33627962 2.96586408 -6.59531296 0 0 0 Z " fill="#087946" transform="translate(380,553)"/>
|
||||
<path d="M0 0 C1.98 0 3.96 0 6 0 C4.13497412 2.62799102 2.69589209 3.86365751 -0.4921875 4.48828125 C-2.9964272 4.72593573 -5.48653979 4.9040664 -8 5 C-5.08235 3.0549 -3.37462715 2.54723683 0 2 C0 1.34 0 0.68 0 0 Z " fill="#07834A" transform="translate(388,548)"/>
|
||||
<path d="M0 0 C2.0625 0.4375 2.0625 0.4375 4 1 C1.35261084 2.4606285 0.10551666 3 -3 3 C-3 3.66 -3 4.32 -3 5 C-6.4019487 7.09350689 -9.05272412 7.17942163 -13 7 C-9.84906684 4.24293349 -6.60395471 2.99320144 -2.70703125 1.6484375 C-0.99486039 1.13150184 -0.99486039 1.13150184 0 0 Z " fill="#0B623C" transform="translate(319,635)"/>
|
||||
<path d="M0 0 C-2.58411731 1.72274487 -4.54902656 2.68895522 -7.4375 3.6875 C-8.19933594 3.95949219 -8.96117187 4.23148438 -9.74609375 4.51171875 C-12.21829721 5.04729142 -13.62983284 4.80445309 -16 4 C-14.08959948 3.1319092 -12.17139263 2.28098217 -10.25 1.4375 C-9.18265625 0.96183594 -8.1153125 0.48617187 -7.015625 -0.00390625 C-4.01630163 -0.9946154 -2.86578759 -1.16161779 0 0 Z " fill="#24262A" transform="translate(295,685)"/>
|
||||
<path d="M0 0 C2.31 0.33 4.62 0.66 7 1 C7 1.66 7 2.32 7 3 C8.32 3 9.64 3 11 3 C11 3.66 11 4.32 11 5 C11.99 5.33 12.98 5.66 14 6 C9.50540476 5.29771949 5.09389959 3.99912387 1 2 C0.67 1.34 0.34 0.68 0 0 Z " fill="#0B5E3B" transform="translate(387,469)"/>
|
||||
<path d="M0 0 C-2.52480693 2.52480693 -4.13823034 2.55738633 -7.625 3.125 C-8.62789063 3.29257812 -9.63078125 3.46015625 -10.6640625 3.6328125 C-11.43492187 3.75398437 -12.20578125 3.87515625 -13 4 C-11.72807637 1.45615274 -10.80051531 1.30631844 -8.1875 0.3125 C-7.50042969 0.04050781 -6.81335938 -0.23148438 -6.10546875 -0.51171875 C-3.70623504 -1.06812731 -2.3105636 -0.77364938 0 0 Z " fill="#0A673F" transform="translate(363,620)"/>
|
||||
<path d="M0 0 C-1.24176695 0.70226141 -2.49385599 1.38629109 -3.75 2.0625 C-4.79414063 2.63677734 -4.79414063 2.63677734 -5.859375 3.22265625 C-8.36140628 4.1312406 -9.5332355 3.88336837 -12 3 C-9.84267191 0.84267191 -8.88177557 0.51705175 -6 -0.1875 C-5.29875 -0.36667969 -4.5975 -0.54585937 -3.875 -0.73046875 C-2 -1 -2 -1 0 0 Z " fill="#0D4E34" transform="translate(396,607)"/>
|
||||
<path d="M0 0 C9.71428571 1.71428571 9.71428571 1.71428571 13 5 C11.35 5 9.7 5 8 5 C8 4.34 8 3.68 8 3 C7.236875 2.87625 6.47375 2.7525 5.6875 2.625 C3 2 3 2 0 0 Z " fill="#0B623D" transform="translate(338,510)"/>
|
||||
<path d="M0 0 C-0.33 0.99 -0.66 1.98 -1 3 C-4.28976808 4.09658936 -5.71303767 3.79953138 -9 3 C-4.5 0 -4.5 0 0 0 Z " fill="#BEC0C1" transform="translate(523,592)"/>
|
||||
<path d="M0 0 C1.24605143 -0.00320648 2.49210286 -0.00641296 3.77591342 -0.0097166 C7.20551324 -0.01654213 10.63489336 -0.01454336 14.06448046 -0.00866088 C17.80678204 -0.00446316 21.54906441 -0.01190796 25.29136121 -0.01781338 C31.84084796 -0.02655123 38.39030342 -0.02789611 44.93979454 -0.02431011 C54.67961151 -0.01899148 64.41939833 -0.02511011 74.15921202 -0.03339304 C92.56926142 -0.04861937 110.97929706 -0.05033948 129.38935144 -0.04787002 C143.60890976 -0.04603597 157.82846357 -0.0477993 172.04802132 -0.05209446 C174.09235779 -0.05269687 176.13669427 -0.05329806 178.18103074 -0.05389806 C181.26182901 -0.05480534 184.34262728 -0.05571401 187.42342555 -0.05663041 C216.06152484 -0.06505806 244.69962243 -0.06939698 273.33772278 -0.06522369 C274.95656045 -0.06498913 274.95656045 -0.06498913 276.60810192 -0.06474983 C282.07778518 -0.06394407 287.54746842 -0.0630983 293.01715167 -0.06223653 C294.64653437 -0.06197989 294.64653437 -0.06197989 296.30883388 -0.06171808 C298.50550538 -0.06137142 300.70217687 -0.06102289 302.89884837 -0.06067249 C337.07170733 -0.05531644 371.24454049 -0.06324318 405.41739464 -0.08170605 C443.83052411 -0.10243565 482.24363759 -0.11314686 520.65677297 -0.10913056 C524.75506425 -0.10873523 528.85335552 -0.10837368 532.9516468 -0.10803318 C533.96071349 -0.10794245 534.96978018 -0.10785172 536.00942464 -0.10775824 C551.22334929 -0.10659366 566.43725776 -0.11323001 581.65117836 -0.1238451 C598.96103916 -0.13591042 616.2708644 -0.13744077 633.58072574 -0.12505493 C643.25916158 -0.11843905 652.93751231 -0.12002798 662.6159413 -0.13364345 C669.09234848 -0.1419165 675.56869223 -0.13915349 682.04509432 -0.12766818 C685.72751758 -0.12143989 689.40977538 -0.11973724 693.09218879 -0.13153151 C742.29620511 -0.27803261 783.72404367 14.44275388 819.33878136 49.25447559 C838.15454949 68.05987805 851.6807596 92.15656693 859.15128136 117.62947559 C859.50259117 118.81223072 859.50259117 118.81223072 859.86099815 120.01887989 C864.84716349 137.93439184 865.34131204 155.41560974 865.28668402 173.91294083 C865.27899556 177.65690687 865.28888432 181.40083665 865.29641402 185.14479738 C865.3071966 191.69097825 865.30695291 198.23708474 865.29927254 204.78326893 C865.2878824 214.51804038 865.2937484 224.25274959 865.30289746 233.98752068 C865.31866453 251.37833898 865.3173375 268.76912415 865.30866072 286.15994558 C865.30110466 301.37985494 865.29914832 316.59975219 865.30272865 331.81966305 C865.30295781 332.83047421 865.30318696 333.84128537 865.30342306 334.88272717 C865.30435713 338.98627537 865.30531233 343.08982357 865.30630235 347.19337176 C865.3154731 385.66415573 865.30716901 424.13491865 865.29101166 462.60569937 C865.27677663 496.7590625 865.27804855 530.91239206 865.29239464 565.06575489 C865.30847087 603.46608751 865.31482155 641.86640153 865.30555785 680.26673704 C865.30459574 684.36298215 865.30365722 688.45922726 865.30272865 692.55547237 C865.3023728 694.06829454 865.3023728 694.06829454 865.30200977 695.61167876 C865.29860234 710.81857886 865.3025789 726.02546547 865.31016731 741.2323637 C865.31875621 758.53008151 865.31657695 775.82776109 865.30032508 793.12547415 C865.29153676 802.79747308 865.2909114 812.46938917 865.3022636 822.1413859 C865.30901854 828.61208821 865.30475417 835.08272447 865.2917493 841.55341703 C865.28465281 845.23332826 865.2820887 848.9130756 865.29296724 852.59298133 C865.3442355 871.30113003 864.89727324 889.46647244 859.15128136 907.44197559 C858.89524132 908.2652837 858.63920128 909.0885918 858.37540245 909.93684864 C855.79685812 917.9856683 852.58102984 925.65348896 848.90128136 933.25447559 C848.61591515 933.86033497 848.33054893 934.46619434 848.03653526 935.09041309 C830.37472338 972.22703729 796.06237029 1001.64914703 757.52628136 1015.44197559 C741.01943895 1021.1452558 724.41526408 1024.57505987 706.89996243 1024.5117445 C705.65228729 1024.51525057 704.40461215 1024.51875665 703.11912867 1024.52236897 C699.68034267 1024.53174518 696.24166373 1024.52885318 692.80287276 1024.52381342 C689.05227316 1024.52052693 685.3016955 1024.52884577 681.55110306 1024.53563178 C674.98544107 1024.54591004 668.41980856 1024.54877936 661.85413933 1024.54671955 C652.09014292 1024.54366388 642.32617902 1024.55200613 632.56218895 1024.5624688 C614.10585117 1024.58179372 595.64952453 1024.58743648 577.19317783 1024.58858292 C562.93642719 1024.5895027 548.6796809 1024.59362298 534.42293167 1024.59998417 C530.31618419 1024.60177932 526.2094367 1024.60355895 522.10268921 1024.60533392 C521.08167553 1024.60577602 520.06066186 1024.60621812 519.00870837 1024.60667361 C490.29523693 1024.61897939 461.581767 1024.62687502 432.86829281 1024.62598705 C431.78608639 1024.6259559 430.70387998 1024.62592474 429.58887942 1024.62589264 C424.10406817 1024.62572643 418.61925692 1024.62552835 413.13444567 1024.62532322 C412.04518472 1024.62528333 410.95592376 1024.62524344 409.8336549 1024.62520234 C407.63090343 1024.62512147 405.42815196 1024.62503787 403.22540049 1024.62495156 C368.96540695 10Line truncated
|
||||
<path d="M0 0 C2.3607826 -0.00533069 4.7215618 -0.01239937 7.08233458 -0.02104706 C12.86781777 -0.03768809 18.65308544 -0.03586118 24.43857937 -0.02603391 C31.37506207 -0.01528955 38.31145513 -0.02784201 45.24792682 -0.04094901 C58.83711965 -0.06583655 72.42624267 -0.0677834 86.01545164 -0.0596749 C97.90842776 -0.05259347 109.80137684 -0.05255306 121.6943531 -0.05953789 C123.29160454 -0.06044923 124.88885599 -0.06135999 126.48610743 -0.06227018 C128.89321227 -0.06365335 131.3003171 -0.06504445 133.70742193 -0.06645631 C156.07605562 -0.07943052 178.44468029 -0.0845595 200.81331539 -0.07292271 C202.07720574 -0.07226575 202.07720574 -0.07226575 203.36662924 -0.07159552 C210.20845318 -0.06798894 217.05027701 -0.06415405 223.89210082 -0.06030459 C250.57968088 -0.04545695 277.26715859 -0.05423912 303.95472717 -0.08226013 C333.96091556 -0.11370558 363.96704337 -0.12865264 393.97324854 -0.11846012 C397.17413714 -0.11742339 400.37502576 -0.11643616 403.57591438 -0.11547661 C404.75806397 -0.11510328 404.75806397 -0.11510328 405.96409537 -0.11472241 C417.84673069 -0.11128624 429.72931241 -0.12057354 441.61193657 -0.13624763 C455.12861173 -0.15403651 468.64515667 -0.15411882 482.16182631 -0.13089598 C489.05584211 -0.11951551 495.94963463 -0.11592963 502.84363937 -0.13519669 C509.15952167 -0.15202846 515.47494573 -0.1456129 521.79079849 -0.1209799 C524.07057599 -0.11610521 526.35039063 -0.11958862 528.63013696 -0.13246734 C545.55919757 -0.22117063 559.49258132 1.40958132 573.70204163 11.5146637 C574.65143616 12.18948792 574.65143616 12.18948792 575.62001038 12.87794495 C584.56469704 19.88757798 591.37473569 31.41173592 593.70204163 42.5146637 C593.95009484 46.21967671 593.9332442 49.92717174 593.95204163 53.6396637 C593.98297913 55.13239807 593.98297913 55.13239807 594.01454163 56.6552887 C594.09018739 71.14145219 588.46971331 82.75864333 578.63954163 93.3271637 C572.7611904 98.9075711 566.35707715 102.79152692 558.70204163 105.5146637 C557.98319668 105.77360066 557.26435173 106.03253763 556.5237236 106.29932117 C545.67592079 109.56985516 534.52679546 108.91143924 523.30848694 108.85523987 C520.90407196 108.85431984 518.49965616 108.85472308 516.09524155 108.85633564 C509.68138016 108.85701034 503.26768815 108.83967715 496.85386658 108.81850433 C490.82587678 108.80136501 484.79792729 108.80061201 478.76991272 108.79847717 C461.16393343 108.7827621 443.55798536 108.74087841 425.95204163 108.7021637 C386.59954163 108.6402887 347.24704163 108.5784137 306.70204163 108.5146637 C306.70204163 227.6446637 306.70204163 346.7746637 306.70204163 469.5146637 C286.90204163 469.5146637 267.10204163 469.5146637 246.70204163 469.5146637 C246.70204163 513.4046637 246.70204163 557.2946637 246.70204163 602.5146637 C266.50204163 602.5146637 286.30204163 602.5146637 306.70204163 602.5146637 C306.79485413 611.0843512 306.79485413 611.0843512 306.88954163 619.8271637 C306.91685364 621.60647278 306.94416565 623.38578186 306.9723053 625.2190094 C307.11177013 640.67747815 304.32285969 652.54241746 293.63954163 664.2646637 C283.35164203 674.1494599 271.29383334 675.99381057 257.74501038 675.7881012 C245.19699202 675.32053069 236.72499202 668.99701836 228.38954163 660.0771637 C220.57277107 650.9239274 218.39160693 641.68285062 218.46153259 629.87820435 C218.45896932 628.28889812 218.45896932 628.28889812 218.45635426 626.6674847 C218.45306486 623.123718 218.46405419 619.58014142 218.47499084 616.03639221 C218.47555508 613.49352015 218.47523817 610.95064775 218.47410583 608.40777588 C218.47365067 602.93649447 218.47931674 597.46527407 218.49058151 591.9940052 C218.50685769 584.08354929 218.51204668 576.17311643 218.5145575 568.26264559 C218.51893128 555.42744908 218.53223178 542.59227803 218.55116272 529.75709534 C218.56952972 517.29127632 218.58368221 504.82546422 218.59217834 492.3596344 C218.59270379 491.5908286 218.59322924 490.82202281 218.59377061 490.02991987 C218.59638074 486.17292397 218.59890815 482.31592802 218.60139441 478.45893204 C218.62211705 446.47749075 218.65733395 414.49608076 218.70204163 382.5146637 C224.61760486 380.08952596 230.53376566 377.66585017 236.45030212 375.24308777 C238.45381022 374.42248424 240.45719208 373.60157242 242.46043396 372.78031921 C294.33562193 351.5146637 294.33562193 351.5146637 297.70204163 351.5146637 C297.70204163 313.8946637 297.70204163 276.2746637 297.70204163 237.5146637 C293.24704163 236.0296637 293.24704163 236.0296637 288.70204163 234.5146637 C286.21106492 233.56633242 283.72835319 232.59599737 281.25453186 231.60377502 C280.55959405 231.32587222 279.86465624 231.04796942 279.14865971 230.76164532 C276.93500856 229.87574275 274.72279495 228.98631453 272.51063538 228.09669495 C271.12991114 227.54352131 269.74909265 226.9905829 268.36817932 226.43788147 C263.29082801 224.40381573 258.21529078 222.36522413 253.13954163 220.3271637 C241.77516663 215.7690387 230.41079163 211.2109137 218.70204163 206.5146637 C218.70204163 174.1746637 218.70204163 141.8346637 218.70204163 108.5146637 C211.4392218 1Line truncated
|
||||
<path d="M0 0 C5.86817508 1.95605836 11.59073119 3.95365747 17.30737305 6.27001953 C18.09306325 6.58584142 18.87875345 6.9016633 19.68825245 7.22705555 C22.29709699 8.27645462 24.90439611 9.32963844 27.51171875 10.3828125 C29.39488199 11.14115547 31.2781448 11.89925122 33.16149902 12.65711975 C37.21264727 14.28789145 41.26312379 15.92031644 45.31307983 17.55404663 C51.7542863 20.15236465 58.19696081 22.74702807 64.63989258 25.34106445 C71.41130904 28.06755974 78.18261714 30.79432275 84.95341492 33.52235413 C108.15087431 42.86883922 131.35537268 52.19778488 154.5609436 61.52410889 C159.994401 63.70786608 165.42764165 65.89216193 170.86079502 68.07667542 C193.23239581 77.07158187 215.60890835 86.05370489 238 95 C238 116.12 238 137.24 238 159 C232.01359375 161.39185547 232.01359375 161.39185547 225.90625 163.83203125 C206.57998543 171.55973546 187.26934996 179.32631616 167.95794678 187.09106445 C161.36560101 189.74165676 154.77289802 192.39135988 148.18017578 195.04101562 C126.79552102 203.63604381 105.41240644 212.23486217 84.03399658 220.84541321 C75.73587703 224.18760199 67.43708216 227.52811242 59.13812637 230.86822414 C52.33470621 233.60660835 45.53222385 236.34730609 38.73039818 239.08964825 C34.51543173 240.78839672 30.29959748 242.48498283 26.08351517 244.1809597 C24.12464123 244.96946815 22.16613491 245.75889055 20.20801926 246.54928017 C17.53921709 247.62640467 14.86914587 248.70032007 12.19873047 249.7734375 C11.02965778 250.24663689 11.02965778 250.24663689 9.83696747 250.72939587 C6.56202144 252.04197447 3.43106937 253.14223266 0 254 C0 231.56 0 209.12 0 186 C21.85641702 177.6267728 43.73124762 169.30784563 65.63814926 161.06789398 C71.62769516 158.81489337 77.61678737 156.56068672 83.60580635 154.30628586 C85.09797285 153.74460399 86.5901539 153.18296077 88.08234978 152.62135696 C97.85972778 148.94125789 107.63197621 145.24805417 117.3984375 141.5390625 C119.52367608 140.73273275 121.64892001 139.92641707 123.77416992 139.12011719 C127.62723328 137.65810939 131.4786996 136.19199339 135.3293457 134.72363281 C137.01507528 134.08382248 138.70086226 133.44416335 140.38671875 132.8046875 C141.51894363 132.37079208 141.51894363 132.37079208 142.67404175 131.9281311 C147.77255889 130 147.77255889 130 150 130 C149.67 128.02 149.34 126.04 149 124 C147.88878784 123.58995728 147.88878784 123.58995728 146.75512695 123.17163086 C117.43275623 112.3398187 88.19328622 101.28796181 58.94955444 90.24617195 C50.58387921 87.08758433 42.21738212 83.93117504 33.85093689 80.77462769 C22.56697288 76.5172614 11.28352836 72.25852172 0 68 C0 45.56 0 23.12 0 0 Z " fill="#F39044" transform="translate(272,342)"/>
|
||||
<path d="M0 0 C81.84 0 163.68 0 248 0 C248 19.47 248 38.94 248 59 C166.16 59 84.32 59 0 59 C0 39.53 0 20.06 0 0 Z " fill="#F39044" transform="translate(533,681)"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 447 B After Width: | Height: | Size: 383 B |
|
Before Width: | Height: | Size: 671 B After Width: | Height: | Size: 537 B |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 896 B After Width: | Height: | Size: 683 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 938 B |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 353 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 524 KiB |
|
Before Width: | Height: | Size: 685 KiB |
|
Before Width: | Height: | Size: 158 KiB |
|
Before Width: | Height: | Size: 415 KiB |
|
Before Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 598 KiB |
|
Before Width: | Height: | Size: 402 KiB |
|
Before Width: | Height: | Size: 355 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 307 KiB |
|
Before Width: | Height: | Size: 723 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 153 KiB |
@@ -0,0 +1,117 @@
|
||||
import sharp from 'sharp';
|
||||
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { join, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(__dirname, '..');
|
||||
const publicDir = join(root, 'public');
|
||||
const iconsDir = join(publicDir, 'icons');
|
||||
|
||||
mkdirSync(iconsDir, { recursive: true });
|
||||
|
||||
const svgBuffer = readFileSync(join(publicDir, 'icon.svg'));
|
||||
|
||||
const pngSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024];
|
||||
|
||||
console.log('Generating PNG icons...');
|
||||
await Promise.all(
|
||||
pngSizes.map(size =>
|
||||
sharp(svgBuffer)
|
||||
.resize(size, size)
|
||||
.png()
|
||||
.toFile(join(iconsDir, `${size}x${size}.png`))
|
||||
.then(() => console.log(` ✓ icons/${size}x${size}.png`))
|
||||
)
|
||||
);
|
||||
|
||||
// icon.png (1024x1024) for Linux electron-builder
|
||||
await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(publicDir, 'icon.png'));
|
||||
console.log(' ✓ icon.png');
|
||||
|
||||
// icon-mac.png (512x512) for macOS
|
||||
await sharp(svgBuffer).resize(512, 512).png().toFile(join(publicDir, 'icon-mac.png'));
|
||||
console.log(' ✓ icon-mac.png');
|
||||
|
||||
// full-icon.png (1024x1024) used in app UI
|
||||
await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(publicDir, 'full-icon.png'));
|
||||
console.log(' ✓ full-icon.png');
|
||||
|
||||
// favicon.ico — embed 16, 32, 48 px layers
|
||||
console.log('Generating favicon.ico...');
|
||||
const icoSizes = [16, 32, 48];
|
||||
const icoBuffers = await Promise.all(
|
||||
icoSizes.map(size =>
|
||||
sharp(svgBuffer).resize(size, size).png().toBuffer()
|
||||
)
|
||||
);
|
||||
writeFileSync(join(publicDir, 'favicon.ico'), buildIco(icoBuffers, icoSizes));
|
||||
console.log(' ✓ favicon.ico');
|
||||
|
||||
// icon.ico — embed 16, 32, 48, 64, 128, 256 px layers
|
||||
console.log('Generating icon.ico...');
|
||||
const winSizes = [16, 32, 48, 64, 128, 256];
|
||||
const winBuffers = await Promise.all(
|
||||
winSizes.map(size =>
|
||||
sharp(svgBuffer).resize(size, size).png().toBuffer()
|
||||
)
|
||||
);
|
||||
writeFileSync(join(publicDir, 'icon.ico'), buildIco(winBuffers, winSizes));
|
||||
console.log(' ✓ icon.ico');
|
||||
|
||||
// icons/icon.ico and icons/icon.icns placeholders (stubs pointing to source)
|
||||
// electron-builder generates .icns; copy the 1024 PNG as icons/icon.png for reference
|
||||
await sharp(svgBuffer).resize(1024, 1024).png().toFile(join(iconsDir, 'icon.ico').replace('icon.ico', '1024x1024.png'));
|
||||
// Copy icon.ico and icon.icns into icons/ as well
|
||||
import { copyFileSync } from 'fs';
|
||||
copyFileSync(join(publicDir, 'icon.ico'), join(iconsDir, 'icon.ico'));
|
||||
console.log(' ✓ icons/icon.ico');
|
||||
|
||||
console.log('\nDone! Note: icon.icns requires macOS tools (iconutil). Use electron-builder on macOS to generate it.');
|
||||
|
||||
/**
|
||||
* Builds a minimal ICO file from PNG buffers.
|
||||
* @param {Buffer[]} pngBuffers
|
||||
* @param {number[]} sizes
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
function buildIco(pngBuffers, sizes) {
|
||||
const count = pngBuffers.length;
|
||||
const headerSize = 6;
|
||||
const dirEntrySize = 16;
|
||||
const dirSize = headerSize + count * dirEntrySize;
|
||||
|
||||
let offset = dirSize;
|
||||
const entries = pngBuffers.map((buf, i) => {
|
||||
const size = sizes[i];
|
||||
const entry = { size, width: size > 255 ? 0 : size, height: size > 255 ? 0 : size, buf, offset };
|
||||
offset += buf.length;
|
||||
return entry;
|
||||
});
|
||||
|
||||
const totalSize = offset;
|
||||
const ico = Buffer.alloc(totalSize);
|
||||
|
||||
// ICO header
|
||||
ico.writeUInt16LE(0, 0); // reserved
|
||||
ico.writeUInt16LE(1, 2); // type: 1 = ICO
|
||||
ico.writeUInt16LE(count, 4); // image count
|
||||
|
||||
// Directory entries
|
||||
entries.forEach((e, i) => {
|
||||
const base = headerSize + i * dirEntrySize;
|
||||
ico.writeUInt8(e.width, base); // width (0 = 256)
|
||||
ico.writeUInt8(e.height, base + 1); // height (0 = 256)
|
||||
ico.writeUInt8(0, base + 2); // color count
|
||||
ico.writeUInt8(0, base + 3); // reserved
|
||||
ico.writeUInt16LE(1, base + 4); // planes
|
||||
ico.writeUInt16LE(32, base + 6); // bit count
|
||||
ico.writeUInt32LE(e.buf.length, base + 8); // size of image data
|
||||
ico.writeUInt32LE(e.offset, base + 12); // offset of image data
|
||||
});
|
||||
|
||||
// Image data
|
||||
entries.forEach(e => e.buf.copy(ico, e.offset));
|
||||
|
||||
return ico;
|
||||
}
|
||||
@@ -1044,6 +1044,23 @@ const migrateSchema = () => {
|
||||
{ column: "host_key_changed_count", sql: "ALTER TABLE ssh_data ADD COLUMN host_key_changed_count INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "mac_address", sql: "ALTER TABLE ssh_data ADD COLUMN mac_address TEXT" },
|
||||
{ column: "port_knock_sequence", sql: "ALTER TABLE ssh_data ADD COLUMN port_knock_sequence TEXT" },
|
||||
{ column: "enable_ssh", sql: "ALTER TABLE ssh_data ADD COLUMN enable_ssh INTEGER NOT NULL DEFAULT 1" },
|
||||
{ column: "enable_rdp", sql: "ALTER TABLE ssh_data ADD COLUMN enable_rdp INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "enable_vnc", sql: "ALTER TABLE ssh_data ADD COLUMN enable_vnc INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "enable_telnet", sql: "ALTER TABLE ssh_data ADD COLUMN enable_telnet INTEGER NOT NULL DEFAULT 0" },
|
||||
{ column: "ssh_port", sql: "ALTER TABLE ssh_data ADD COLUMN ssh_port INTEGER DEFAULT 22" },
|
||||
{ column: "rdp_port", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_port INTEGER DEFAULT 3389" },
|
||||
{ column: "vnc_port", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_port INTEGER DEFAULT 5900" },
|
||||
{ column: "telnet_port", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_port INTEGER DEFAULT 23" },
|
||||
{ column: "rdp_user", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_user TEXT" },
|
||||
{ column: "rdp_password", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_password TEXT" },
|
||||
{ column: "rdp_domain", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_domain TEXT" },
|
||||
{ column: "rdp_security", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_security TEXT" },
|
||||
{ column: "rdp_ignore_cert", sql: "ALTER TABLE ssh_data ADD COLUMN rdp_ignore_cert INTEGER DEFAULT 0" },
|
||||
{ column: "vnc_password", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_password TEXT" },
|
||||
{ column: "vnc_user", sql: "ALTER TABLE ssh_data ADD COLUMN vnc_user TEXT" },
|
||||
{ column: "telnet_user", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_user TEXT" },
|
||||
{ column: "telnet_password", sql: "ALTER TABLE ssh_data ADD COLUMN telnet_password TEXT" },
|
||||
];
|
||||
|
||||
for (const migration of sshDataMigrations) {
|
||||
|
||||
@@ -129,6 +129,28 @@ export const hosts = sqliteTable("ssh_data", {
|
||||
terminalConfig: text("terminal_config"),
|
||||
quickActions: text("quick_actions"),
|
||||
notes: text("notes"),
|
||||
enableSsh: integer("enable_ssh", { mode: "boolean" }).notNull().default(true),
|
||||
enableRdp: integer("enable_rdp", { mode: "boolean" }).notNull().default(false),
|
||||
enableVnc: integer("enable_vnc", { mode: "boolean" }).notNull().default(false),
|
||||
enableTelnet: integer("enable_telnet", { mode: "boolean" }).notNull().default(false),
|
||||
|
||||
sshPort: integer("ssh_port").default(22),
|
||||
rdpPort: integer("rdp_port").default(3389),
|
||||
vncPort: integer("vnc_port").default(5900),
|
||||
telnetPort: integer("telnet_port").default(23),
|
||||
|
||||
rdpUser: text("rdp_user"),
|
||||
rdpPassword: text("rdp_password"),
|
||||
rdpDomain: text("rdp_domain"),
|
||||
rdpSecurity: text("rdp_security"),
|
||||
rdpIgnoreCert: integer("rdp_ignore_cert", { mode: "boolean" }).default(false),
|
||||
|
||||
vncPassword: text("vnc_password"),
|
||||
vncUser: text("vnc_user"),
|
||||
|
||||
telnetUser: text("telnet_user"),
|
||||
telnetPassword: text("telnet_password"),
|
||||
|
||||
domain: text("domain"),
|
||||
security: text("security"),
|
||||
ignoreCert: integer("ignore_cert", { mode: "boolean" }).default(false),
|
||||
|
||||
@@ -252,6 +252,9 @@ const SENSITIVE_FIELDS = [
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
"socks5Password",
|
||||
"rdpPassword",
|
||||
"vncPassword",
|
||||
"telnetPassword",
|
||||
];
|
||||
|
||||
function stripSensitiveFields(
|
||||
@@ -288,6 +291,23 @@ function transformHostResponse(
|
||||
showTunnelInSidebar: !!host.showTunnelInSidebar,
|
||||
showDockerInSidebar: !!host.showDockerInSidebar,
|
||||
showServerStatsInSidebar: !!host.showServerStatsInSidebar,
|
||||
enableSsh: host.enableSsh !== undefined ? !!host.enableSsh : true,
|
||||
enableRdp: !!host.enableRdp,
|
||||
enableVnc: !!host.enableVnc,
|
||||
enableTelnet: !!host.enableTelnet,
|
||||
sshPort: host.sshPort ?? host.port ?? 22,
|
||||
rdpPort: host.rdpPort ?? 3389,
|
||||
vncPort: host.vncPort ?? 5900,
|
||||
telnetPort: host.telnetPort ?? 23,
|
||||
rdpUser: host.rdpUser || undefined,
|
||||
rdpDomain: host.rdpDomain || undefined,
|
||||
rdpSecurity: host.rdpSecurity || undefined,
|
||||
rdpIgnoreCert: !!host.rdpIgnoreCert,
|
||||
vncUser: host.vncUser || undefined,
|
||||
telnetUser: host.telnetUser || undefined,
|
||||
hasRdpPassword: !!host.rdpPassword,
|
||||
hasVncPassword: !!host.vncPassword,
|
||||
hasTelnetPassword: !!host.telnetPassword,
|
||||
tunnelConnections: host.tunnelConnections
|
||||
? JSON.parse(host.tunnelConnections as string)
|
||||
: [],
|
||||
@@ -593,6 +613,23 @@ router.post(
|
||||
portKnockSequence,
|
||||
overrideCredentialUsername,
|
||||
macAddress,
|
||||
enableSsh,
|
||||
enableRdp,
|
||||
enableVnc,
|
||||
enableTelnet,
|
||||
sshPort,
|
||||
rdpPort,
|
||||
vncPort,
|
||||
telnetPort,
|
||||
rdpUser,
|
||||
rdpPassword,
|
||||
rdpDomain,
|
||||
rdpSecurity,
|
||||
rdpIgnoreCert,
|
||||
vncPassword,
|
||||
vncUser,
|
||||
telnetUser,
|
||||
telnetPassword,
|
||||
} = hostData;
|
||||
databaseLogger.info("Creating SSH host", {
|
||||
operation: "host_create",
|
||||
@@ -685,6 +722,20 @@ router.post(
|
||||
portKnockSequence: portKnockSequence
|
||||
? JSON.stringify(portKnockSequence)
|
||||
: null,
|
||||
enableSsh: enableSsh !== false ? 1 : 0,
|
||||
enableRdp: enableRdp ? 1 : 0,
|
||||
enableVnc: enableVnc ? 1 : 0,
|
||||
enableTelnet: enableTelnet ? 1 : 0,
|
||||
sshPort: sshPort || port || 22,
|
||||
rdpPort: rdpPort || 3389,
|
||||
vncPort: vncPort || 5900,
|
||||
telnetPort: telnetPort || 23,
|
||||
rdpUser: rdpUser || null,
|
||||
rdpDomain: rdpDomain || null,
|
||||
rdpSecurity: rdpSecurity || null,
|
||||
rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
|
||||
vncUser: vncUser || null,
|
||||
telnetUser: telnetUser || null,
|
||||
};
|
||||
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
@@ -743,6 +794,10 @@ router.post(
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
sshDataObj.rdpPassword = rdpPassword || null;
|
||||
sshDataObj.vncPassword = vncPassword || null;
|
||||
sshDataObj.telnetPassword = telnetPassword || null;
|
||||
|
||||
try {
|
||||
const result = await SimpleDBOps.insert(
|
||||
hosts,
|
||||
@@ -1092,6 +1147,23 @@ router.put(
|
||||
portKnockSequence,
|
||||
overrideCredentialUsername,
|
||||
macAddress,
|
||||
enableSsh,
|
||||
enableRdp,
|
||||
enableVnc,
|
||||
enableTelnet,
|
||||
sshPort,
|
||||
rdpPort,
|
||||
vncPort,
|
||||
telnetPort,
|
||||
rdpUser,
|
||||
rdpPassword,
|
||||
rdpDomain,
|
||||
rdpSecurity,
|
||||
rdpIgnoreCert,
|
||||
vncPassword,
|
||||
vncUser,
|
||||
telnetUser,
|
||||
telnetPassword,
|
||||
} = hostData;
|
||||
databaseLogger.info("Updating SSH host", {
|
||||
operation: "host_update",
|
||||
@@ -1181,6 +1253,20 @@ router.put(
|
||||
portKnockSequence: portKnockSequence
|
||||
? JSON.stringify(portKnockSequence)
|
||||
: null,
|
||||
enableSsh: enableSsh !== false ? 1 : 0,
|
||||
enableRdp: enableRdp ? 1 : 0,
|
||||
enableVnc: enableVnc ? 1 : 0,
|
||||
enableTelnet: enableTelnet ? 1 : 0,
|
||||
sshPort: sshPort || port || 22,
|
||||
rdpPort: rdpPort || 3389,
|
||||
vncPort: vncPort || 5900,
|
||||
telnetPort: telnetPort || 23,
|
||||
rdpUser: rdpUser || null,
|
||||
rdpDomain: rdpDomain || null,
|
||||
rdpSecurity: rdpSecurity || null,
|
||||
rdpIgnoreCert: rdpIgnoreCert ? 1 : 0,
|
||||
vncUser: vncUser || null,
|
||||
telnetUser: telnetUser || null,
|
||||
};
|
||||
|
||||
// For non-SSH hosts (RDP, VNC, Telnet), always save password if provided
|
||||
@@ -1249,6 +1335,11 @@ router.put(
|
||||
sshDataObj.keyType = null;
|
||||
}
|
||||
|
||||
if (rdpPassword !== undefined) sshDataObj.rdpPassword = rdpPassword || null;
|
||||
if (vncPassword !== undefined) sshDataObj.vncPassword = vncPassword || null;
|
||||
if (telnetPassword !== undefined)
|
||||
sshDataObj.telnetPassword = telnetPassword || null;
|
||||
|
||||
try {
|
||||
const accessInfo = await permissionManager.canAccessHost(
|
||||
userId,
|
||||
|
||||
@@ -676,7 +676,18 @@ interface StatsConfig {
|
||||
}
|
||||
|
||||
const DEFAULT_STATS_CONFIG: StatsConfig = {
|
||||
enabledWidgets: ["cpu", "memory", "disk", "network", "uptime", "system"],
|
||||
enabledWidgets: [
|
||||
"cpu",
|
||||
"memory",
|
||||
"disk",
|
||||
"network",
|
||||
"uptime",
|
||||
"system",
|
||||
"login_stats",
|
||||
"processes",
|
||||
"ports",
|
||||
"firewall",
|
||||
],
|
||||
statusCheckEnabled: true,
|
||||
statusCheckInterval: 60,
|
||||
metricsEnabled: true,
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
import swaggerJSDoc from "@deadendjs/swagger-jsdoc";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promises as fs } from "fs";
|
||||
import { systemLogger } from "./utils/logger.js";
|
||||
|
||||
interface SwaggerOptions {
|
||||
definition: object;
|
||||
apis: string[];
|
||||
}
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const projectRoot = path.join(__dirname, "..", "..", "..");
|
||||
|
||||
const swaggerOptions: SwaggerOptions = {
|
||||
definition: {
|
||||
openapi: "3.0.3",
|
||||
info: {
|
||||
title: "Termix API",
|
||||
version: "0.0.0",
|
||||
description: "Termix Backend API Reference",
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
url: "http://localhost:30001",
|
||||
description: "Main database and authentication server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30003",
|
||||
description: "SSH tunnel management server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30004",
|
||||
description: "SSH file manager server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30005",
|
||||
description: "Server statistics and monitoring server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30006",
|
||||
description: "Dashboard server",
|
||||
},
|
||||
{
|
||||
url: "http://localhost:30007",
|
||||
description: "Docker management server",
|
||||
},
|
||||
],
|
||||
components: {
|
||||
securitySchemes: {
|
||||
bearerAuth: {
|
||||
type: "http",
|
||||
scheme: "bearer",
|
||||
bearerFormat: "JWT",
|
||||
},
|
||||
},
|
||||
schemas: {
|
||||
Error: {
|
||||
type: "object",
|
||||
properties: {
|
||||
error: { type: "string" },
|
||||
details: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
security: [
|
||||
{
|
||||
bearerAuth: [],
|
||||
},
|
||||
],
|
||||
tags: [
|
||||
{
|
||||
name: "Alerts",
|
||||
description: "System alerts and notifications management",
|
||||
},
|
||||
{
|
||||
name: "Credentials",
|
||||
description: "SSH credential management",
|
||||
},
|
||||
{
|
||||
name: "Network Topology",
|
||||
description: "Network topology visualization and management",
|
||||
},
|
||||
{
|
||||
name: "RBAC",
|
||||
description: "Role-based access control for host sharing",
|
||||
},
|
||||
{
|
||||
name: "Snippets",
|
||||
description: "Command snippet management",
|
||||
},
|
||||
{
|
||||
name: "Terminal",
|
||||
description: "Terminal command history",
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
description: "User management and authentication",
|
||||
},
|
||||
{
|
||||
name: "Dashboard",
|
||||
description: "Dashboard statistics and activity",
|
||||
},
|
||||
{
|
||||
name: "Docker",
|
||||
description: "Docker container management",
|
||||
},
|
||||
{
|
||||
name: "SSH Tunnels",
|
||||
description: "SSH tunnel connection management",
|
||||
},
|
||||
{
|
||||
name: "Server Stats",
|
||||
description: "Server status monitoring and metrics collection",
|
||||
},
|
||||
{
|
||||
name: "File Manager",
|
||||
description: "SSH file management operations",
|
||||
},
|
||||
],
|
||||
},
|
||||
apis: [
|
||||
path.join(projectRoot, "src", "backend", "database", "routes", "*.ts"),
|
||||
path.join(projectRoot, "src", "backend", "dashboard.ts"),
|
||||
path.join(projectRoot, "src", "backend", "ssh", "*.ts"),
|
||||
],
|
||||
};
|
||||
|
||||
async function generateOpenAPISpec() {
|
||||
try {
|
||||
systemLogger.info("Generating OpenAPI specification", {
|
||||
operation: "openapi_generate_start",
|
||||
});
|
||||
|
||||
const swaggerSpec = await swaggerJSDoc(swaggerOptions);
|
||||
|
||||
const outputPath = path.join(projectRoot, "openapi.json");
|
||||
|
||||
await fs.writeFile(
|
||||
outputPath,
|
||||
JSON.stringify(swaggerSpec, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
systemLogger.success("OpenAPI specification generated", {
|
||||
operation: "openapi_generate_success",
|
||||
});
|
||||
} catch (error) {
|
||||
systemLogger.error("Failed to generate OpenAPI specification", error, {
|
||||
operation: "openapi_generation",
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
generateOpenAPISpec();
|
||||
|
||||
export { swaggerOptions, generateOpenAPISpec };
|
||||
@@ -30,6 +30,10 @@ class FieldCrypto {
|
||||
"autostartPassword",
|
||||
"autostartKey",
|
||||
"autostartKeyPassword",
|
||||
"socks5Password",
|
||||
"rdpPassword",
|
||||
"vncPassword",
|
||||
"telnetPassword",
|
||||
]),
|
||||
ssh_credentials: new Set([
|
||||
"password",
|
||||
|
||||
@@ -267,6 +267,10 @@ export class LazyFieldEncryption {
|
||||
autostartPassword: "autostart_password",
|
||||
autostartKey: "autostart_key",
|
||||
autostartKeyPassword: "autostart_key_password",
|
||||
socks5Password: "socks5_password",
|
||||
rdpPassword: "rdp_password",
|
||||
vncPassword: "vnc_password",
|
||||
telnetPassword: "telnet_password",
|
||||
totpSecret: "totp_secret",
|
||||
totpBackupCodes: "totp_backup_codes",
|
||||
clientSecret: "client_secret",
|
||||
|
||||
@@ -1,492 +0,0 @@
|
||||
import { getDb } from "../database/db/index.js";
|
||||
import {
|
||||
users,
|
||||
hosts,
|
||||
sshCredentials,
|
||||
fileManagerRecent,
|
||||
fileManagerPinned,
|
||||
fileManagerShortcuts,
|
||||
dismissedAlerts,
|
||||
} from "../database/db/schema.js";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import { DataCrypto } from "./data-crypto.js";
|
||||
import { UserDataExport, type UserExportData } from "./user-data-export.js";
|
||||
import { databaseLogger } from "./logger.js";
|
||||
|
||||
interface ImportOptions {
|
||||
replaceExisting?: boolean;
|
||||
skipCredentials?: boolean;
|
||||
skipFileManagerData?: boolean;
|
||||
dryRun?: boolean;
|
||||
}
|
||||
|
||||
interface ImportResult {
|
||||
success: boolean;
|
||||
summary: {
|
||||
sshHostsImported: number;
|
||||
sshCredentialsImported: number;
|
||||
fileManagerItemsImported: number;
|
||||
dismissedAlertsImported: number;
|
||||
skippedItems: number;
|
||||
errors: string[];
|
||||
};
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
class UserDataImport {
|
||||
static async importUserData(
|
||||
targetUserId: string,
|
||||
exportData: UserExportData,
|
||||
options: ImportOptions = {},
|
||||
): Promise<ImportResult> {
|
||||
const {
|
||||
replaceExisting = false,
|
||||
skipCredentials = false,
|
||||
skipFileManagerData = false,
|
||||
dryRun = false,
|
||||
} = options;
|
||||
|
||||
try {
|
||||
const targetUser = await getDb()
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, targetUserId));
|
||||
if (!targetUser || targetUser.length === 0) {
|
||||
throw new Error(`Target user not found: ${targetUserId}`);
|
||||
}
|
||||
|
||||
const validation = UserDataExport.validateExportData(exportData);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid export data: ${validation.errors.join(", ")}`);
|
||||
}
|
||||
|
||||
let userDataKey: Buffer | null = null;
|
||||
if (exportData.metadata.encrypted) {
|
||||
userDataKey = DataCrypto.getUserDataKey(targetUserId);
|
||||
if (!userDataKey) {
|
||||
throw new Error(
|
||||
"Target user data not unlocked - password required for encrypted import",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const result: ImportResult = {
|
||||
success: false,
|
||||
summary: {
|
||||
sshHostsImported: 0,
|
||||
sshCredentialsImported: 0,
|
||||
fileManagerItemsImported: 0,
|
||||
dismissedAlertsImported: 0,
|
||||
skippedItems: 0,
|
||||
errors: [],
|
||||
},
|
||||
dryRun,
|
||||
};
|
||||
|
||||
if (
|
||||
exportData.userData.sshHosts &&
|
||||
exportData.userData.sshHosts.length > 0
|
||||
) {
|
||||
const importStats = await this.importSshHosts(
|
||||
targetUserId,
|
||||
exportData.userData.sshHosts as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun, userDataKey },
|
||||
);
|
||||
result.summary.sshHostsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
if (
|
||||
!skipCredentials &&
|
||||
exportData.userData.sshCredentials &&
|
||||
exportData.userData.sshCredentials.length > 0
|
||||
) {
|
||||
const importStats = await this.importSshCredentials(
|
||||
targetUserId,
|
||||
exportData.userData.sshCredentials as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun, userDataKey },
|
||||
);
|
||||
result.summary.sshCredentialsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
if (!skipFileManagerData && exportData.userData.fileManagerData) {
|
||||
const importStats = await this.importFileManagerData(
|
||||
targetUserId,
|
||||
exportData.userData.fileManagerData,
|
||||
{ replaceExisting, dryRun },
|
||||
);
|
||||
result.summary.fileManagerItemsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
if (
|
||||
exportData.userData.dismissedAlerts &&
|
||||
exportData.userData.dismissedAlerts.length > 0
|
||||
) {
|
||||
const importStats = await this.importDismissedAlerts(
|
||||
targetUserId,
|
||||
exportData.userData.dismissedAlerts as Record<string, unknown>[],
|
||||
{ replaceExisting, dryRun },
|
||||
);
|
||||
result.summary.dismissedAlertsImported = importStats.imported;
|
||||
result.summary.skippedItems += importStats.skipped;
|
||||
result.summary.errors.push(...importStats.errors);
|
||||
}
|
||||
|
||||
result.success = result.summary.errors.length === 0;
|
||||
|
||||
databaseLogger.success("User data import completed", {
|
||||
operation: "user_data_import_complete",
|
||||
targetUserId,
|
||||
dryRun,
|
||||
...result.summary,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
databaseLogger.error("User data import failed", error, {
|
||||
operation: "user_data_import_failed",
|
||||
targetUserId,
|
||||
dryRun,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private static async importSshHosts(
|
||||
targetUserId: string,
|
||||
sshHosts: Record<string, unknown>[],
|
||||
options: {
|
||||
replaceExisting: boolean;
|
||||
dryRun: boolean;
|
||||
userDataKey: Buffer | null;
|
||||
},
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const host of sshHosts) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(hosts)
|
||||
.where(
|
||||
and(
|
||||
eq(hosts.userId, targetUserId),
|
||||
eq(hosts.ip, host.ip as string),
|
||||
eq(hosts.port, host.port as number),
|
||||
eq(hosts.username, host.username as string),
|
||||
),
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newHostData: Record<string, unknown> = {
|
||||
...host,
|
||||
userId: targetUserId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length === 0) {
|
||||
newHostData.createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
let processedHostData: Record<string, unknown> = newHostData;
|
||||
if (options.userDataKey) {
|
||||
processedHostData = DataCrypto.encryptRecord(
|
||||
"ssh_data",
|
||||
newHostData,
|
||||
targetUserId,
|
||||
options.userDataKey,
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
delete processedHostData.id;
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(hosts)
|
||||
.set(processedHostData as unknown as typeof hosts.$inferInsert)
|
||||
.where(eq(hosts.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(hosts)
|
||||
.values(processedHostData as unknown as typeof hosts.$inferInsert);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`SSH host import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
private static async importSshCredentials(
|
||||
targetUserId: string,
|
||||
credentials: Record<string, unknown>[],
|
||||
options: {
|
||||
replaceExisting: boolean;
|
||||
dryRun: boolean;
|
||||
userDataKey: Buffer | null;
|
||||
},
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const credential of credentials) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(sshCredentials)
|
||||
.where(
|
||||
and(
|
||||
eq(sshCredentials.userId, targetUserId),
|
||||
eq(sshCredentials.name, credential.name as string),
|
||||
),
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newCredentialData: Record<string, unknown> = {
|
||||
...credential,
|
||||
userId: targetUserId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length === 0) {
|
||||
newCredentialData.usageCount = 0;
|
||||
newCredentialData.lastUsed = null;
|
||||
newCredentialData.createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
let processedCredentialData: Record<string, unknown> =
|
||||
newCredentialData;
|
||||
if (options.userDataKey) {
|
||||
processedCredentialData = DataCrypto.encryptRecord(
|
||||
"ssh_credentials",
|
||||
newCredentialData,
|
||||
targetUserId,
|
||||
options.userDataKey,
|
||||
) as Record<string, unknown>;
|
||||
}
|
||||
|
||||
delete processedCredentialData.id;
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(sshCredentials)
|
||||
.set(
|
||||
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
|
||||
)
|
||||
.where(eq(sshCredentials.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(sshCredentials)
|
||||
.values(
|
||||
processedCredentialData as unknown as typeof sshCredentials.$inferInsert,
|
||||
);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`SSH credential import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
private static async importFileManagerData(
|
||||
targetUserId: string,
|
||||
fileManagerData: Record<string, unknown>,
|
||||
options: { replaceExisting: boolean; dryRun: boolean },
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
try {
|
||||
if (fileManagerData.recent && Array.isArray(fileManagerData.recent)) {
|
||||
for (const item of fileManagerData.recent) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
lastOpened: new Date().toISOString(),
|
||||
};
|
||||
await getDb().insert(fileManagerRecent).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Recent file import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fileManagerData.pinned && Array.isArray(fileManagerData.pinned)) {
|
||||
for (const item of fileManagerData.pinned) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
pinnedAt: new Date().toISOString(),
|
||||
};
|
||||
await getDb().insert(fileManagerPinned).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Pinned file import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
fileManagerData.shortcuts &&
|
||||
Array.isArray(fileManagerData.shortcuts)
|
||||
) {
|
||||
for (const item of fileManagerData.shortcuts) {
|
||||
try {
|
||||
if (!options.dryRun) {
|
||||
const newItem = {
|
||||
...item,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
await getDb().insert(fileManagerShortcuts).values(newItem);
|
||||
}
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Shortcut import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`File manager data import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
private static async importDismissedAlerts(
|
||||
targetUserId: string,
|
||||
alerts: Record<string, unknown>[],
|
||||
options: { replaceExisting: boolean; dryRun: boolean },
|
||||
) {
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const alert of alerts) {
|
||||
try {
|
||||
if (options.dryRun) {
|
||||
imported++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const existing = await getDb()
|
||||
.select()
|
||||
.from(dismissedAlerts)
|
||||
.where(
|
||||
and(
|
||||
eq(dismissedAlerts.userId, targetUserId),
|
||||
eq(dismissedAlerts.alertId, alert.alertId as string),
|
||||
),
|
||||
);
|
||||
|
||||
if (existing.length > 0 && !options.replaceExisting) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAlert = {
|
||||
...alert,
|
||||
id: undefined,
|
||||
userId: targetUserId,
|
||||
dismissedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (existing.length > 0 && options.replaceExisting) {
|
||||
await getDb()
|
||||
.update(dismissedAlerts)
|
||||
.set(newAlert as typeof dismissedAlerts.$inferInsert)
|
||||
.where(eq(dismissedAlerts.id, existing[0].id));
|
||||
} else {
|
||||
await getDb()
|
||||
.insert(dismissedAlerts)
|
||||
.values(newAlert as typeof dismissedAlerts.$inferInsert);
|
||||
}
|
||||
|
||||
imported++;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`Dismissed alert import failed: ${error instanceof Error ? error.message : "Unknown error"}`,
|
||||
);
|
||||
skipped++;
|
||||
}
|
||||
}
|
||||
|
||||
return { imported, skipped, errors };
|
||||
}
|
||||
|
||||
static async importUserDataFromJSON(
|
||||
targetUserId: string,
|
||||
jsonData: string,
|
||||
options: ImportOptions = {},
|
||||
): Promise<ImportResult> {
|
||||
try {
|
||||
const exportData: UserExportData = JSON.parse(jsonData);
|
||||
return await this.importUserData(targetUserId, exportData, options);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error("Invalid JSON format in import data", { cause: error });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { UserDataImport, type ImportOptions, type ImportResult };
|
||||
@@ -1,62 +0,0 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-100 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-95",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ComponentProps<"button">, VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants, type ButtonProps };
|
||||
@@ -1,24 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Chart Container
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = "ChartContainer";
|
||||
|
||||
export { ChartContainer, RechartsPrimitive };
|
||||
@@ -1,184 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto thin-scrollbar",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -1,198 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, Circle } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent data-[state=open]:bg-accent flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=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 z-50 min-w-[8rem] overflow-hidden rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-4 w-4 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("bg-muted -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
};
|
||||
@@ -1,21 +0,0 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground bg-elevated dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base shadow-xs transition-[color,box-shadow] duration-200 outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -1,59 +0,0 @@
|
||||
import { type ComponentProps, type ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type KbdProps = ComponentProps<"span"> & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const Kbd = ({ className, children, ...props }: KbdProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex select-none items-center rounded-md border px-2 py-1 text-[10px] font-mono font-medium relative",
|
||||
"bg-linear-to-b from-gray-100 to-gray-200 border-gray-300 shadow-[0_2px_0_#ccc,0_3px_2px_rgba(0,0,0,0.25)]",
|
||||
"dark:from-zinc-800 dark:to-zinc-900 dark:border-zinc-700 dark:shadow-[0_2px_0_#222,0_3px_2px_rgba(0,0,0,0.4)]",
|
||||
"dark:text-zinc-200",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export type KbdKeyProps = ComponentProps<"span"> & {
|
||||
"aria-label"?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const KbdKey = ({ className, children, ...props }: KbdKeyProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"px-1 py-px rounded-sm select-none text-[10px] font-mono font-medium bg-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
|
||||
export type KbdSeparatorProps = ComponentProps<"span"> & {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const KbdSeparator = ({
|
||||
className,
|
||||
children = "+",
|
||||
...props
|
||||
}: KbdSeparatorProps) => (
|
||||
<span
|
||||
className={cn(
|
||||
"text-muted-foreground/70 text-[10px] mx-0.5 select-none pointer-events-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
@@ -1,29 +0,0 @@
|
||||
import * as React from "react";
|
||||
import * as ProgressPrimitive from "@radix-ui/react-progress";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
data-slot="progress"
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ProgressPrimitive.Indicator
|
||||
data-slot="progress-indicator"
|
||||
className="bg-primary h-full w-full flex-1 transition-all"
|
||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
||||
/>
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
@@ -1,58 +0,0 @@
|
||||
import * as React from "react";
|
||||
import { GripVerticalIcon } from "lucide-react";
|
||||
import {
|
||||
Group as ResizableGroup,
|
||||
Panel as ResizablePrimitivePanel,
|
||||
Separator as ResizableSeparator,
|
||||
} from "react-resizable-panels";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ResizablePanelGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizableGroup>) {
|
||||
return (
|
||||
<ResizableGroup
|
||||
data-slot="resizable-panel-group"
|
||||
className={cn(
|
||||
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ResizablePanel({
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizablePrimitivePanel>) {
|
||||
return <ResizablePrimitivePanel data-slot="resizable-panel" {...props} />;
|
||||
}
|
||||
|
||||
function ResizableHandle({
|
||||
withHandle,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ResizableSeparator> & {
|
||||
withHandle?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ResizableSeparator
|
||||
data-slot="resizable-handle"
|
||||
className={cn(
|
||||
"relative flex w-1 items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-1 data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:translate-x-0 data-[panel-group-direction=vertical]:after:-translate-y-1/2 [&[data-panel-group-direction=vertical]>div]:rotate-90 bg-edge-hover hover:bg-interact active:bg-pressed transition-colors duration-150",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{withHandle && (
|
||||
<div className="bg-edge-hover hover:bg-interact active:bg-pressed z-10 flex h-4 w-3 items-center justify-center rounded-xs border transition-colors duration-150">
|
||||
<GripVerticalIcon className="size-2.5" />
|
||||
</div>
|
||||
)}
|
||||
</ResizableSeparator>
|
||||
);
|
||||
}
|
||||
|
||||
export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
|
||||
@@ -1,68 +0,0 @@
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { Toaster as Sonner, type ToasterProps, toast } from "sonner";
|
||||
import { useRef } from "react";
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
const lastToastRef = useRef<{ text: string; timestamp: number } | null>(null);
|
||||
|
||||
const originalToast = toast;
|
||||
|
||||
const rateLimitedToast = (
|
||||
message: string,
|
||||
options?: Record<string, unknown>,
|
||||
) => {
|
||||
const now = Date.now();
|
||||
const lastToast = lastToastRef.current;
|
||||
|
||||
if (
|
||||
lastToast &&
|
||||
lastToast.text === message &&
|
||||
now - lastToast.timestamp < 1000
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastToastRef.current = { text: message, timestamp: now };
|
||||
return originalToast(message, options);
|
||||
};
|
||||
|
||||
Object.assign(toast, {
|
||||
success: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "success" }),
|
||||
error: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "error" }),
|
||||
warning: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "warning" }),
|
||||
info: (message: string, options?: Record<string, unknown>) =>
|
||||
rateLimitedToast(message, { ...options, type: "info" }),
|
||||
message: rateLimitedToast,
|
||||
});
|
||||
|
||||
const darkCustomThemes = [
|
||||
"dracula",
|
||||
"gentlemansChoice",
|
||||
"midnightEspresso",
|
||||
"catppuccinMocha",
|
||||
];
|
||||
const sonnerTheme: ToasterProps["theme"] = darkCustomThemes.includes(theme)
|
||||
? "dark"
|
||||
: (theme as ToasterProps["theme"]);
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={sonnerTheme}
|
||||
className="toaster group"
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||
@@ -1,60 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
|
||||
return (
|
||||
<TooltipPrimitive.Provider
|
||||
data-slot="tooltip-provider"
|
||||
delayDuration={delayDuration}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
|
||||
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function TooltipContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
|
||||
return (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-elevated text-foreground border border-edge-medium shadow-lg animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=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 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -1,559 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: var(--foreground);
|
||||
background-color: var(--bg-base);
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--radius: 0.625rem;
|
||||
--background: #ffffff;
|
||||
--foreground: #18181b;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #18181b;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #18181b;
|
||||
--primary: #27272a;
|
||||
--primary-foreground: #fafafa;
|
||||
--secondary: #f4f4f5;
|
||||
--secondary-foreground: #27272a;
|
||||
--muted: #f4f4f5;
|
||||
--muted-foreground: #71717a;
|
||||
--accent: #f4f4f5;
|
||||
--accent-foreground: #27272a;
|
||||
--destructive: #dc2626;
|
||||
--border: #e4e4e7;
|
||||
--input: #e4e4e7;
|
||||
--ring: #a1a1aa;
|
||||
--chart-1: #e76e50;
|
||||
--chart-2: #2a9d8f;
|
||||
--chart-3: #264653;
|
||||
--chart-4: #e9c46a;
|
||||
--chart-5: #f4a261;
|
||||
--sidebar: #f9f9f9;
|
||||
--sidebar-foreground: #18181b;
|
||||
--sidebar-primary: #27272a;
|
||||
--sidebar-primary-foreground: #fafafa;
|
||||
--sidebar-accent: #f4f4f5;
|
||||
--sidebar-accent-foreground: #27272a;
|
||||
--sidebar-border: #e4e4e7;
|
||||
--sidebar-ring: #a1a1aa;
|
||||
|
||||
--bg-base: #fcfcfc;
|
||||
--bg-elevated: #ffffff;
|
||||
--bg-surface: #f3f4f6;
|
||||
--bg-surface-hover: #e5e7eb;
|
||||
--bg-input: #ffffff;
|
||||
--bg-deepest: #e5e7eb;
|
||||
--bg-header: #eeeeef;
|
||||
--bg-button: #f3f4f6;
|
||||
--bg-active: #e5e7eb;
|
||||
--bg-light: #fafafa;
|
||||
--bg-subtle: #f5f5f5;
|
||||
--bg-interact: #d1d5db;
|
||||
--border-base: #e5e7eb;
|
||||
--border-panel: #d1d5db;
|
||||
--border-subtle: #f3f4f6;
|
||||
--border-medium: #d1d5db;
|
||||
--bg-hover: #f3f4f6;
|
||||
--bg-hover-alt: #e5e7eb;
|
||||
--bg-pressed: #d1d5db;
|
||||
--border-hover: #d1d5db;
|
||||
--border-active: #9ca3af;
|
||||
|
||||
--foreground-secondary: #334155;
|
||||
--foreground-subtle: #94a3b8;
|
||||
|
||||
--scrollbar-thumb: #c1c1c3;
|
||||
--scrollbar-thumb-hover: #a1a1a3;
|
||||
--scrollbar-track: #f3f4f6;
|
||||
|
||||
--bg-overlay: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--color-dark-bg: #18181b;
|
||||
--color-dark-bg-darker: #0e0e10;
|
||||
--color-dark-bg-darkest: #09090b;
|
||||
--color-dark-bg-input: #222225;
|
||||
--color-dark-bg-button: #23232a;
|
||||
--color-dark-bg-active: #1d1d1f;
|
||||
--color-dark-bg-header: #131316;
|
||||
--color-dark-border: #303032;
|
||||
--color-dark-border-active: #2d2d30;
|
||||
--color-dark-border-hover: #434345;
|
||||
--color-dark-hover: #2d2d30;
|
||||
--color-dark-active: #2a2a2c;
|
||||
--color-dark-pressed: #1a1a1c;
|
||||
--color-dark-hover-alt: #2a2a2d;
|
||||
--color-dark-border-light: #5a5a5d;
|
||||
--color-dark-bg-light: #141416;
|
||||
--color-dark-border-medium: #373739;
|
||||
--color-dark-bg-very-light: #101014;
|
||||
--color-dark-bg-panel: #1b1b1e;
|
||||
--color-dark-border-panel: #222224;
|
||||
--color-dark-bg-panel-hover: #232327;
|
||||
|
||||
--color-canvas: var(--bg-base);
|
||||
--color-elevated: var(--bg-elevated);
|
||||
--color-surface: var(--bg-surface);
|
||||
--color-surface-hover: var(--bg-surface-hover);
|
||||
--color-field: var(--bg-input);
|
||||
--color-deepest: var(--bg-deepest);
|
||||
--color-header: var(--bg-header);
|
||||
--color-button: var(--bg-button);
|
||||
--color-active: var(--bg-active);
|
||||
--color-light: var(--bg-light);
|
||||
--color-subtle: var(--bg-subtle);
|
||||
--color-interact: var(--bg-interact);
|
||||
--color-hover: var(--bg-hover);
|
||||
--color-hover-alt: var(--bg-hover-alt);
|
||||
--color-pressed: var(--bg-pressed);
|
||||
|
||||
--color-edge: var(--border-base);
|
||||
--color-edge-panel: var(--border-panel);
|
||||
--color-edge-subtle: var(--border-subtle);
|
||||
--color-edge-medium: var(--border-medium);
|
||||
--color-edge-hover: var(--border-hover);
|
||||
--color-edge-active: var(--border-active);
|
||||
|
||||
--color-foreground-secondary: var(--foreground-secondary);
|
||||
--color-foreground-subtle: var(--foreground-subtle);
|
||||
|
||||
--color-overlay: var(--bg-overlay);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: #09090b;
|
||||
--foreground: #fafafa;
|
||||
--card: #18181b;
|
||||
--card-foreground: #fafafa;
|
||||
--popover: #27272a;
|
||||
--popover-foreground: #fafafa;
|
||||
--primary: #e4e4e7;
|
||||
--primary-foreground: #27272a;
|
||||
--secondary: #3f3f46;
|
||||
--secondary-foreground: #fafafa;
|
||||
--muted: #27272a;
|
||||
--muted-foreground: #9ca3af;
|
||||
--accent: #3f3f46;
|
||||
--accent-foreground: #fafafa;
|
||||
--destructive: #f87171;
|
||||
--border: #ffffff1a;
|
||||
--input: #ffffff26;
|
||||
--ring: #71717a;
|
||||
--chart-1: #3b82f6;
|
||||
--chart-2: #34d399;
|
||||
--chart-3: #f4a261;
|
||||
--chart-4: #a855f7;
|
||||
--chart-5: #f43f5e;
|
||||
--sidebar: #18181b;
|
||||
--sidebar-foreground: #fafafa;
|
||||
--sidebar-primary: #3b82f6;
|
||||
--sidebar-primary-foreground: #fafafa;
|
||||
--sidebar-accent: #3f3f46;
|
||||
--sidebar-accent-foreground: #fafafa;
|
||||
--sidebar-border: #ffffff1a;
|
||||
--sidebar-ring: #71717a;
|
||||
|
||||
--bg-base: #18181b;
|
||||
--bg-elevated: #0e0e10;
|
||||
--bg-surface: #1b1b1e;
|
||||
--bg-surface-hover: #232327;
|
||||
--bg-input: #222225;
|
||||
--bg-deepest: #09090b;
|
||||
--bg-header: #131316;
|
||||
--bg-button: #23232a;
|
||||
--bg-active: #1d1d1f;
|
||||
--bg-light: #141416;
|
||||
--bg-subtle: #101014;
|
||||
--bg-interact: #2a2a2c;
|
||||
--border-base: #303032;
|
||||
--border-panel: #222224;
|
||||
--border-subtle: #5a5a5d;
|
||||
--border-medium: #373739;
|
||||
--bg-hover: #2d2d30;
|
||||
--bg-hover-alt: #2a2a2d;
|
||||
--bg-pressed: #1a1a1c;
|
||||
--border-hover: #434345;
|
||||
--border-active: #2d2d30;
|
||||
|
||||
--foreground-secondary: #d1d5db;
|
||||
--foreground-subtle: #6b7280;
|
||||
|
||||
--scrollbar-thumb: #434345;
|
||||
--scrollbar-thumb-hover: #5a5a5d;
|
||||
--scrollbar-track: #18181b;
|
||||
|
||||
--bg-overlay: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
|
||||
.dracula {
|
||||
--background: #282a36;
|
||||
--foreground: #ffffff;
|
||||
--card: #343746;
|
||||
--card-foreground: #ffffff;
|
||||
--popover: #343746;
|
||||
--popover-foreground: #ffffff;
|
||||
--primary: #ffffff;
|
||||
--primary-foreground: #282a36;
|
||||
--secondary: #44475a;
|
||||
--secondary-foreground: #ffffff;
|
||||
--muted: #343746;
|
||||
--muted-foreground: #a6accd;
|
||||
--accent: #44475a;
|
||||
--accent-foreground: #ffffff;
|
||||
--destructive: #ff5555;
|
||||
--border: #44475a;
|
||||
--input: #343746;
|
||||
--ring: #8be9fd;
|
||||
--chart-1: #8be9fd;
|
||||
--chart-2: #50fa7b;
|
||||
--chart-3: #ffb86c;
|
||||
--chart-4: #bd93f9;
|
||||
--chart-5: #ff79c6;
|
||||
--sidebar: #1e1f29;
|
||||
--sidebar-foreground: #ffffff;
|
||||
--sidebar-primary: #8be9fd;
|
||||
--sidebar-primary-foreground: #1e1f29;
|
||||
--sidebar-accent: #44475a;
|
||||
--sidebar-accent-foreground: #ffffff;
|
||||
--sidebar-border: #44475a;
|
||||
--sidebar-ring: #8be9fd;
|
||||
|
||||
--bg-base: #282a36;
|
||||
--bg-elevated: #343746;
|
||||
--bg-surface: #343746;
|
||||
--bg-surface-hover: #44475a;
|
||||
--bg-input: #343746;
|
||||
--bg-deepest: #1e1f29;
|
||||
--bg-header: #1e1f29;
|
||||
--bg-button: #343746;
|
||||
--bg-active: #44475a;
|
||||
--bg-light: #343746;
|
||||
--bg-subtle: #1e1f29;
|
||||
--bg-interact: #44475a;
|
||||
--border-base: #44475a;
|
||||
--border-panel: #1e1f29;
|
||||
--border-subtle: #44475a;
|
||||
--border-medium: #44475a;
|
||||
--bg-hover: #44475a;
|
||||
--bg-hover-alt: #44475a;
|
||||
--bg-pressed: #1e1f29;
|
||||
--border-hover: #6272a4;
|
||||
--border-active: #8be9fd;
|
||||
|
||||
--foreground-secondary: #a6accd;
|
||||
--foreground-subtle: #6272a4;
|
||||
|
||||
--scrollbar-thumb: #44475a;
|
||||
--scrollbar-thumb-hover: #6272a4;
|
||||
--scrollbar-track: #282a36;
|
||||
|
||||
--bg-overlay: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.gentlemansChoice {
|
||||
--background: #1a1c1a;
|
||||
--foreground: #d1c7a3;
|
||||
--card: #2a2c2a;
|
||||
--card-foreground: #d1c7a3;
|
||||
--popover: #2a2c2a;
|
||||
--popover-foreground: #d1c7a3;
|
||||
--primary: #d1c7a3;
|
||||
--primary-foreground: #1a1c1a;
|
||||
--secondary: #3e4437;
|
||||
--secondary-foreground: #d1c7a3;
|
||||
--muted: #2a2c2a;
|
||||
--muted-foreground: #8e8463;
|
||||
--accent: #3e4437;
|
||||
--accent-foreground: #d1c7a3;
|
||||
--destructive: #9d3a3a;
|
||||
--border: #3e4437;
|
||||
--input: #2a2c2a;
|
||||
--ring: #5a7a3a;
|
||||
--chart-1: #5a7a3a;
|
||||
--chart-2: #3a5a7a;
|
||||
--chart-3: #b39a3a;
|
||||
--chart-4: #7a3a5a;
|
||||
--chart-5: #3a7a7a;
|
||||
--sidebar: #151715;
|
||||
--sidebar-foreground: #d1c7a3;
|
||||
--sidebar-primary: #5a7a3a;
|
||||
--sidebar-primary-foreground: #151715;
|
||||
--sidebar-accent: #3e4437;
|
||||
--sidebar-accent-foreground: #d1c7a3;
|
||||
--sidebar-border: #3e4437;
|
||||
--sidebar-ring: #5a7a3a;
|
||||
|
||||
--bg-base: #1a1c1a;
|
||||
--bg-elevated: #2a2c2a;
|
||||
--bg-surface: #2a2c2a;
|
||||
--bg-surface-hover: #3e4437;
|
||||
--bg-input: #2a2c2a;
|
||||
--bg-deepest: #151715;
|
||||
--bg-header: #151715;
|
||||
--bg-button: #2a2c2a;
|
||||
--bg-active: #3e4437;
|
||||
--bg-light: #2a2c2a;
|
||||
--bg-subtle: #151715;
|
||||
--bg-interact: #3e4437;
|
||||
--border-base: #3e4437;
|
||||
--border-panel: #151715;
|
||||
--border-subtle: #3e4437;
|
||||
--border-medium: #3e4437;
|
||||
--bg-hover: #3e4437;
|
||||
--bg-hover-alt: #3e4437;
|
||||
--bg-pressed: #151715;
|
||||
--border-hover: #5a7a3a;
|
||||
--border-active: #d1c7a3;
|
||||
|
||||
--foreground-secondary: #8e8463;
|
||||
--foreground-subtle: #5e5841;
|
||||
|
||||
--scrollbar-thumb: #3e4437;
|
||||
--scrollbar-thumb-hover: #5e5841;
|
||||
--scrollbar-track: #1a1c1a;
|
||||
|
||||
--bg-overlay: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.midnightEspresso {
|
||||
--background: #120f0d;
|
||||
--foreground: #ceb195;
|
||||
--card: #1f1a17;
|
||||
--card-foreground: #ceb195;
|
||||
--popover: #1f1a17;
|
||||
--popover-foreground: #ceb195;
|
||||
--primary: #ceb195;
|
||||
--primary-foreground: #120f0d;
|
||||
--secondary: #3d2b1f;
|
||||
--secondary-foreground: #ceb195;
|
||||
--muted: #1f1a17;
|
||||
--muted-foreground: #9a8a7a;
|
||||
--accent: #3d2b1f;
|
||||
--accent-foreground: #ceb195;
|
||||
--destructive: #a05a4a;
|
||||
--border: #3d2b1f;
|
||||
--input: #1f1a17;
|
||||
--ring: #7a8a5a;
|
||||
--chart-1: #7a8a5a;
|
||||
--chart-2: #5a7a9a;
|
||||
--chart-3: #b08a4a;
|
||||
--chart-4: #8a5a7a;
|
||||
--chart-5: #5a8a8a;
|
||||
--sidebar: #0d0b0a;
|
||||
--sidebar-foreground: #ceb195;
|
||||
--sidebar-primary: #7a8a5a;
|
||||
--sidebar-primary-foreground: #0d0b0a;
|
||||
--sidebar-accent: #3d2b1f;
|
||||
--sidebar-accent-foreground: #ceb195;
|
||||
--sidebar-border: #3d2b1f;
|
||||
--sidebar-ring: #7a8a5a;
|
||||
|
||||
--bg-base: #120f0d;
|
||||
--bg-elevated: #1f1a17;
|
||||
--bg-surface: #1f1a17;
|
||||
--bg-surface-hover: #3d2b1f;
|
||||
--bg-input: #1f1a17;
|
||||
--bg-deepest: #0d0b0a;
|
||||
--bg-header: #0d0b0a;
|
||||
--bg-button: #1f1a17;
|
||||
--bg-active: #3d2b1f;
|
||||
--bg-light: #1f1a17;
|
||||
--bg-subtle: #0d0b0a;
|
||||
--bg-interact: #3d2b1f;
|
||||
--border-base: #3d2b1f;
|
||||
--border-panel: #0d0b0a;
|
||||
--border-subtle: #3d2b1f;
|
||||
--border-medium: #3d2b1f;
|
||||
--bg-hover: #3d2b1f;
|
||||
--bg-hover-alt: #3d2b1f;
|
||||
--bg-pressed: #0d0b0a;
|
||||
--border-hover: #7a8a5a;
|
||||
--border-active: #ceb195;
|
||||
|
||||
--foreground-secondary: #9a8a7a;
|
||||
--foreground-subtle: #6a5a4a;
|
||||
|
||||
--scrollbar-thumb: #3d2b1f;
|
||||
--scrollbar-thumb-hover: #6a5a4a;
|
||||
--scrollbar-track: #120f0d;
|
||||
|
||||
--bg-overlay: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.catppuccinMocha {
|
||||
--background: #1e1e2e;
|
||||
--foreground: #cdd6f4;
|
||||
--card: #181825;
|
||||
--card-foreground: #cdd6f4;
|
||||
--popover: #181825;
|
||||
--popover-foreground: #cdd6f4;
|
||||
--primary: #cdd6f4;
|
||||
--primary-foreground: #1e1e2e;
|
||||
--secondary: #313244;
|
||||
--secondary-foreground: #cdd6f4;
|
||||
--muted: #181825;
|
||||
--muted-foreground: #a6adc8;
|
||||
--accent: #313244;
|
||||
--accent-foreground: #cdd6f4;
|
||||
--destructive: #f38ba8;
|
||||
--border: #313244;
|
||||
--input: #181825;
|
||||
--ring: #89b4fa;
|
||||
--chart-1: #89b4fa;
|
||||
--chart-2: #a6e3a1;
|
||||
--chart-3: #f9e2af;
|
||||
--chart-4: #f5c2e7;
|
||||
--chart-5: #94e2d5;
|
||||
--sidebar: #11111b;
|
||||
--sidebar-foreground: #cdd6f4;
|
||||
--sidebar-primary: #89b4fa;
|
||||
--sidebar-primary-foreground: #11111b;
|
||||
--sidebar-accent: #313244;
|
||||
--sidebar-accent-foreground: #cdd6f4;
|
||||
--sidebar-border: #313244;
|
||||
--sidebar-ring: #89b4fa;
|
||||
|
||||
--bg-base: #1e1e2e;
|
||||
--bg-elevated: #181825;
|
||||
--bg-surface: #181825;
|
||||
--bg-surface-hover: #313244;
|
||||
--bg-input: #181825;
|
||||
--bg-deepest: #11111b;
|
||||
--bg-header: #11111b;
|
||||
--bg-button: #181825;
|
||||
--bg-active: #313244;
|
||||
--bg-light: #181825;
|
||||
--bg-subtle: #11111b;
|
||||
--bg-interact: #313244;
|
||||
--border-base: #313244;
|
||||
--border-panel: #11111b;
|
||||
--border-subtle: #313244;
|
||||
--border-medium: #313244;
|
||||
--bg-hover: #313244;
|
||||
--bg-hover-alt: #313244;
|
||||
--bg-pressed: #11111b;
|
||||
--border-hover: #89b4fa;
|
||||
--border-active: #cdd6f4;
|
||||
|
||||
--foreground-secondary: #a6adc8;
|
||||
--foreground-subtle: #7f849c;
|
||||
|
||||
--scrollbar-thumb: #313244;
|
||||
--scrollbar-thumb-hover: #45475a;
|
||||
--scrollbar-track: #1e1e2e;
|
||||
|
||||
--bg-overlay: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
.thin-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-track {
|
||||
background: var(--scrollbar-track);
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.thin-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.skinny-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||
}
|
||||
|
||||
.skinny-scrollbar::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.skinny-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.skinny-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: var(--scrollbar-thumb);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.skinny-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--scrollbar-thumb-hover);
|
||||
}
|
||||
|
||||
.h-fade {
|
||||
mask-image: linear-gradient(transparent, #000 6%, #000 94%, transparent);
|
||||
}
|
||||
@@ -1,53 +1,88 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
import { prepareClientCacheVersion } from "@/lib/client-cache-version";
|
||||
import { StrictMode, Suspense, lazy, useEffect, useState, useRef } from "react";
|
||||
import { StrictMode, Suspense, lazy, useState, useRef, useEffect } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import "./ui/index.css";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import "./i18n/i18n";
|
||||
import "./ui/i18n/i18n";
|
||||
import { isElectron } from "@/lib/electron";
|
||||
import { Toaster } from "@/components/sonner";
|
||||
import { Auth, getStoredAuth, clearStoredAuth } from "@/auth/Auth";
|
||||
import { applyAccentColor, applyFontSize } from "@/lib/theme";
|
||||
import type { FontSizeId } from "@/types/ui-types";
|
||||
import { useServiceWorker } from "@/hooks/use-service-worker";
|
||||
|
||||
const DesktopApp = lazy(() => import("@/ui/desktop/DesktopApp.tsx"));
|
||||
const MobileApp = lazy(() =>
|
||||
import("@/ui/mobile/MobileApp.tsx").then((module) => ({
|
||||
default: module.MobileApp,
|
||||
})),
|
||||
);
|
||||
const HostManagerApp = lazy(
|
||||
() => import("./ui/desktop/apps/host-manager/HostManagerApp.tsx"),
|
||||
);
|
||||
const TerminalApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/terminal/TerminalApp.tsx"),
|
||||
const AppShell = lazy(() =>
|
||||
import("@/AppShell").then((m) => ({ default: m.AppShell })),
|
||||
);
|
||||
|
||||
// Full-screen apps opened via query params (e.g. from external links or Electron)
|
||||
const TerminalApp = lazy(() => import("@/features/terminal/TerminalApp"));
|
||||
const FileManagerApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/file-manager/FileManagerApp.tsx"),
|
||||
);
|
||||
const TunnelApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/tunnel/TunnelApp.tsx"),
|
||||
() => import("@/features/file-manager/FileManagerApp"),
|
||||
);
|
||||
const TunnelApp = lazy(() => import("@/features/tunnel/TunnelApp"));
|
||||
const ServerStatsApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/server-stats/ServerStatsApp.tsx"),
|
||||
);
|
||||
const DockerApp = lazy(
|
||||
() => import("./ui/desktop/apps/features/docker/DockerApp.tsx"),
|
||||
);
|
||||
const GuacamoleApp = lazy(
|
||||
() => import("@/ui/desktop/apps/features/guacamole/GuacamoleApp.tsx"),
|
||||
() => import("@/features/server-stats/ServerStatsApp"),
|
||||
);
|
||||
const DockerApp = lazy(() => import("@/features/docker/DockerApp"));
|
||||
const GuacamoleApp = lazy(() => import("@/features/guacamole/GuacamoleApp"));
|
||||
|
||||
const ElectronVersionCheck = lazy(() =>
|
||||
import("@/ui/desktop/user/ElectronVersionCheck.tsx").then((module) => ({
|
||||
import("@/user/ElectronVersionCheck").then((module) => ({
|
||||
default: module.ElectronVersionCheck,
|
||||
})),
|
||||
);
|
||||
|
||||
const FullscreenApp: React.FC = () => {
|
||||
type Phase = "idle-auth" | "fading-in" | "idle-app" | "fading-out";
|
||||
|
||||
function useWindowWidth() {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const lastSwitchTime = useRef(0);
|
||||
const isCurrentlyMobile = useRef(window.innerWidth < 768);
|
||||
const hasSwitchedOnce = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: number;
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
const newWidth = window.innerWidth;
|
||||
const newIsMobile = newWidth < 768;
|
||||
const now = Date.now();
|
||||
if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
|
||||
setWidth(newWidth);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
newIsMobile !== isCurrentlyMobile.current &&
|
||||
now - lastSwitchTime.current > 5000
|
||||
) {
|
||||
lastSwitchTime.current = now;
|
||||
isCurrentlyMobile.current = newIsMobile;
|
||||
hasSwitchedOnce.current = true;
|
||||
setWidth(newWidth);
|
||||
} else {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
function FullscreenApp() {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const view = searchParams.get("view");
|
||||
const hostId = searchParams.get("hostId");
|
||||
|
||||
switch (view) {
|
||||
case "host-manager":
|
||||
return <HostManagerApp />;
|
||||
case "terminal":
|
||||
return <TerminalApp hostId={hostId || undefined} />;
|
||||
case "file-manager":
|
||||
@@ -63,53 +98,83 @@ const FullscreenApp: React.FC = () => {
|
||||
case "telnet":
|
||||
return <GuacamoleApp hostId={hostId || undefined} />;
|
||||
default:
|
||||
return <DesktopApp />;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
import { useServiceWorker } from "@/hooks/use-service-worker";
|
||||
}
|
||||
|
||||
function useWindowWidth() {
|
||||
const [width, setWidth] = useState(window.innerWidth);
|
||||
const lastSwitchTime = useRef(0);
|
||||
const isCurrentlyMobile = useRef(window.innerWidth < 768);
|
||||
const hasSwitchedOnce = useRef(false);
|
||||
function App() {
|
||||
const stored = getStoredAuth();
|
||||
const [phase, setPhase] = useState<Phase>(
|
||||
stored?.loggedIn ? "idle-app" : "idle-auth",
|
||||
);
|
||||
const [authUsername, setAuthUsername] = useState(stored?.username ?? "");
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const handleResize = () => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => {
|
||||
const newWidth = window.innerWidth;
|
||||
const newIsMobile = newWidth < 768;
|
||||
const now = Date.now();
|
||||
|
||||
if (hasSwitchedOnce.current && now - lastSwitchTime.current < 10000) {
|
||||
setWidth(newWidth);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
newIsMobile !== isCurrentlyMobile.current &&
|
||||
now - lastSwitchTime.current > 5000
|
||||
) {
|
||||
lastSwitchTime.current = now;
|
||||
isCurrentlyMobile.current = newIsMobile;
|
||||
hasSwitchedOnce.current = true;
|
||||
setWidth(newWidth);
|
||||
} else {
|
||||
setWidth(newWidth);
|
||||
}
|
||||
}, 2000);
|
||||
};
|
||||
window.addEventListener("resize", handleResize);
|
||||
|
||||
const savedAccent = localStorage.getItem("termix-accent");
|
||||
if (savedAccent) applyAccentColor(savedAccent);
|
||||
const savedSize = localStorage.getItem(
|
||||
"termix-font-size",
|
||||
) as FontSizeId | null;
|
||||
applyFontSize(savedSize ?? "lg");
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
window.removeEventListener("resize", handleResize);
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return width;
|
||||
function handleLogin(u: string) {
|
||||
setAuthUsername(u);
|
||||
setPhase("fading-in");
|
||||
timerRef.current = setTimeout(() => setPhase("idle-app"), 450);
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
clearStoredAuth();
|
||||
setPhase("fading-out");
|
||||
timerRef.current = setTimeout(() => {
|
||||
setAuthUsername("");
|
||||
setPhase("idle-auth");
|
||||
}, 450);
|
||||
}
|
||||
|
||||
const showApp =
|
||||
phase === "idle-app" || phase === "fading-in" || phase === "fading-out";
|
||||
const showAuth =
|
||||
phase === "idle-auth" || phase === "fading-in" || phase === "fading-out";
|
||||
const appOpacity = phase === "idle-app" ? 1 : 0;
|
||||
const authOpacity = phase === "idle-auth" ? 1 : 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
{showApp && (
|
||||
<div
|
||||
className="fixed inset-0 z-0 transition-opacity duration-[450ms] ease-in-out"
|
||||
style={{
|
||||
opacity: appOpacity,
|
||||
pointerEvents: phase === "idle-app" ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
<Suspense fallback={null}>
|
||||
<AppShell username={authUsername} onLogout={handleLogout} />
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAuth && (
|
||||
<div
|
||||
className="fixed inset-0 z-10 transition-opacity duration-[450ms] ease-in-out"
|
||||
style={{
|
||||
opacity: authOpacity,
|
||||
pointerEvents: phase === "idle-auth" ? "auto" : "none",
|
||||
}}
|
||||
>
|
||||
<Auth onLogin={handleLogin} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Toaster position="bottom-right" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function RootApp() {
|
||||
@@ -125,61 +190,26 @@ function RootApp() {
|
||||
(window as Window & { opera?: string }).opera ||
|
||||
"";
|
||||
const isTermixMobile = /Termix-Mobile/.test(userAgent);
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const isFullscreen = searchParams.has("view");
|
||||
|
||||
const renderApp = () => {
|
||||
if (isFullscreen) {
|
||||
return <FullscreenApp />;
|
||||
}
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<FullscreenApp />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (isElectron()) {
|
||||
return <DesktopApp />;
|
||||
}
|
||||
if (isElectron() && showVersionCheck) {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ElectronVersionCheck onContinue={() => setShowVersionCheck(false)} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
if (isTermixMobile) {
|
||||
return <MobileApp key="mobile" />;
|
||||
}
|
||||
|
||||
return isMobile ? <MobileApp key="mobile" /> : <DesktopApp key="desktop" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFullscreen && (
|
||||
<div
|
||||
className="fixed inset-0 pointer-events-none"
|
||||
style={{
|
||||
backgroundColor: "var(--bg-base)",
|
||||
backgroundImage: `linear-gradient(
|
||||
135deg,
|
||||
transparent 0%,
|
||||
transparent 49%,
|
||||
rgba(128, 128, 128, 0.03) 49%,
|
||||
rgba(128, 128, 128, 0.03) 51%,
|
||||
transparent 51%,
|
||||
transparent 100%
|
||||
)`,
|
||||
backgroundSize: "80px 80px",
|
||||
zIndex: 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div className="relative min-h-screen" style={{ zIndex: 1 }}>
|
||||
{isElectron() && showVersionCheck && !isFullscreen ? (
|
||||
<Suspense fallback={null}>
|
||||
<ElectronVersionCheck
|
||||
onContinue={() => setShowVersionCheck(false)}
|
||||
isAuthenticated={false}
|
||||
/>
|
||||
</Suspense>
|
||||
) : (
|
||||
<Suspense fallback={null}>{renderApp()}</Suspense>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
return <App />;
|
||||
}
|
||||
|
||||
prepareClientCacheVersion().finally(() => {
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
import { Separator } from "@/components/separator";
|
||||
import { Button } from "@/components/button";
|
||||
import { Sheet, SheetContent } from "@/components/sheet";
|
||||
import { ChevronLeft, ChevronRight, Maximize2 } from "lucide-react";
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { MobileBottomBar } from "@/shell/MobileBottomBar";
|
||||
import { CommandPalette } from "@/shell/CommandPalette";
|
||||
import { AppRail } from "@/sidebar/AppRail";
|
||||
import type { RailView } from "@/sidebar/AppRail";
|
||||
import { HostsPanel } from "@/sidebar/HostsPanel";
|
||||
import { QuickConnectPanel } from "@/sidebar/QuickConnectPanel";
|
||||
import { SshToolsPanel } from "@/sidebar/SshToolsPanel";
|
||||
import { SnippetsPanel } from "@/sidebar/SnippetsPanel";
|
||||
import { HistoryPanel } from "@/sidebar/HistoryPanel";
|
||||
import { SplitScreenPanel } from "@/sidebar/SplitScreenPanel";
|
||||
import { UserProfilePanel } from "@/sidebar/UserProfilePanel";
|
||||
import { AdminSettingsPanel } from "@/sidebar/AdminSettingsPanel";
|
||||
import { SplitView } from "@/shell/SplitView";
|
||||
import { TabBar } from "@/shell/TabBar";
|
||||
import type {
|
||||
Tab,
|
||||
TabType,
|
||||
Host,
|
||||
SplitMode,
|
||||
HostFolder,
|
||||
} from "@/types/ui-types";
|
||||
import { getSSHHosts } from "@/main-axios";
|
||||
import type { SSHHostWithStatus } from "@/main-axios";
|
||||
|
||||
function sshHostToHost(h: SSHHostWithStatus): Host {
|
||||
return {
|
||||
id: String(h.id),
|
||||
name: h.name,
|
||||
username: h.username,
|
||||
ip: h.ip,
|
||||
port: h.port,
|
||||
folder: h.folder ?? "",
|
||||
online: h.status === "online",
|
||||
cpu: 0,
|
||||
ram: 0,
|
||||
lastAccess: "",
|
||||
tags: h.tags ?? [],
|
||||
authType: h.authType,
|
||||
password: h.password,
|
||||
key: typeof h.key === "string" ? h.key : undefined,
|
||||
keyPassword: h.keyPassword,
|
||||
keyType: h.keyType,
|
||||
credentialId: h.credentialId != null ? String(h.credentialId) : undefined,
|
||||
notes: h.notes,
|
||||
pin: h.pin ?? false,
|
||||
macAddress: h.macAddress,
|
||||
enableTerminal: h.enableTerminal ?? true,
|
||||
enableTunnel: h.enableTunnel ?? false,
|
||||
enableFileManager: h.enableFileManager ?? false,
|
||||
enableDocker: h.enableDocker ?? false,
|
||||
enableSsh: h.connectionType === "ssh" || !h.connectionType,
|
||||
enableRdp: h.connectionType === "rdp",
|
||||
enableVnc: h.connectionType === "vnc",
|
||||
enableTelnet: h.connectionType === "telnet",
|
||||
sshPort: h.port,
|
||||
rdpPort: 3389,
|
||||
vncPort: 5900,
|
||||
telnetPort: 23,
|
||||
quickActions: (h.quickActions ?? []).map((a) => ({
|
||||
name: a.name,
|
||||
snippetId: String(a.snippetId),
|
||||
})),
|
||||
serverTunnels: [],
|
||||
defaultPath: h.defaultPath,
|
||||
terminalConfig: h.terminalConfig as Host["terminalConfig"],
|
||||
useSocks5: h.useSocks5,
|
||||
socks5Host: h.socks5Host,
|
||||
socks5Port: h.socks5Port,
|
||||
socks5Username: h.socks5Username,
|
||||
socks5Password: h.socks5Password,
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostTree(hosts: SSHHostWithStatus[]): HostFolder {
|
||||
const root: HostFolder = { name: "root", children: [] };
|
||||
const folderMap = new Map<string, HostFolder>();
|
||||
const getOrCreateFolder = (path: string): HostFolder => {
|
||||
if (folderMap.has(path)) return folderMap.get(path)!;
|
||||
const parts = path.split(" / ");
|
||||
let current = root;
|
||||
let accumulated = "";
|
||||
for (const part of parts) {
|
||||
accumulated = accumulated ? `${accumulated} / ${part}` : part;
|
||||
if (!folderMap.has(accumulated)) {
|
||||
const folder: HostFolder = { name: part, children: [] };
|
||||
folderMap.set(accumulated, folder);
|
||||
current.children.push(folder);
|
||||
}
|
||||
current = folderMap.get(accumulated)!;
|
||||
}
|
||||
return current;
|
||||
};
|
||||
for (const h of hosts) {
|
||||
const host = sshHostToHost(h);
|
||||
if (h.folder) {
|
||||
getOrCreateFolder(h.folder).children.push(host);
|
||||
} else {
|
||||
root.children.push(host);
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
export { tabIcon, renderTabContent } from "@/shell/tabUtils";
|
||||
import { renderTabContent } from "@/shell/tabUtils";
|
||||
|
||||
const DASHBOARD_TAB: Tab = {
|
||||
id: "dashboard",
|
||||
type: "dashboard",
|
||||
label: "Dashboard",
|
||||
};
|
||||
|
||||
const SINGLETON_TAB_LABELS: Partial<Record<TabType, string>> = {
|
||||
"host-manager": "Host Manager",
|
||||
docker: "Docker",
|
||||
tunnel: "Tunnels",
|
||||
};
|
||||
|
||||
// ─── AppShell ────────────────────────────────────────────────────────────────
|
||||
|
||||
export function AppShell({
|
||||
username,
|
||||
onLogout,
|
||||
}: {
|
||||
username: string;
|
||||
onLogout: () => void;
|
||||
}) {
|
||||
const [tabs, setTabs] = useState<Tab[]>([DASHBOARD_TAB]);
|
||||
const [activeTabId, setActiveTabId] = useState("dashboard");
|
||||
const [commandPaletteOpen, setCommandPaletteOpen] = useState(false);
|
||||
const [splitMode, setSplitMode] = useState<SplitMode>("none");
|
||||
const [paneTabIds, setPaneTabIds] = useState<(string | null)[]>(
|
||||
Array(6).fill(null),
|
||||
);
|
||||
const [realHostTree, setRealHostTree] = useState<HostFolder | null>(null);
|
||||
const [allHosts, setAllHosts] = useState<Host[]>([]);
|
||||
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [railView, setRailView] = useState<RailView>("hosts");
|
||||
const [profileDropdownOpen, setProfileDropdownOpen] = useState(false);
|
||||
const [hostManagerExpanded, setHostManagerExpanded] = useState(false);
|
||||
const [sidebarWidth, setSidebarWidth] = useState(256);
|
||||
const [sidebarDragging, setSidebarDragging] = useState(false);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// Close the sidebar when switching to mobile (it becomes a sheet overlay)
|
||||
useEffect(() => {
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}, [isMobile]);
|
||||
|
||||
const pendingHostManagerEditId = useRef<string | null>(null);
|
||||
const pendingHostManagerAction = useRef<"add-host" | "add-credential" | null>(
|
||||
null,
|
||||
);
|
||||
const lastShiftTime = useRef(0);
|
||||
|
||||
const sidebarTitle: Record<RailView, string> = {
|
||||
hosts: "Hosts",
|
||||
"quick-connect": "Quick Connect",
|
||||
"ssh-tools": "SSH Tools",
|
||||
snippets: "Snippets",
|
||||
history: "History",
|
||||
"split-screen": "Split Screen",
|
||||
"user-profile": "User Profile",
|
||||
"admin-settings": "Admin Settings",
|
||||
};
|
||||
|
||||
// Double-shift opens command palette
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "ShiftLeft") {
|
||||
const now = Date.now();
|
||||
if (now - lastShiftTime.current < 300)
|
||||
setCommandPaletteOpen((prev) => !prev);
|
||||
lastShiftTime.current = now;
|
||||
}
|
||||
};
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const handle = () => onLogout();
|
||||
window.addEventListener("termix:logout", handle);
|
||||
return () => window.removeEventListener("termix:logout", handle);
|
||||
}, [onLogout]);
|
||||
|
||||
// Load real hosts from API
|
||||
const loadHosts = useCallback(async () => {
|
||||
try {
|
||||
const raw = await getSSHHosts();
|
||||
const converted = raw.map(sshHostToHost);
|
||||
setAllHosts(converted);
|
||||
setRealHostTree(buildHostTree(raw));
|
||||
} catch {
|
||||
// Keep empty state on error
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadHosts();
|
||||
}, [loadHosts]);
|
||||
|
||||
// Sync tab host data when allHosts updates (e.g. after editing terminal theme in host settings)
|
||||
useEffect(() => {
|
||||
if (allHosts.length === 0) return;
|
||||
setTabs((prev) =>
|
||||
prev.map((t) =>
|
||||
t.host
|
||||
? { ...t, host: allHosts.find((h) => h.id === t.host!.id) ?? t.host }
|
||||
: t,
|
||||
),
|
||||
);
|
||||
}, [allHosts]);
|
||||
|
||||
// Let HostManager trigger tab opens via custom event
|
||||
useEffect(() => {
|
||||
const handle = (e: Event) => {
|
||||
const { hostId, type } = (
|
||||
e as CustomEvent<{ hostId: string; type?: TabType }>
|
||||
).detail;
|
||||
const host = allHosts.find((h) => h.id === hostId);
|
||||
if (host) connectHost(host, type);
|
||||
};
|
||||
window.addEventListener("termix:open-tab", handle);
|
||||
return () => window.removeEventListener("termix:open-tab", handle);
|
||||
}, [tabs, allHosts]);
|
||||
|
||||
// ─── Tab management ──────────────────────────────────────────────────────
|
||||
|
||||
function openTab(host: Host, type: TabType) {
|
||||
setTabs((prev) => {
|
||||
const same = prev.filter(
|
||||
(t) =>
|
||||
t.type === type && t.label.replace(/ \(\d+\)$/, "") === host.name,
|
||||
);
|
||||
if (same.length === 0) {
|
||||
const tab = {
|
||||
id: `${host.name}-${type}`,
|
||||
type,
|
||||
label: host.name,
|
||||
host,
|
||||
};
|
||||
setActiveTabId(tab.id);
|
||||
return [...prev, tab];
|
||||
}
|
||||
const next = prev.map((t) =>
|
||||
t.id === same[0].id && !/\(\d+\)$/.test(t.label)
|
||||
? { ...t, label: `${host.name} (1)`, host }
|
||||
: t,
|
||||
);
|
||||
const tab = {
|
||||
id: `${host.name}-${type}-${Date.now()}`,
|
||||
type,
|
||||
label: `${host.name} (${same.length + 1})`,
|
||||
host,
|
||||
};
|
||||
setActiveTabId(tab.id);
|
||||
return [...next, tab];
|
||||
});
|
||||
}
|
||||
|
||||
function connectHost(host: Host, preferredType?: TabType) {
|
||||
const type: TabType =
|
||||
preferredType ??
|
||||
(host.enableSsh
|
||||
? "terminal"
|
||||
: host.enableRdp
|
||||
? "rdp"
|
||||
: host.enableVnc
|
||||
? "vnc"
|
||||
: host.enableTelnet
|
||||
? "telnet"
|
||||
: "terminal");
|
||||
openTab(host, type);
|
||||
}
|
||||
|
||||
function openSingletonTab(type: TabType, pendingEvent?: string) {
|
||||
if (type === "host-manager") {
|
||||
if (pendingEvent === "host-manager:add-host")
|
||||
pendingHostManagerAction.current = "add-host";
|
||||
else if (pendingEvent === "host-manager:add-credential")
|
||||
pendingHostManagerAction.current = "add-credential";
|
||||
|
||||
setHostManagerExpanded(true);
|
||||
setSidebarOpen(true);
|
||||
setRailView("hosts");
|
||||
|
||||
if (pendingEvent) {
|
||||
// Use a small delay to ensure HostManager is mounted if it wasn't already,
|
||||
// and to allow the current render cycle to complete.
|
||||
setTimeout(() => {
|
||||
window.dispatchEvent(new CustomEvent(pendingEvent));
|
||||
}, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (type === "user-profile" || type === "admin-settings") {
|
||||
handleRailClick(type as RailView);
|
||||
return;
|
||||
}
|
||||
const id = type;
|
||||
setTabs((prev) => {
|
||||
if (prev.find((t) => t.id === id)) return prev;
|
||||
return [...prev, { id, type, label: SINGLETON_TAB_LABELS[type] ?? type }];
|
||||
});
|
||||
setActiveTabId(id);
|
||||
}
|
||||
|
||||
function closeTab(id: string) {
|
||||
setTabs((prev) => {
|
||||
const next = prev.filter((t) => t.id !== id);
|
||||
if (id === activeTabId) setActiveTabId(next[next.length - 1].id);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Rail / sidebar ──────────────────────────────────────────────────────
|
||||
|
||||
function handleRailClick(view: RailView) {
|
||||
if (railView === view && sidebarOpen) {
|
||||
setSidebarOpen(false);
|
||||
} else {
|
||||
setRailView(view);
|
||||
setSidebarOpen(true);
|
||||
}
|
||||
}
|
||||
|
||||
function editHostInManager(host: Host) {
|
||||
pendingHostManagerEditId.current = host.id;
|
||||
setHostManagerExpanded(true);
|
||||
setSidebarOpen(true);
|
||||
setRailView("hosts");
|
||||
}
|
||||
|
||||
const onSidebarMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setSidebarDragging(true);
|
||||
const startX = e.clientX;
|
||||
const startW = sidebarWidth;
|
||||
function onMove(ev: MouseEvent) {
|
||||
setSidebarWidth(
|
||||
Math.max(160, Math.min(480, startW + ev.clientX - startX)),
|
||||
);
|
||||
}
|
||||
function onUp() {
|
||||
setSidebarDragging(false);
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
}
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
},
|
||||
[sidebarWidth],
|
||||
);
|
||||
|
||||
const activeTab = tabs.find((t) => t.id === activeTabId)!;
|
||||
const isSplit = splitMode !== "none";
|
||||
const terminalTabs = tabs.filter((t) => t.type === "terminal");
|
||||
|
||||
// Sidebar panel content — shared between desktop inline sidebar and mobile sheet
|
||||
const sidebarPanelContent = (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
{railView === "hosts" && (
|
||||
<HostsPanel
|
||||
expanded={hostManagerExpanded}
|
||||
onExpand={() => setHostManagerExpanded(true)}
|
||||
onCollapse={() => {
|
||||
setHostManagerExpanded(false);
|
||||
loadHosts();
|
||||
}}
|
||||
pendingEditId={pendingHostManagerEditId}
|
||||
pendingAction={pendingHostManagerAction}
|
||||
onOpenTab={(host, type) => {
|
||||
connectHost(host, type);
|
||||
if (isMobile) setSidebarOpen(false);
|
||||
}}
|
||||
onEditHost={editHostInManager}
|
||||
hostTree={realHostTree ?? undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
{railView === "quick-connect" && <QuickConnectPanel />}
|
||||
|
||||
{railView === "ssh-tools" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<SshToolsPanel
|
||||
terminalTabs={terminalTabs}
|
||||
activeTabId={activeTabId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "snippets" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<SnippetsPanel
|
||||
terminalTabs={terminalTabs}
|
||||
activeTabId={activeTabId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "history" && (
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-y-auto">
|
||||
<HistoryPanel terminalTabs={terminalTabs} activeTabId={activeTabId} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "split-screen" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<SplitScreenPanel
|
||||
tabs={tabs}
|
||||
splitMode={splitMode}
|
||||
setSplitMode={setSplitMode}
|
||||
paneTabIds={paneTabIds}
|
||||
setPaneTabIds={setPaneTabIds}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "user-profile" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<UserProfilePanel username={username} onLogout={onLogout} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{railView === "admin-settings" && (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto">
|
||||
<AdminSettingsPanel />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Sidebar header — shared
|
||||
const sidebarHeader = (
|
||||
<div className="flex flex-row items-center border-b border-border h-12.5 shrink-0">
|
||||
<span className="flex-1 text-base font-bold tracking-tight text-foreground px-3">
|
||||
{sidebarTitle[railView]}
|
||||
</span>
|
||||
{!hostManagerExpanded && !isMobile && (
|
||||
<>
|
||||
<Separator orientation="vertical" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-full w-12.5 rounded-none text-muted-foreground hover:text-foreground"
|
||||
title="Reset width"
|
||||
onClick={() => setSidebarWidth(256)}
|
||||
>
|
||||
<Maximize2 className="size-3.5" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Separator orientation="vertical" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-full w-12.5 rounded-none text-muted-foreground hover:text-foreground"
|
||||
onClick={() => {
|
||||
setSidebarOpen(false);
|
||||
setHostManagerExpanded(false);
|
||||
}}
|
||||
>
|
||||
<ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex w-screen bg-background" style={{ height: "100dvh" }}>
|
||||
{/* Skinny icon rail — desktop only, hidden on mobile */}
|
||||
<AppRail
|
||||
railView={railView}
|
||||
sidebarOpen={sidebarOpen}
|
||||
splitMode={splitMode}
|
||||
username={username}
|
||||
profileDropdownOpen={profileDropdownOpen}
|
||||
onProfileDropdownChange={setProfileDropdownOpen}
|
||||
onRailClick={handleRailClick}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
|
||||
{/* Desktop: inline resizable sidebar */}
|
||||
{!isMobile && (
|
||||
<div
|
||||
className={`relative flex flex-col bg-sidebar shrink-0 overflow-hidden ${sidebarOpen ? `border-r transition-colors ${sidebarDragging ? "border-accent-brand/60" : "border-border"}` : ""}`}
|
||||
style={{
|
||||
width: sidebarOpen
|
||||
? hostManagerExpanded && railView === "hosts"
|
||||
? 720
|
||||
: sidebarWidth
|
||||
: 0,
|
||||
transition: sidebarDragging ? "none" : "width 0.2s",
|
||||
}}
|
||||
>
|
||||
{sidebarHeader}
|
||||
{sidebarPanelContent}
|
||||
|
||||
{sidebarOpen && !(hostManagerExpanded && railView === "hosts") && (
|
||||
<div
|
||||
onMouseDown={onSidebarMouseDown}
|
||||
className={`absolute right-0 top-0 bottom-0 w-1 cursor-col-resize z-30 transition-colors ${sidebarDragging ? "bg-accent-brand/60" : "hover:bg-accent-brand/40"}`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mobile: sidebar as overlay sheet */}
|
||||
{isMobile && (
|
||||
<Sheet
|
||||
open={sidebarOpen}
|
||||
onOpenChange={(open) => {
|
||||
setSidebarOpen(open);
|
||||
if (!open) setHostManagerExpanded(false);
|
||||
}}
|
||||
>
|
||||
<SheetContent
|
||||
side="left"
|
||||
showCloseButton={false}
|
||||
className="p-0 flex flex-col w-[min(85vw,360px)] max-w-full bg-sidebar border-r border-border gap-0"
|
||||
style={{ height: "100dvh" }}
|
||||
>
|
||||
{sidebarHeader}
|
||||
{sidebarPanelContent}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)}
|
||||
|
||||
{/* Main content area */}
|
||||
<div
|
||||
className={`relative flex flex-col flex-1 min-w-0 overflow-hidden transition-all duration-200 ${!isMobile && !sidebarOpen ? "pl-6" : ""}`}
|
||||
>
|
||||
{!isMobile && !sidebarOpen && (
|
||||
<button
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
title="Open Sidebar"
|
||||
className="absolute left-0 top-0 bottom-0 z-20 flex items-center justify-center w-6 bg-sidebar border-r border-border text-muted-foreground hover:text-accent-brand hover:bg-accent-brand/5 transition-colors"
|
||||
>
|
||||
<ChevronRight className="size-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex flex-col flex-1 min-w-0 min-h-0 overflow-hidden">
|
||||
<TabBar
|
||||
tabs={tabs}
|
||||
activeTabId={activeTabId}
|
||||
onSetActiveTab={setActiveTabId}
|
||||
onCloseTab={closeTab}
|
||||
onReorderTabs={setTabs}
|
||||
/>
|
||||
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
||||
{isSplit && !isMobile ? (
|
||||
<SplitView
|
||||
tabs={tabs}
|
||||
paneTabIds={paneTabIds}
|
||||
splitMode={splitMode}
|
||||
onOpenSingletonTab={openSingletonTab}
|
||||
onOpenTab={openTab}
|
||||
/>
|
||||
) : activeTab ? (
|
||||
renderTabContent(activeTab, openSingletonTab, openTab)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom nav bar — mobile only */}
|
||||
<MobileBottomBar
|
||||
railView={railView}
|
||||
sidebarOpen={sidebarOpen}
|
||||
splitMode={splitMode}
|
||||
onRailClick={handleRailClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CommandPalette
|
||||
isOpen={commandPaletteOpen}
|
||||
setIsOpen={setCommandPaletteOpen}
|
||||
hosts={allHosts}
|
||||
onOpenTab={(type, label, pendingEvent) => {
|
||||
if (
|
||||
[
|
||||
"dashboard",
|
||||
"host-manager",
|
||||
"user-profile",
|
||||
"admin-settings",
|
||||
"docker",
|
||||
"tunnel",
|
||||
].includes(type)
|
||||
) {
|
||||
openSingletonTab(type, pendingEvent);
|
||||
} else if (label) {
|
||||
const host = allHosts.find((h) => h.name === label);
|
||||
if (host) openTab(host, type);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from "react";
|
||||
import { useSidebar } from "@/components/ui/sidebar.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { useSidebar } from "@/components/sidebar.tsx";
|
||||
import { Separator } from "@/components/separator.tsx";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs.tsx";
|
||||
} from "@/components/tabs.tsx";
|
||||
import { Shield, Users, Database, Clock, Key } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -21,15 +21,15 @@ import {
|
||||
isElectron,
|
||||
getSessions,
|
||||
unlinkOIDCFromPasswordAccount,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { SimpleLoader } from "@/ui/desktop/navigation/animations/SimpleLoader.tsx";
|
||||
import { RolesTab } from "@/ui/desktop/apps/admin/tabs/RolesTab.tsx";
|
||||
import { GeneralSettingsTab } from "@/ui/desktop/apps/admin/tabs/GeneralSettingsTab.tsx";
|
||||
import { OIDCSettingsTab } from "@/ui/desktop/apps/admin/tabs/OIDCSettingsTab.tsx";
|
||||
import { UserManagementTab } from "@/ui/desktop/apps/admin/tabs/UserManagementTab.tsx";
|
||||
import { SessionManagementTab } from "@/ui/desktop/apps/admin/tabs/SessionManagementTab.tsx";
|
||||
import { DatabaseSecurityTab } from "@/ui/desktop/apps/admin/tabs/DatabaseSecurityTab.tsx";
|
||||
import { ApiKeysTab } from "@/ui/desktop/apps/admin/tabs/ApiKeysTab.tsx";
|
||||
} from "@/main-axios.ts";
|
||||
import { SimpleLoader } from "@/lib/SimpleLoader.tsx";
|
||||
import { RolesTab } from "@/admin/tabs/RolesTab.tsx";
|
||||
import { GeneralSettingsTab } from "@/admin/tabs/GeneralSettingsTab.tsx";
|
||||
import { OIDCSettingsTab } from "@/admin/tabs/OIDCSettingsTab.tsx";
|
||||
import { UserManagementTab } from "@/admin/tabs/UserManagementTab.tsx";
|
||||
import { SessionManagementTab } from "@/admin/tabs/SessionManagementTab.tsx";
|
||||
import { DatabaseSecurityTab } from "@/admin/tabs/DatabaseSecurityTab.tsx";
|
||||
import { ApiKeysTab } from "@/admin/tabs/ApiKeysTab.tsx";
|
||||
import { CreateUserDialog } from "./dialogs/CreateUserDialog.tsx";
|
||||
import { UserEditDialog } from "./dialogs/UserEditDialog.tsx";
|
||||
import { LinkAccountDialog } from "./dialogs/LinkAccountDialog.tsx";
|
||||
@@ -6,16 +6,16 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
} from "@/components/dialog.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { PasswordInput } from "@/components/password-input.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { UserPlus, AlertCircle } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { registerUser } from "@/ui/main-axios.ts";
|
||||
import { registerUser } from "@/main-axios.ts";
|
||||
|
||||
interface CreateUserDialogProps {
|
||||
open: boolean;
|
||||
@@ -6,15 +6,15 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
} from "@/components/dialog.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { linkOIDCToPasswordAccount } from "@/ui/main-axios.ts";
|
||||
import { linkOIDCToPasswordAccount } from "@/main-axios.ts";
|
||||
|
||||
interface LinkAccountDialogProps {
|
||||
open: boolean;
|
||||
@@ -5,13 +5,13 @@ import {
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
import { Switch } from "@/components/ui/switch.tsx";
|
||||
import { Separator } from "@/components/ui/separator.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
} from "@/components/dialog.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Badge } from "@/components/badge.tsx";
|
||||
import { Switch } from "@/components/switch.tsx";
|
||||
import { Separator } from "@/components/separator.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
UserCog,
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
deleteUser,
|
||||
type UserRole,
|
||||
type Role,
|
||||
} from "@/ui/main-axios.ts";
|
||||
} from "@/main-axios.ts";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table.tsx";
|
||||
} from "@/components/table.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -15,12 +15,12 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
} from "@/components/dialog.tsx";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover.tsx";
|
||||
} from "@/components/popover.tsx";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -28,11 +28,11 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
} from "@/components/command.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Badge } from "@/components/badge.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import {
|
||||
Key,
|
||||
Plus,
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
getUserList,
|
||||
type ApiKey,
|
||||
type CreatedApiKey,
|
||||
} from "@/ui/main-axios.ts";
|
||||
} from "@/main-axios.ts";
|
||||
|
||||
interface UserOption {
|
||||
id: string;
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Download, Upload } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { isElectron } from "@/ui/main-axios.ts";
|
||||
import { isElectron } from "@/main-axios.ts";
|
||||
import { getBasePath } from "@/lib/base-path";
|
||||
|
||||
export function DatabaseSecurityTab(): React.ReactElement {
|
||||
@@ -1,13 +1,13 @@
|
||||
import React from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Checkbox } from "@/components/checkbox.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select.tsx";
|
||||
} from "@/components/select.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
@@ -23,8 +23,8 @@ import {
|
||||
updateLogLevel,
|
||||
getSessionTimeout,
|
||||
updateSessionTimeout,
|
||||
} from "@/ui/main-axios.ts";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
} from "@/main-axios.ts";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
|
||||
interface GeneralSettingsTabProps {
|
||||
allowRegistration: boolean;
|
||||
@@ -1,14 +1,14 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { PasswordInput } from "@/components/password-input.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { updateOIDCConfig, disableOIDCConfig } from "@/ui/main-axios.ts";
|
||||
import { Textarea } from "@/components/textarea.tsx";
|
||||
import { updateOIDCConfig, disableOIDCConfig } from "@/main-axios.ts";
|
||||
|
||||
interface OIDCSettingsTabProps {
|
||||
allowPasswordLogin: boolean;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog.tsx";
|
||||
} from "@/components/dialog.tsx";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -15,11 +15,11 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Textarea } from "@/components/ui/textarea.tsx";
|
||||
import { Badge } from "@/components/ui/badge.tsx";
|
||||
} from "@/components/table.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Textarea } from "@/components/textarea.tsx";
|
||||
import { Badge } from "@/components/badge.tsx";
|
||||
import { Shield, Plus, Edit, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
updateRole,
|
||||
deleteRole,
|
||||
type Role,
|
||||
} from "@/ui/main-axios.ts";
|
||||
} from "@/main-axios.ts";
|
||||
|
||||
export function RolesTab(): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table.tsx";
|
||||
} from "@/components/table.tsx";
|
||||
import { Monitor, Smartphone, Globe, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import { revokeSession, revokeAllUserSessions } from "@/ui/main-axios.ts";
|
||||
import { revokeSession, revokeAllUserSessions } from "@/main-axios.ts";
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table.tsx";
|
||||
} from "@/components/table.tsx";
|
||||
import { UserPlus, Edit, Trash2, Link2, Unlink } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { useConfirmation } from "@/hooks/use-confirmation.ts";
|
||||
import { deleteUser } from "@/ui/main-axios.ts";
|
||||
import { deleteUser } from "@/main-axios.ts";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AlertCircle, Loader2, ArrowLeft, RefreshCw } from "lucide-react";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Alert, AlertTitle, AlertDescription } from "@/components/alert.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getServerConfig,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
getEmbeddedServerStatus,
|
||||
setEmbeddedMode,
|
||||
type ServerConfig,
|
||||
} from "@/ui/main-axios.ts";
|
||||
} from "@/main-axios.ts";
|
||||
import { Server, Monitor, Loader2 } from "lucide-react";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import React, { useState, useEffect, useCallback } from "react";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { PasswordInput } from "@/components/ui/password-input.tsx";
|
||||
import { Label } from "@/components/ui/label.tsx";
|
||||
import { Checkbox } from "@/components/ui/checkbox.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx";
|
||||
import { Button } from "@/components/button.tsx";
|
||||
import { Input } from "@/components/input.tsx";
|
||||
import { PasswordInput } from "@/components/password-input.tsx";
|
||||
import { Label } from "@/components/label.tsx";
|
||||
import { Checkbox } from "@/components/checkbox.tsx";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/alert.tsx";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/tabs.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx";
|
||||
import { LanguageSwitcher } from "@/user/LanguageSwitcher.tsx";
|
||||
import { toast } from "sonner";
|
||||
import { Sun, Moon, Monitor } from "lucide-react";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
@@ -28,9 +28,9 @@ import {
|
||||
saveServerConfig,
|
||||
isElectron,
|
||||
getEmbeddedServerStatus,
|
||||
} from "../../main-axios.ts";
|
||||
import { ElectronServerConfig as ServerConfigComponent } from "@/ui/desktop/authentication/ElectronServerConfig.tsx";
|
||||
import { ElectronLoginForm } from "@/ui/desktop/authentication/ElectronLoginForm.tsx";
|
||||
} from "@/main-axios";
|
||||
import { ElectronServerConfig as ServerConfigComponent } from "@/auth/ElectronServerConfig.tsx";
|
||||
import { ElectronLoginForm } from "@/auth/ElectronLoginForm.tsx";
|
||||
|
||||
function isMissingServerConfigError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) {
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { buttonVariants } from "@/components/button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
@@ -1,6 +1,11 @@
|
||||
import { Children, ReactElement, cloneElement, isValidElement } from "react";
|
||||
import {
|
||||
Children,
|
||||
type ReactElement,
|
||||
cloneElement,
|
||||
isValidElement,
|
||||
} from "react";
|
||||
|
||||
import { type ButtonProps } from "@/components/ui/button";
|
||||
import { type ButtonProps } from "@/components/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ButtonGroupProps {
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group/button inline-flex shrink-0 items-center justify-center rounded-none border border-transparent bg-clip-padding text-xs font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm": "size-7 rounded-none",
|
||||
"icon-lg": "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
data-variant={variant}
|
||||
data-size={size}
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type ButtonProps = React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & { asChild?: boolean };
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -2,12 +2,17 @@ import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"bg-elevated text-foreground flex flex-col gap-6 rounded-lg border-2 border-edge py-6 shadow-sm",
|
||||
"group/card flex flex-col gap-4 overflow-hidden rounded-none bg-card py-4 text-xs/relaxed text-card-foreground border border-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -20,7 +25,7 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -32,7 +37,10 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn("leading-none font-semibold", className)}
|
||||
className={cn(
|
||||
"font-heading text-sm font-medium group-data-[size=sm]/card:text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -42,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-xs/relaxed text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -65,7 +73,7 @@ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-6", className)}
|
||||
className={cn("px-4 group-data-[size=sm]/card:px-3", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -75,7 +83,10 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn(
|
||||
"flex items-center rounded-none border-t p-4 group-data-[size=sm]/card:p-3",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,130 @@
|
||||
import * as React from "react";
|
||||
import { Search } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-none bg-popover text-popover-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = "Command";
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
React.InputHTMLAttributes<HTMLInputElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className="flex items-center border-b border-border px-3"
|
||||
cm-input-wrapper=""
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-none bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
CommandInput.displayName = "CommandInput";
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandList.displayName = "CommandList";
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { heading?: string }
|
||||
>(({ className, heading, children, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden p-1 text-foreground [&_[cm-group-heading]]:px-2 [&_[cm-group-heading]]:py-1.5 [&_[cm-group-heading]]:text-xs [&_[cm-group-heading]]:font-medium [&_[cm-group-heading]]:text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{heading && <div cm-group-heading="">{heading}</div>}
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
CommandGroup.displayName = "CommandGroup";
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
|
||||
));
|
||||
CommandSeparator.displayName = "CommandSeparator";
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & { onSelect?: () => void }
|
||||
>(({ className, onSelect, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
onClick={onSelect}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-muted hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 transition-colors",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandItem.displayName = "CommandItem";
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("py-6 text-center text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandEmpty.displayName = "CommandEmpty";
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandSeparator,
|
||||
CommandEmpty,
|
||||
CommandShortcut,
|
||||
};
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/button";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
@@ -36,7 +37,7 @@ function DialogOverlay({
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-overlay",
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -53,24 +54,27 @@ function DialogContent({
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-none bg-popover p-4 text-xs/relaxed text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<DialogPrimitive.Close data-slot="dialog-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
@@ -82,13 +86,20 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
className={cn("flex flex-col gap-1 text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
@@ -97,7 +108,14 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,7 +126,7 @@ function DialogTitle({
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
className={cn("font-heading text-sm font-medium", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -121,7 +139,10 @@ function DialogDescription({
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn(
|
||||
"text-xs/relaxed text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,271 @@
|
||||
import * as React from "react";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ChevronRightIcon } from "lucide-react";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
align = "start",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
align={align}
|
||||
className={cn(
|
||||
"z-50 max-h-(--radix-dropdown-menu-content-available-height) w-(--radix-dropdown-menu-trigger-width) min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-border duration-100 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 data-[state=closed]:overflow-hidden data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 data-[variant=destructive]:*:[svg]:text-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-checkbox-item-indicator"
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground data-inset:pl-7 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="pointer-events-none absolute right-2 flex items-center justify-center"
|
||||
data-slot="dropdown-menu-radio-item-indicator"
|
||||
>
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-2 text-xs text-muted-foreground data-inset:pl-7",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("-mx-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded-none px-2 py-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-7 data-open:bg-accent data-open:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"z-50 min-w-[96px] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-none bg-popover text-popover-foreground shadow-lg ring-1 ring-border duration-100 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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Label } from "@/components/label";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface KbdProps extends React.HTMLAttributes<HTMLElement> {}
|
||||
|
||||
const Kbd = React.forwardRef<HTMLElement, KbdProps>(
|
||||
({ className, ...props }, ref) => {
|
||||
return (
|
||||
<kbd
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded-none border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Kbd.displayName = "Kbd";
|
||||
|
||||
const KbdKey = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span className={cn("", className)} {...props} />
|
||||
);
|
||||
KbdKey.displayName = "KbdKey";
|
||||
|
||||
const KbdSeparator = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => (
|
||||
<span className={cn("text-muted-foreground/50", className)} {...props}>
|
||||
+
|
||||
</span>
|
||||
);
|
||||
KbdSeparator.displayName = "KbdSeparator";
|
||||
|
||||
export { Kbd, KbdKey, KbdSeparator };
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { Eye, EyeOff } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Input } from "@/components/input";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PasswordInputProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from "react";
|
||||
import type React from "react";
|
||||
|
||||
export function SectionCard({
|
||||
title,
|
||||
icon,
|
||||
action,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
action?: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-col border border-border bg-card">
|
||||
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border shrink-0">
|
||||
<span className="text-muted-foreground">{icon}</span>
|
||||
<span className="text-xs font-semibold uppercase tracking-widest text-muted-foreground flex-1">
|
||||
{title}
|
||||
</span>
|
||||
{action && <div className="ml-auto">{action}</div>}
|
||||
</div>
|
||||
<div className="px-3 md:px-4 py-1">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SettingRow({
|
||||
label,
|
||||
badge,
|
||||
description,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
badge?: string;
|
||||
description?: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between py-3 border-b border-border last:border-0">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-sm font-medium">{label}</span>
|
||||
{badge && (
|
||||
<span className="text-[10px] font-bold text-yellow-500 border border-yellow-500/40 px-1">
|
||||
{badge}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{description && (
|
||||
<span className="text-xs text-muted-foreground">{description}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="shrink-0 ml-4 md:ml-8">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FakeSwitch({
|
||||
defaultChecked = false,
|
||||
onChange,
|
||||
}: {
|
||||
defaultChecked?: boolean;
|
||||
onChange?: (v: boolean) => void;
|
||||
}) {
|
||||
const [on, setOn] = useState(defaultChecked);
|
||||
return (
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = !on;
|
||||
setOn(next);
|
||||
onChange?.(next);
|
||||
}}
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center border-2 transition-colors ${on ? "bg-accent-brand border-accent-brand" : "bg-muted border-border"}`}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-3 w-3 bg-background shadow-sm transition-transform ${on ? "translate-x-4" : "translate-x-0.5"}`}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -17,7 +15,7 @@ function Separator({
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
"shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ComponentProps, HTMLAttributes } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Badge } from "@/components/badge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from "react";
|
||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Dialog as SheetPrimitive } from "radix-ui";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/button";
|
||||
import { XIcon } from "lucide-react";
|
||||
|
||||
function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) {
|
||||
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
|
||||
@@ -34,7 +35,7 @@ function SheetOverlay({
|
||||
<SheetPrimitive.Overlay
|
||||
data-slot="sheet-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-overlay data-[state=closed]:pointer-events-none",
|
||||
"fixed inset-0 z-50 bg-black/10 text-xs/relaxed duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -46,34 +47,37 @@ function SheetContent({
|
||||
className,
|
||||
children,
|
||||
side = "right",
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
data-slot="sheet-content"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=closed]:pointer-events-none",
|
||||
side === "right" &&
|
||||
"data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l",
|
||||
side === "left" &&
|
||||
"data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r",
|
||||
side === "top" &&
|
||||
"data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b",
|
||||
side === "bottom" &&
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t",
|
||||
"fixed z-50 flex flex-col bg-popover bg-clip-padding text-xs/relaxed text-popover-foreground shadow-lg transition duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-[side=bottom]:data-open:slide-in-from-bottom-10 data-[side=left]:data-open:slide-in-from-left-10 data-[side=right]:data-open:slide-in-from-right-10 data-[side=top]:data-open:slide-in-from-top-10 data-closed:animate-out data-closed:fade-out-0 data-[side=bottom]:data-closed:slide-out-to-bottom-10 data-[side=left]:data-closed:slide-out-to-left-10 data-[side=right]:data-closed:slide-out-to-right-10 data-[side=top]:data-closed:slide-out-to-top-10",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{showCloseButton && (
|
||||
<SheetPrimitive.Close data-slot="sheet-close" asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute top-3 right-3"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</SheetPrimitive.Close>
|
||||
)}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
);
|
||||
@@ -83,7 +87,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="sheet-header"
|
||||
className={cn("flex flex-col gap-1.5 p-4", className)}
|
||||
className={cn("flex flex-col gap-0.5 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -106,7 +110,10 @@ function SheetTitle({
|
||||
return (
|
||||
<SheetPrimitive.Title
|
||||
data-slot="sheet-title"
|
||||
className={cn("text-foreground font-semibold", className)}
|
||||
className={cn(
|
||||
"font-heading text-sm font-medium text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -119,7 +126,7 @@ function SheetDescription({
|
||||
return (
|
||||
<SheetPrimitive.Description
|
||||
data-slot="sheet-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
className={cn("text-xs/relaxed text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -1,25 +1,29 @@
|
||||
/* eslint-disable react-refresh/only-export-components */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
import { Slot } from "radix-ui";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/button";
|
||||
import { Input } from "@/components/input";
|
||||
import { Separator } from "@/components/separator";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/sheet";
|
||||
import { Skeleton } from "@/components/skeleton";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip";
|
||||
import { PanelLeftIcon } from "lucide-react";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
@@ -119,25 +123,23 @@ function SidebarProvider({
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper has-data-[variant=inset]:bg-sidebar flex min-h-svh w-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<div
|
||||
data-slot="sidebar-wrapper"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-data-[variant=inset]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
}
|
||||
@@ -148,20 +150,21 @@ function Sidebar({
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
dir,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}) {
|
||||
const { state } = useSidebar();
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
data-slot="sidebar"
|
||||
className={cn(
|
||||
"bg-sidebar text-sidebar-foreground flex h-full w-(--sidebar-width) flex-col",
|
||||
"flex h-full w-(--sidebar-width) flex-col bg-sidebar text-sidebar-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -171,41 +174,42 @@ function Sidebar({
|
||||
);
|
||||
}
|
||||
|
||||
// Commented out mobile behavior to keep sidebar always visible
|
||||
// if (isMobile) {
|
||||
// return (
|
||||
// <Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
// <SheetContent
|
||||
// data-sidebar="sidebar"
|
||||
// data-slot="sidebar"
|
||||
// data-mobile="true"
|
||||
// className="bg-sidebar text-sidebar-foreground w-(--sidebar-width) p-0 [&>button]:hidden"
|
||||
// style={
|
||||
// {
|
||||
// "--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
// } as React.CSSProperties
|
||||
// }
|
||||
// side={side}
|
||||
// >
|
||||
// <SheetHeader className="sr-only">
|
||||
// <SheetTitle>Sidebar</SheetTitle>
|
||||
// <SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
// </SheetHeader>
|
||||
// <div className="flex h-full w-full flex-col">{children}</div>
|
||||
// </SheetContent>
|
||||
// </Sheet>
|
||||
// )
|
||||
// }
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
dir={dir}
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-(--sidebar-width) bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="group peer text-sidebar-foreground block"
|
||||
className="group peer hidden text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
data-slot="sidebar"
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
data-slot="sidebar-gap"
|
||||
className={cn(
|
||||
@@ -219,11 +223,9 @@ function Sidebar({
|
||||
/>
|
||||
<div
|
||||
data-slot="sidebar-container"
|
||||
data-side={side}
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-10 flex h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
"fixed inset-y-0 z-10 hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-200 ease-linear data-[side=left]:left-0 data-[side=left]:group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)] data-[side=right]:right-0 data-[side=right]:group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)] md:flex",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)+(--spacing(4))+2px)]"
|
||||
@@ -235,7 +237,7 @@ function Sidebar({
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-slot="sidebar-inner"
|
||||
className="bg-sidebar group-data-[variant=floating]:border-sidebar-border flex h-full w-full flex-col group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border-2 group-data-[variant=floating]:shadow-sm"
|
||||
className="flex size-full flex-col bg-sidebar group-data-[variant=floating]:rounded-none group-data-[variant=floating]:shadow-sm group-data-[variant=floating]:ring-1 group-data-[variant=floating]:ring-sidebar-border"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -256,8 +258,8 @@ function SidebarTrigger({
|
||||
data-sidebar="trigger"
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("size-7", className)}
|
||||
size="icon-sm"
|
||||
className={cn(className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
@@ -282,10 +284,10 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"hover:after:bg-sidebar-border absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] sm:flex",
|
||||
"absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
|
||||
"in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"hover:group-data-[collapsible=offcanvas]:bg-sidebar group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
@@ -300,8 +302,7 @@ function SidebarInset({ className, ...props }: React.ComponentProps<"main">) {
|
||||
<main
|
||||
data-slot="sidebar-inset"
|
||||
className={cn(
|
||||
"bg-background relative flex w-full flex-1 flex-col",
|
||||
"md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
"relative flex w-full flex-1 flex-col bg-background md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-none md:peer-data-[variant=inset]:shadow-sm md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -317,7 +318,7 @@ function SidebarInput({
|
||||
<Input
|
||||
data-slot="sidebar-input"
|
||||
data-sidebar="input"
|
||||
className={cn("bg-background h-8 w-full shadow-none", className)}
|
||||
className={cn("h-8 w-full bg-background shadow-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -353,7 +354,7 @@ function SidebarSeparator({
|
||||
<Separator
|
||||
data-slot="sidebar-separator"
|
||||
data-sidebar="separator"
|
||||
className={cn("bg-sidebar-border mx-2 w-auto", className)}
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -365,7 +366,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
data-slot="sidebar-content"
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto thin-scrollbar group-data-[collapsible=icon]:overflow-hidden",
|
||||
"no-scrollbar flex min-h-0 flex-1 flex-col gap-0 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -389,15 +390,14 @@ function SidebarGroupLabel({
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
const Comp = asChild ? Slot.Root : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-label"
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"text-sidebar-foreground/70 ring-sidebar-ring flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium outline-hidden transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
"flex h-8 shrink-0 items-center rounded-none px-2 text-xs text-sidebar-foreground/70 ring-sidebar-ring outline-hidden transition-[margin,opacity] duration-200 ease-linear group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0 focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -410,17 +410,14 @@ function SidebarGroupAction({
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-group-action"
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -436,7 +433,7 @@ function SidebarGroupContent({
|
||||
<div
|
||||
data-slot="sidebar-group-content"
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
className={cn("w-full text-xs", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -447,7 +444,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
<ul
|
||||
data-slot="sidebar-menu"
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
className={cn("flex w-full min-w-0 flex-col gap-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -465,7 +462,7 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
}
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-hidden ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"peer/menu-button group/menu-button flex w-full items-center gap-2 overflow-hidden rounded-none p-2 text-left text-xs ring-sidebar-ring outline-hidden transition-[width,height,padding] group-has-data-[sidebar=menu-action]/menu-item:pr-8 group-data-[collapsible=icon]:size-8! group-data-[collapsible=icon]:p-2! hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-open:hover:bg-sidebar-accent data-open:hover:text-sidebar-accent-foreground data-active:bg-sidebar-accent data-active:font-medium data-active:text-sidebar-accent-foreground [&_svg]:size-4 [&_svg]:shrink-0 [&>span:last-child]:truncate",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
@@ -474,9 +471,9 @@ const sidebarMenuButtonVariants = cva(
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
default: "h-8 text-xs",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:p-0!",
|
||||
lg: "h-12 text-xs group-data-[collapsible=icon]:p-0!",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
@@ -499,7 +496,7 @@ function SidebarMenuButton({
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
@@ -545,22 +542,16 @@ function SidebarMenuAction({
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? Slot.Root : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="sidebar-menu-action"
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground peer-hover/menu-button:text-sidebar-accent-foreground absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 outline-hidden transition-transform focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:absolute after:-inset-2 md:after:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
showOnHover &&
|
||||
"peer-data-[active=true]/menu-button:text-sidebar-accent-foreground group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0",
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -577,12 +568,7 @@ function SidebarMenuBadge({
|
||||
data-slot="sidebar-menu-badge"
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"text-sidebar-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-md px-1 text-xs font-medium tabular-nums select-none",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 items-center justify-center rounded-none px-1 text-xs font-medium text-sidebar-foreground tabular-nums select-none group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 peer-data-active/menu-button:text-sidebar-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -606,12 +592,12 @@ function SidebarMenuSkeleton({
|
||||
<div
|
||||
data-slot="sidebar-menu-skeleton"
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
className={cn("flex h-8 items-center gap-2 rounded-none px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
className="size-4 rounded-none"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
@@ -634,8 +620,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) {
|
||||
data-slot="sidebar-menu-sub"
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"border-sidebar-border mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-l border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -668,7 +653,7 @@ function SidebarMenuSubButton({
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
const Comp = asChild ? Slot.Root : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
@@ -677,11 +662,7 @@ function SidebarMenuSubButton({
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"text-sidebar-foreground ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground [&>svg]:text-sidebar-accent-foreground flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-md px-2 outline-hidden focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
"flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-none px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-xs data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -4,7 +4,7 @@ function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
className={cn("animate-pulse rounded-none bg-muted", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { Toaster as Sonner } from "sonner";
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>;
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme();
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-card group-[.toaster]:text-card-foreground group-[.toaster]:border-border group-[.toaster]:rounded-none! group-[.toaster]:shadow-lg group-[.toaster]:font-mono",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-accent-brand group-[.toast]:text-white group-[.toast]:font-semibold",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
success:
|
||||
"group-[.toast]:!text-accent-brand group-[.toast]:!border-accent-brand/30",
|
||||
info: "group-[.toast]:!text-accent-brand group-[.toast]:!border-accent-brand/30",
|
||||
error:
|
||||
"group-[.toast]:!text-destructive group-[.toast]:!border-destructive/30",
|
||||
},
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--radius": "0px",
|
||||
"--normal-bg": "var(--card)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--normal-text": "var(--card-foreground)",
|
||||
"--success-bg": "var(--card)",
|
||||
"--success-border": "var(--border)",
|
||||
"--success-text": "var(--color-accent-brand)",
|
||||
"--info-bg": "var(--card)",
|
||||
"--info-border": "var(--border)",
|
||||
"--info-text": "var(--color-accent-brand)",
|
||||
"--error-bg": "var(--card)",
|
||||
"--error-border": "var(--border)",
|
||||
"--error-text": "var(--destructive)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Toaster };
|
||||