feature: add portal app commander

Hee Sung Son committed Dec 6, 2025 at 02:28 UTC e7d70f6ebcea506992cb06c3bf5899873bb4c7c8
14 files changed +598 -457
cmd/relay-server/frontend/package-lock.json
+117 -324
@@ -1,43 +1,44 @@
1 {
2 "name": "relay-server-frontend",
3 - "version": "0.0.0",
3 + "version": "1.0.0",
4 "lockfileVersion": 3,
5 "requires": true,
6 "packages": {
7 "": {
8 "name": "relay-server-frontend",
9 - "version": "0.0.0",
9 + "version": "1.0.0",
10 "dependencies": {
11 "@radix-ui/react-dialog": "^1.1.15",
12 + "@radix-ui/react-icons": "^1.3.2",
13 "@radix-ui/react-popover": "^1.1.15",
14 "@radix-ui/react-scroll-area": "^1.2.10",
15 "@radix-ui/react-select": "^2.2.6",
16 "@radix-ui/react-slot": "^1.2.4",
16 - "@ssgoi/react": "^2.5.3",
17 + "@ssgoi/react": "^2.5.5",
18 "class-variance-authority": "^0.7.1",
19 "clsx": "^2.1.1",
20 "cmdk": "^1.1.1",
20 - "lucide-react": "^0.554.0",
21 - "react": "^19.2.0",
22 - "react-dom": "^19.2.0",
23 - "react-router-dom": "^7.9.6",
21 + "lucide-react": "^0.556.0",
22 + "react": "^19.2.1",
23 + "react-dom": "^19.2.1",
24 + "react-router-dom": "^7.10.1",
25 "tailwind-merge": "^3.4.0"
26 },
27 "devDependencies": {
28 "@tailwindcss/vite": "^4.1.17",
29 "@types/node": "^24.10.1",
29 - "@types/react": "^19.2.6",
30 + "@types/react": "^19.2.7",
31 "@types/react-dom": "^19.2.3",
31 - "@typescript-eslint/eslint-plugin": "^8.47.0",
32 - "@typescript-eslint/parser": "^8.47.0",
32 + "@typescript-eslint/eslint-plugin": "^8.48.1",
33 + "@typescript-eslint/parser": "^8.48.1",
34 "@vitejs/plugin-react": "^5.1.1",
34 - "babel-plugin-react-compiler": "rc",
35 + "babel-plugin-react-compiler": "1.0.0",
36 "eslint": "^9.39.1",
37 "eslint-plugin-react-hooks": "^7.0.1",
38 "eslint-plugin-react-refresh": "^0.4.24",
39 "tailwindcss": "^4.1.17",
40 "typescript": "^5.9.3",
40 - "vite": "^7.2.4"
41 + "vite": "^7.2.6"
42 }
43 },
44 "node_modules/@babel/code-frame": {
@@ -1113,44 +1114,6 @@
1114 "@jridgewell/sourcemap-codec": "^1.4.14"
1115 }
1116 },
1116 - "node_modules/@nodelib/fs.scandir": {
1117 - "version": "2.1.5",
1118 - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
1119 - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
1120 - "dev": true,
1121 - "license": "MIT",
1122 - "dependencies": {
1123 - "@nodelib/fs.stat": "2.0.5",
1124 - "run-parallel": "^1.1.9"
1125 - },
1126 - "engines": {
1127 - "node": ">= 8"
1128 - }
1129 - },
1130 - "node_modules/@nodelib/fs.stat": {
1131 - "version": "2.0.5",
1132 - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
1133 - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
1134 - "dev": true,
1135 - "license": "MIT",
1136 - "engines": {
1137 - "node": ">= 8"
1138 - }
1139 - },
1140 - "node_modules/@nodelib/fs.walk": {
1141 - "version": "1.2.8",
1142 - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
1143 - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
1144 - "dev": true,
1145 - "license": "MIT",
1146 - "dependencies": {
1147 - "@nodelib/fs.scandir": "2.1.5",
1148 - "fastq": "^1.6.0"
1149 - },
1150 - "engines": {
1151 - "node": ">= 8"
1152 - }
1153 - },
1117 "node_modules/@radix-ui/number": {
1118 "version": "1.1.1",
1119 "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
@@ -1396,6 +1359,15 @@
1359 }
1360 }
1361 },
1362 + "node_modules/@radix-ui/react-icons": {
1363 + "version": "1.3.2",
1364 + "resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.2.tgz",
1365 + "integrity": "sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==",
1366 + "license": "MIT",
1367 + "peerDependencies": {
1368 + "react": "^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc"
1369 + }
1370 + },
1371 "node_modules/@radix-ui/react-id": {
1372 "version": "1.1.1",
1373 "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
@@ -2181,21 +2153,21 @@
2153 ]
2154 },
2155 "node_modules/@ssgoi/core": {
2184 - "version": "2.5.3",
2185 - "resolved": "https://registry.npmjs.org/@ssgoi/core/-/core-2.5.3.tgz",
2186 - "integrity": "sha512-e9F/AuhDKtZtScAY7mQ2eho+p4Tdd0N0u+9k/vwD6z0zFhJww7sj5GetLNQXXn2S4frXjAwcl83hahl2d6Gljw==",
2156 + "version": "2.5.5",
2157 + "resolved": "https://registry.npmjs.org/@ssgoi/core/-/core-2.5.5.tgz",
2158 + "integrity": "sha512-BBPD4q7PweQk0j0sxOuipFSMZ5Ty7JQ4RalqrP64abQU7wJkps2YA9eO+2+AgH5O/E9CXF0H/IB7OjmT1zEeoQ==",
2159 "license": "MIT",
2160 "engines": {
2161 "node": ">=18.0.0"
2162 }
2163 },
2164 "node_modules/@ssgoi/react": {
2193 - "version": "2.5.3",
2194 - "resolved": "https://registry.npmjs.org/@ssgoi/react/-/react-2.5.3.tgz",
2195 - "integrity": "sha512-Os9zCw7GOZRNDRrWlyVTgBZObVDiRREqEP6I97jbYu/4XrloPJy34TN0FzdWD+EPACiNIsUCc50Aq1R7KiIWdA==",
2165 + "version": "2.5.5",
2166 + "resolved": "https://registry.npmjs.org/@ssgoi/react/-/react-2.5.5.tgz",
2167 + "integrity": "sha512-GRw5/VEheQguvGJ46r633GwlbQqaiV6cLfsBb9cPAhT6EvoxAHx13i6h2BuShyiY4kSf62hI/HF3lXqRAHifDQ==",
2168 "license": "MIT",
2169 "dependencies": {
2198 - "@ssgoi/core": "2.5.3"
2170 + "@ssgoi/core": "^2.5.5"
2171 },
2172 "engines": {
2173 "node": ">=18.0.0"
@@ -2547,9 +2519,9 @@
2519 }
2520 },
2521 "node_modules/@types/react": {
2550 - "version": "19.2.6",
2551 - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
2552 - "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
2522 + "version": "19.2.7",
2523 + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
2524 + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
2525 "devOptional": true,
2526 "license": "MIT",
2527 "dependencies": {
@@ -2567,17 +2539,17 @@
2539 }
2540 },
2541 "node_modules/@typescript-eslint/eslint-plugin": {
2570 - "version": "8.47.0",
2571 - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.47.0.tgz",
2572 - "integrity": "sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==",
2542 + "version": "8.48.1",
2543 + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz",
2544 + "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==",
2545 "dev": true,
2546 "license": "MIT",
2547 "dependencies": {
2548 "@eslint-community/regexpp": "^4.10.0",
2577 - "@typescript-eslint/scope-manager": "8.47.0",
2578 - "@typescript-eslint/type-utils": "8.47.0",
2579 - "@typescript-eslint/utils": "8.47.0",
2580 - "@typescript-eslint/visitor-keys": "8.47.0",
2549 + "@typescript-eslint/scope-manager": "8.48.1",
2550 + "@typescript-eslint/type-utils": "8.48.1",
2551 + "@typescript-eslint/utils": "8.48.1",
2552 + "@typescript-eslint/visitor-keys": "8.48.1",
2553 "graphemer": "^1.4.0",
2554 "ignore": "^7.0.0",
2555 "natural-compare": "^1.4.0",
@@ -2591,22 +2563,22 @@
2563 "url": "https://opencollective.com/typescript-eslint"
2564 },
2565 "peerDependencies": {
2594 - "@typescript-eslint/parser": "^8.47.0",
2566 + "@typescript-eslint/parser": "^8.48.1",
2567 "eslint": "^8.57.0 || ^9.0.0",
2568 "typescript": ">=4.8.4 <6.0.0"
2569 }
2570 },
2571 "node_modules/@typescript-eslint/parser": {
2600 - "version": "8.47.0",
2601 - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.47.0.tgz",
2602 - "integrity": "sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==",
2572 + "version": "8.48.1",
2573 + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz",
2574 + "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==",
2575 "dev": true,
2576 "license": "MIT",
2577 "dependencies": {
2606 - "@typescript-eslint/scope-manager": "8.47.0",
2607 - "@typescript-eslint/types": "8.47.0",
2608 - "@typescript-eslint/typescript-estree": "8.47.0",
2609 - "@typescript-eslint/visitor-keys": "8.47.0",
2578 + "@typescript-eslint/scope-manager": "8.48.1",
2579 + "@typescript-eslint/types": "8.48.1",
2580 + "@typescript-eslint/typescript-estree": "8.48.1",
2581 + "@typescript-eslint/visitor-keys": "8.48.1",
2582 "debug": "^4.3.4"
2583 },
2584 "engines": {
@@ -2622,14 +2594,14 @@
2594 }
2595 },
2596 "node_modules/@typescript-eslint/project-service": {
2625 - "version": "8.47.0",
2626 - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.47.0.tgz",
2627 - "integrity": "sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==",
2597 + "version": "8.48.1",
2598 + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz",
2599 + "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==",
2600 "dev": true,
2601 "license": "MIT",
2602 "dependencies": {
2631 - "@typescript-eslint/tsconfig-utils": "^8.47.0",
2632 - "@typescript-eslint/types": "^8.47.0",
2603 + "@typescript-eslint/tsconfig-utils": "^8.48.1",
2604 + "@typescript-eslint/types": "^8.48.1",
2605 "debug": "^4.3.4"
2606 },
2607 "engines": {
@@ -2644,14 +2616,14 @@
2616 }
2617 },
2618 "node_modules/@typescript-eslint/scope-manager": {
2647 - "version": "8.47.0",
2648 - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
2649 - "integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
2619 + "version": "8.48.1",
2620 + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz",
2621 + "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==",
2622 "dev": true,
2623 "license": "MIT",
2624 "dependencies": {
2653 - "@typescript-eslint/types": "8.47.0",
2654 - "@typescript-eslint/visitor-keys": "8.47.0"
2625 + "@typescript-eslint/types": "8.48.1",
2626 + "@typescript-eslint/visitor-keys": "8.48.1"
2627 },
2628 "engines": {
2629 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2662,9 +2634,9 @@
2634 }
2635 },
2636 "node_modules/@typescript-eslint/tsconfig-utils": {
2665 - "version": "8.47.0",
2666 - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.47.0.tgz",
2667 - "integrity": "sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==",
2637 + "version": "8.48.1",
2638 + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz",
2639 + "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==",
2640 "dev": true,
2641 "license": "MIT",
2642 "engines": {
@@ -2679,15 +2651,15 @@
2651 }
2652 },
2653 "node_modules/@typescript-eslint/type-utils": {
2682 - "version": "8.47.0",
2683 - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.47.0.tgz",
2684 - "integrity": "sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==",
2654 + "version": "8.48.1",
2655 + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz",
2656 + "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==",
2657 "dev": true,
2658 "license": "MIT",
2659 "dependencies": {
2688 - "@typescript-eslint/types": "8.47.0",
2689 - "@typescript-eslint/typescript-estree": "8.47.0",
2690 - "@typescript-eslint/utils": "8.47.0",
2660 + "@typescript-eslint/types": "8.48.1",
2661 + "@typescript-eslint/typescript-estree": "8.48.1",
2662 + "@typescript-eslint/utils": "8.48.1",
2663 "debug": "^4.3.4",
2664 "ts-api-utils": "^2.1.0"
2665 },
@@ -2704,9 +2676,9 @@
2676 }
2677 },
2678 "node_modules/@typescript-eslint/types": {
2707 - "version": "8.47.0",
2708 - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
2709 - "integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
2679 + "version": "8.48.1",
2680 + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz",
2681 + "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==",
2682 "dev": true,
2683 "license": "MIT",
2684 "engines": {
@@ -2718,21 +2690,20 @@
2690 }
2691 },
2692 "node_modules/@typescript-eslint/typescript-estree": {
2721 - "version": "8.47.0",
2722 - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.47.0.tgz",
2723 - "integrity": "sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==",
2693 + "version": "8.48.1",
2694 + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz",
2695 + "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==",
2696 "dev": true,
2697 "license": "MIT",
2698 "dependencies": {
2727 - "@typescript-eslint/project-service": "8.47.0",
2728 - "@typescript-eslint/tsconfig-utils": "8.47.0",
2729 - "@typescript-eslint/types": "8.47.0",
2730 - "@typescript-eslint/visitor-keys": "8.47.0",
2699 + "@typescript-eslint/project-service": "8.48.1",
2700 + "@typescript-eslint/tsconfig-utils": "8.48.1",
2701 + "@typescript-eslint/types": "8.48.1",
2702 + "@typescript-eslint/visitor-keys": "8.48.1",
2703 "debug": "^4.3.4",
2732 - "fast-glob": "^3.3.2",
2733 - "is-glob": "^4.0.3",
2704 "minimatch": "^9.0.4",
2705 "semver": "^7.6.0",
2706 + "tinyglobby": "^0.2.15",
2707 "ts-api-utils": "^2.1.0"
2708 },
2709 "engines": {
@@ -2747,16 +2718,16 @@
2718 }
2719 },
2720 "node_modules/@typescript-eslint/utils": {
2750 - "version": "8.47.0",
2751 - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.47.0.tgz",
2752 - "integrity": "sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==",
2721 + "version": "8.48.1",
2722 + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz",
2723 + "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==",
2724 "dev": true,
2725 "license": "MIT",
2726 "dependencies": {
2727 "@eslint-community/eslint-utils": "^4.7.0",
2757 - "@typescript-eslint/scope-manager": "8.47.0",
2758 - "@typescript-eslint/types": "8.47.0",
2759 - "@typescript-eslint/typescript-estree": "8.47.0"
2728 + "@typescript-eslint/scope-manager": "8.48.1",
2729 + "@typescript-eslint/types": "8.48.1",
2730 + "@typescript-eslint/typescript-estree": "8.48.1"
2731 },
2732 "engines": {
2733 "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2771,13 +2742,13 @@
2742 }
2743 },
2744 "node_modules/@typescript-eslint/visitor-keys": {
2774 - "version": "8.47.0",
2775 - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
2776 - "integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
2745 + "version": "8.48.1",
2746 + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz",
2747 + "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==",
2748 "dev": true,
2749 "license": "MIT",
2750 "dependencies": {
2780 - "@typescript-eslint/types": "8.47.0",
2751 + "@typescript-eslint/types": "8.48.1",
2752 "eslint-visitor-keys": "^4.2.1"
2753 },
2754 "engines": {
@@ -2898,9 +2869,9 @@
2869 }
2870 },
2871 "node_modules/babel-plugin-react-compiler": {
2901 - "version": "19.1.0-rc.3",
2902 - "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.3.tgz",
2903 - "integrity": "sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA==",
2872 + "version": "1.0.0",
2873 + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
2874 + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
2875 "dev": true,
2876 "license": "MIT",
2877 "dependencies": {
@@ -2934,19 +2905,6 @@
2905 "balanced-match": "^1.0.0"
2906 }
2907 },
2937 - "node_modules/braces": {
2938 - "version": "3.0.3",
2939 - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
2940 - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
2941 - "dev": true,
2942 - "license": "MIT",
2943 - "dependencies": {
2944 - "fill-range": "^7.1.1"
2945 - },
2946 - "engines": {
2947 - "node": ">=8"
2948 - }
2949 - },
2908 "node_modules/browserslist": {
2909 "version": "4.28.0",
2910 "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz",
@@ -3101,12 +3059,16 @@
3059 "license": "MIT"
3060 },
3061 "node_modules/cookie": {
3104 - "version": "1.0.2",
3105 - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
3106 - "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
3062 + "version": "1.1.1",
3063 + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
3064 + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
3065 "license": "MIT",
3066 "engines": {
3067 "node": ">=18"
3068 + },
3069 + "funding": {
3070 + "type": "opencollective",
3071 + "url": "https://opencollective.com/express"
3072 }
3073 },
3074 "node_modules/cross-spawn": {
@@ -3509,36 +3471,6 @@
3471 "dev": true,
3472 "license": "MIT"
3473 },
3512 - "node_modules/fast-glob": {
3513 - "version": "3.3.3",
3514 - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
3515 - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
3516 - "dev": true,
3517 - "license": "MIT",
3518 - "dependencies": {
3519 - "@nodelib/fs.stat": "^2.0.2",
3520 - "@nodelib/fs.walk": "^1.2.3",
3521 - "glob-parent": "^5.1.2",
3522 - "merge2": "^1.3.0",
3523 - "micromatch": "^4.0.8"
3524 - },
3525 - "engines": {
3526 - "node": ">=8.6.0"
3527 - }
3528 - },
3529 - "node_modules/fast-glob/node_modules/glob-parent": {
3530 - "version": "5.1.2",
3531 - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
3532 - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
3533 - "dev": true,
3534 - "license": "ISC",
3535 - "dependencies": {
3536 - "is-glob": "^4.0.1"
3537 - },
3538 - "engines": {
3539 - "node": ">= 6"
3540 - }
3541 - },
3474 "node_modules/fast-json-stable-stringify": {
3475 "version": "2.1.0",
3476 "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@@ -3553,16 +3485,6 @@
3485 "dev": true,
3486 "license": "MIT"
3487 },
3556 - "node_modules/fastq": {
3557 - "version": "1.19.1",
3558 - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
3559 - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
3560 - "dev": true,
3561 - "license": "ISC",
3562 - "dependencies": {
3563 - "reusify": "^1.0.4"
3564 - }
3565 - },
3488 "node_modules/file-entry-cache": {
3489 "version": "8.0.0",
3490 "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -3576,19 +3498,6 @@
3498 "node": ">=16.0.0"
3499 }
3500 },
3579 - "node_modules/fill-range": {
3580 - "version": "7.1.1",
3581 - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
3582 - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
3583 - "dev": true,
3584 - "license": "MIT",
3585 - "dependencies": {
3586 - "to-regex-range": "^5.0.1"
3587 - },
3588 - "engines": {
3589 - "node": ">=8"
3590 - }
3591 - },
3501 "node_modules/find-up": {
3502 "version": "5.0.0",
3503 "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3788,16 +3697,6 @@
3697 "node": ">=0.10.0"
3698 }
3699 },
3791 - "node_modules/is-number": {
3792 - "version": "7.0.0",
3793 - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
3794 - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
3795 - "dev": true,
3796 - "license": "MIT",
3797 - "engines": {
3798 - "node": ">=0.12.0"
3799 - }
3800 - },
3700 "node_modules/isexe": {
3701 "version": "2.0.0",
3702 "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -4201,9 +4100,9 @@
4100 }
4101 },
4102 "node_modules/lucide-react": {
4204 - "version": "0.554.0",
4205 - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.554.0.tgz",
4206 - "integrity": "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA==",
4103 + "version": "0.556.0",
4104 + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.556.0.tgz",
4105 + "integrity": "sha512-iOb8dRk7kLaYBZhR2VlV1CeJGxChBgUthpSP8wom9jfj79qovgG6qcSdiy6vkoREKPnbUYzJsCn4o4PtG3Iy+A==",
4106 "license": "ISC",
4107 "peerDependencies": {
4108 "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -4219,30 +4118,6 @@
4118 "@jridgewell/sourcemap-codec": "^1.5.5"
4119 }
4120 },
4222 - "node_modules/merge2": {
4223 - "version": "1.4.1",
4224 - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
4225 - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
4226 - "dev": true,
4227 - "license": "MIT",
4228 - "engines": {
4229 - "node": ">= 8"
4230 - }
4231 - },
4232 - "node_modules/micromatch": {
4233 - "version": "4.0.8",
4234 - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
4235 - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
4236 - "dev": true,
4237 - "license": "MIT",
4238 - "dependencies": {
4239 - "braces": "^3.0.3",
4240 - "picomatch": "^2.3.1"
4241 - },
4242 - "engines": {
4243 - "node": ">=8.6"
4244 - }
4245 - },
4121 "node_modules/minimatch": {
4122 "version": "9.0.5",
4123 "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@@ -4389,19 +4264,6 @@
4264 "dev": true,
4265 "license": "ISC"
4266 },
4392 - "node_modules/picomatch": {
4393 - "version": "2.3.1",
4394 - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
4395 - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
4396 - "dev": true,
4397 - "license": "MIT",
4398 - "engines": {
4399 - "node": ">=8.6"
4400 - },
4401 - "funding": {
4402 - "url": "https://github.com/sponsors/jonschlinkert"
4403 - }
4404 - },
4267 "node_modules/postcss": {
4268 "version": "8.5.6",
4269 "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
@@ -4451,46 +4313,25 @@
4313 "node": ">=6"
4314 }
4315 },
4454 - "node_modules/queue-microtask": {
4455 - "version": "1.2.3",
4456 - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
4457 - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
4458 - "dev": true,
4459 - "funding": [
4460 - {
4461 - "type": "github",
4462 - "url": "https://github.com/sponsors/feross"
4463 - },
4464 - {
4465 - "type": "patreon",
4466 - "url": "https://www.patreon.com/feross"
4467 - },
4468 - {
4469 - "type": "consulting",
4470 - "url": "https://feross.org/support"
4471 - }
4472 - ],
4473 - "license": "MIT"
4474 - },
4316 "node_modules/react": {
4476 - "version": "19.2.0",
4477 - "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
4478 - "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
4317 + "version": "19.2.1",
4318 + "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
4319 + "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
4320 "license": "MIT",
4321 "engines": {
4322 "node": ">=0.10.0"
4323 }
4324 },
4325 "node_modules/react-dom": {
4485 - "version": "19.2.0",
4486 - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
4487 - "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
4326 + "version": "19.2.1",
4327 + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
4328 + "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
4329 "license": "MIT",
4330 "dependencies": {
4331 "scheduler": "^0.27.0"
4332 },
4333 "peerDependencies": {
4493 - "react": "^19.2.0"
4334 + "react": "^19.2.1"
4335 }
4336 },
4337 "node_modules/react-refresh": {
@@ -4551,9 +4392,9 @@
4392 }
4393 },
4394 "node_modules/react-router": {
4554 - "version": "7.9.6",
4555 - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz",
4556 - "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==",
4395 + "version": "7.10.1",
4396 + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz",
4397 + "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==",
4398 "license": "MIT",
4399 "dependencies": {
4400 "cookie": "^1.0.1",
@@ -4573,12 +4414,12 @@
4414 }
4415 },
4416 "node_modules/react-router-dom": {
4576 - "version": "7.9.6",
4577 - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz",
4578 - "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==",
4417 + "version": "7.10.1",
4418 + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz",
4419 + "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==",
4420 "license": "MIT",
4421 "dependencies": {
4581 - "react-router": "7.9.6"
4422 + "react-router": "7.10.1"
4423 },
4424 "engines": {
4425 "node": ">=20.0.0"
@@ -4620,17 +4461,6 @@
4461 "node": ">=4"
4462 }
4463 },
4623 - "node_modules/reusify": {
4624 - "version": "1.1.0",
4625 - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
4626 - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
4627 - "dev": true,
4628 - "license": "MIT",
4629 - "engines": {
4630 - "iojs": ">=1.0.0",
4631 - "node": ">=0.10.0"
4632 - }
4633 - },
4464 "node_modules/rollup": {
4465 "version": "4.53.2",
4466 "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz",
@@ -4673,30 +4503,6 @@
4503 "fsevents": "~2.3.2"
4504 }
4505 },
4676 - "node_modules/run-parallel": {
4677 - "version": "1.2.0",
4678 - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
4679 - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
4680 - "dev": true,
4681 - "funding": [
4682 - {
4683 - "type": "github",
4684 - "url": "https://github.com/sponsors/feross"
4685 - },
4686 - {
4687 - "type": "patreon",
4688 - "url": "https://www.patreon.com/feross"
4689 - },
4690 - {
4691 - "type": "consulting",
4692 - "url": "https://feross.org/support"
4693 - }
4694 - ],
4695 - "license": "MIT",
4696 - "dependencies": {
4697 - "queue-microtask": "^1.2.2"
4698 - }
4699 - },
4506 "node_modules/scheduler": {
4507 "version": "0.27.0",
4508 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -4860,19 +4666,6 @@
4666 "url": "https://github.com/sponsors/jonschlinkert"
4667 }
4668 },
4863 - "node_modules/to-regex-range": {
4864 - "version": "5.0.1",
4865 - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
4866 - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
4867 - "dev": true,
4868 - "license": "MIT",
4869 - "dependencies": {
4870 - "is-number": "^7.0.0"
4871 - },
4872 - "engines": {
4873 - "node": ">=8.0"
4874 - }
4875 - },
4669 "node_modules/ts-api-utils": {
4670 "version": "2.1.0",
4671 "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
@@ -5011,9 +4804,9 @@
4804 }
4805 },
4806 "node_modules/vite": {
5014 - "version": "7.2.4",
5015 - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
5016 - "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
4807 + "version": "7.2.6",
4808 + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz",
4809 + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
4810 "dev": true,
4811 "license": "MIT",
4812 "dependencies": {
cmd/relay-server/frontend/package.json
+12 -11
@@ -1,7 +1,7 @@
1 {
2 "name": "relay-server-frontend",
3 "private": true,
4 - "version": "0.0.0",
4 + "version": "1.0.0",
5 "type": "module",
6 "scripts": {
7 "dev": "vite",
@@ -13,34 +13,35 @@
13 },
14 "dependencies": {
15 "@radix-ui/react-dialog": "^1.1.15",
16 + "@radix-ui/react-icons": "^1.3.2",
17 "@radix-ui/react-popover": "^1.1.15",
18 "@radix-ui/react-scroll-area": "^1.2.10",
19 "@radix-ui/react-select": "^2.2.6",
20 "@radix-ui/react-slot": "^1.2.4",
20 - "@ssgoi/react": "^2.5.3",
21 + "@ssgoi/react": "^2.5.5",
22 "class-variance-authority": "^0.7.1",
23 "clsx": "^2.1.1",
24 "cmdk": "^1.1.1",
24 - "lucide-react": "^0.554.0",
25 - "react": "^19.2.0",
26 - "react-dom": "^19.2.0",
27 - "react-router-dom": "^7.9.6",
25 + "lucide-react": "^0.556.0",
26 + "react": "^19.2.1",
27 + "react-dom": "^19.2.1",
28 + "react-router-dom": "^7.10.1",
29 "tailwind-merge": "^3.4.0"
30 },
31 "devDependencies": {
32 "@tailwindcss/vite": "^4.1.17",
33 "@types/node": "^24.10.1",
33 - "@types/react": "^19.2.6",
34 + "@types/react": "^19.2.7",
35 "@types/react-dom": "^19.2.3",
35 - "@typescript-eslint/eslint-plugin": "^8.47.0",
36 - "@typescript-eslint/parser": "^8.47.0",
36 + "@typescript-eslint/eslint-plugin": "^8.48.1",
37 + "@typescript-eslint/parser": "^8.48.1",
38 "@vitejs/plugin-react": "^5.1.1",
38 - "babel-plugin-react-compiler": "rc",
39 + "babel-plugin-react-compiler": "1.0.0",
40 "eslint": "^9.39.1",
41 "eslint-plugin-react-hooks": "^7.0.1",
42 "eslint-plugin-react-refresh": "^0.4.24",
43 "tailwindcss": "^4.1.17",
44 "typescript": "^5.9.3",
44 - "vite": "^7.2.4"
45 + "vite": "^7.2.6"
46 }
47 }
cmd/relay-server/frontend/src/components/Header.tsx
+10 -4
@@ -1,5 +1,7 @@
1 import { useEffect, useState } from "react";
2 import { Moon, Sun } from "lucide-react";
3 +import { Button } from "@/components/ui/button";
4 +import { TunnelCommandModal } from "@/components/TunnelCommandModal";
5
6 interface HeaderProps {
7 title?: string;
@@ -74,7 +76,7 @@ export function Header({ title = "PORTAL" }: HeaderProps) {
76 </a>
77 <button
78 onClick={toggleTheme}
77 - className="text-foreground hover:text-primary transition-colors p-1 rounded-lg hover:bg-secondary"
79 + className="text-foreground hover:text-primary transition-colors p-1 rounded-md hover:bg-secondary"
80 aria-label="Toggle theme"
81 >
82 {theme === "dark" ? (
@@ -83,9 +85,13 @@ export function Header({ title = "PORTAL" }: HeaderProps) {
85 <Moon className="w-6 h-6" />
86 )}
87 </button>
86 - {/* <Button>
87 - <span className="truncate">Add Your Server</span>
88 - </Button> */}
88 + <TunnelCommandModal
89 + trigger={
90 + <Button>
91 + <span className="truncate">Add Your Server</span>
92 + </Button>
93 + }
94 + />
95 </div>
96 </header>
97 );
cmd/relay-server/frontend/src/components/Pagination.tsx deleted
-35
@@ -1,35 +0,0 @@
1 -import { ChevronLeft, ChevronRight } from "lucide-react";
2 -
3 -interface PaginationProps {
4 - currentPage: number;
5 - totalPages: number;
6 - onPageChange: (page: number) => void;
7 -}
8 -
9 -export function Pagination({
10 - currentPage,
11 - totalPages,
12 - onPageChange,
13 -}: PaginationProps) {
14 - return (
15 - <div className="flex items-center justify-center gap-4 px-4 sm:px-6 py-8">
16 - <button
17 - onClick={() => onPageChange(currentPage - 1)}
18 - disabled={currentPage === 1}
19 - className="flex h-10 w-10 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-border text-foreground hover:bg-border/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
20 - >
21 - <ChevronLeft className="w-6 h-6" />
22 - </button>
23 - <span className="text-sm text-gray-400">
24 - Page {currentPage} of {totalPages}
25 - </span>
26 - <button
27 - onClick={() => onPageChange(currentPage + 1)}
28 - disabled={currentPage === totalPages}
29 - className="flex h-10 w-10 cursor-pointer items-center justify-center overflow-hidden rounded-full bg-border text-foreground hover:bg-border/80 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
30 - >
31 - <ChevronRight className="w-6 h-6" />
32 - </button>
33 - </div>
34 - );
35 -}
cmd/relay-server/frontend/src/components/SearchBar.tsx
+4 -4
@@ -37,15 +37,15 @@ export function SearchBar({
37 }: SearchBarProps) {
38 return (
39 <div className="flex flex-wrap mt-4 sm:mt-6 items-center gap-3 px-4 sm:px-6">
40 - <label className="flex min-w-[220px] flex-1 items-stretch h-11">
41 - <div className="text-text-muted flex items-center justify-center pl-4 pr-2 rounded-l-lg bg-border">
42 - <Search className="w-5 h-5" />
40 + <label className="flex min-w-[220px] flex-1 items-stretch h-10">
41 + <div className="text-text-muted flex items-center justify-center pl-3 pr-2 rounded-l-md bg-border">
42 + <Search className="w-4 h-4" />
43 </div>
44 <Input
45 placeholder="Search by server name..."
46 value={searchQuery}
47 onChange={(e) => onSearchChange(e.target.value)}
48 - className="rounded-l-none"
48 + className="rounded-l-none h-10"
49 />
50 </label>
51
cmd/relay-server/frontend/src/components/ServerCard.tsx
+2 -2
@@ -266,7 +266,7 @@ export function ServerCard({
266 onClick={handleBPSModalClose}
267 >
268 <div
269 - className="bg-background rounded-lg p-6 w-96 shadow-xl border border-foreground/20"
269 + className="bg-background rounded-md p-6 w-96 shadow-xl border border-foreground/20"
270 onClick={(e) => e.stopPropagation()}
271 >
272 <h3 className="text-lg font-bold mb-4">BPS Settings</h3>
@@ -287,7 +287,7 @@ export function ServerCard({
287 const idx = parseInt(e.target.value, 10);
288 handleSliderChange(idx);
289 }}
290 - className="w-full h-2 bg-secondary rounded-lg appearance-none cursor-pointer mb-2"
290 + className="w-full h-2 bg-secondary rounded-md appearance-none cursor-pointer mb-2"
291 />
292 {/* Step labels */}
293 <div className="flex justify-between text-xs text-text-muted mb-4">
cmd/relay-server/frontend/src/components/TagCombobox.tsx
+3 -3
@@ -98,14 +98,14 @@ export function TagCombobox({
98 ref={containerRef}
99 className="flex w-full sm:w-auto sm:min-w-[320px] flex-1 items-center gap-2 overflow-visible"
100 >
101 - <div className="flex min-w-0 flex-1 items-center gap-2 rounded-lg border border-border bg-background px-2 py-1.5 min-h-10">
101 + <div className="flex min-w-0 flex-1 items-center gap-2 rounded-md border border-border bg-background px-2 py-1.5 min-h-10">
102 <div className="flex flex-1 flex-wrap items-center gap-2 overflow-hidden">
103 {selectedTags.map((tag) => (
104 <Button
105 key={tag}
106 variant="secondary"
107 size="sm"
108 - className="h-7 rounded px-3 bg-secondary text-primary text-xs"
108 + className="h-7 rounded-md px-3 bg-secondary text-primary text-xs"
109 onClick={() => onRemove(tag)}
110 aria-label={`Remove tag ${tag}`}
111 >
@@ -140,7 +140,7 @@ export function TagCombobox({
140 createPortal(
141 <div
142 style={panelStyle}
143 - className="rounded-lg border border-border bg-background shadow-lg overflow-hidden"
143 + className="rounded-md border border-border bg-background shadow-lg overflow-hidden"
144 >
145 <ul
146 id={listId}
cmd/relay-server/frontend/src/components/TunnelCommandModal.tsx new
+147
@@ -0,0 +1,147 @@
1 +import { useState, useMemo } from "react";
2 +import { Copy, Check, Terminal } from "lucide-react";
3 +import {
4 + Dialog,
5 + DialogContent,
6 + DialogHeader,
7 + DialogTitle,
8 + DialogDescription,
9 + DialogTrigger,
10 +} from "@/components/ui/dialog";
11 +import { Button } from "@/components/ui/button";
12 +import { Input } from "@/components/ui/input";
13 +
14 +interface TunnelCommandModalProps {
15 + trigger?: React.ReactNode;
16 +}
17 +
18 +export function TunnelCommandModal({ trigger }: TunnelCommandModalProps) {
19 + const defaultPort = "3000";
20 + const defaultName = "myapp";
21 +
22 + const [port, setPort] = useState(defaultPort);
23 + const [name, setName] = useState(defaultName);
24 + const [copied, setCopied] = useState(false);
25 +
26 + // Get current host URL dynamically
27 + const hostUrl = useMemo(() => {
28 + if (typeof window !== "undefined") {
29 + return window.location.origin;
30 + }
31 + return "http://localhost:4017";
32 + }, []);
33 +
34 + // Generate the tunnel command
35 + const command = useMemo(() => {
36 + return `curl -fsSL ${hostUrl}/tunnel | PORT=${
37 + port === "" ? defaultPort : port
38 + } NAME=${name === "" ? defaultName : name} sh`;
39 + }, [hostUrl, port, name]);
40 +
41 + const handleCopy = async () => {
42 + try {
43 + await navigator.clipboard.writeText(command);
44 + setCopied(true);
45 + setTimeout(() => setCopied(false), 2000);
46 + } catch (err) {
47 + console.error("Failed to copy:", err);
48 + }
49 + };
50 +
51 + return (
52 + <Dialog>
53 + <DialogTrigger asChild>
54 + {trigger || (
55 + <Button>
56 + <span className="truncate">Add Your Server</span>
57 + </Button>
58 + )}
59 + </DialogTrigger>
60 + <DialogContent className="sm:max-w-[500px]">
61 + <DialogHeader>
62 + <DialogTitle className="flex items-center gap-2">
63 + <Terminal className="w-5 h-5" />
64 + Tunnel Setup Command
65 + </DialogTitle>
66 + <DialogDescription>
67 + Configure your tunnel settings and copy the command to start
68 + exposing your local server.
69 + </DialogDescription>
70 + </DialogHeader>
71 +
72 + <div className="space-y-4 py-4 [&>div]:flex [&>div]:flex-col">
73 + {/* Port Input */}
74 + <div className="space-y-2">
75 + <label
76 + htmlFor="port"
77 + className="text-sm font-medium text-foreground"
78 + >
79 + Local Port
80 + </label>
81 + <div className="flex items-center rounded-md bg-border">
82 + <span className="px-3 text-sm text-text-muted">PORT=</span>
83 + <Input
84 + id="port"
85 + type="number"
86 + value={port}
87 + onChange={(e) => setPort(e.target.value)}
88 + placeholder={defaultPort}
89 + className="rounded-l-none"
90 + />
91 + </div>
92 + <p className="text-xs text-text-muted">
93 + The port your local service is running on
94 + </p>
95 + </div>
96 +
97 + {/* Name Input */}
98 + <div className="space-y-2">
99 + <label
100 + htmlFor="name"
101 + className="text-sm font-medium text-foreground"
102 + >
103 + Service Name
104 + </label>
105 + <div className="flex items-center rounded-md bg-border">
106 + <span className="px-3 text-sm text-text-muted">NAME=</span>
107 + <Input
108 + id="name"
109 + type="text"
110 + value={name}
111 + onChange={(e) => setName(e.target.value)}
112 + placeholder={defaultName}
113 + className="rounded-l-none"
114 + />
115 + </div>
116 + <p className="text-xs text-text-muted">
117 + A unique identifier for your tunnel
118 + </p>
119 + </div>
120 +
121 + {/* Generated Command */}
122 + <div className="space-y-2">
123 + <label className="text-sm font-medium text-foreground">
124 + Generated Command
125 + </label>
126 + <div className="relative">
127 + <pre className="p-3 pr-12 rounded-md bg-border text-sm text-foreground overflow-x-auto whitespace-pre-wrap break-all font-mono">
128 + {command}
129 + </pre>
130 + <button
131 + onClick={handleCopy}
132 + className="cursor-pointer absolute right-2 top-1/2 -translate-y-1/2 p-2 rounded-md hover:bg-background/50 transition-colors"
133 + aria-label="Copy command"
134 + >
135 + {copied ? (
136 + <Check className="w-4 h-4 text-green-500" />
137 + ) : (
138 + <Copy className="w-4 h-4 text-text-muted" />
139 + )}
140 + </button>
141 + </div>
142 + </div>
143 + </div>
144 + </DialogContent>
145 + </Dialog>
146 + );
147 +}
cmd/relay-server/frontend/src/components/ui/button.tsx
+27 -19
@@ -1,22 +1,30 @@
1 -import * as React from "react";
2 -import { Slot } from "@radix-ui/react-slot";
3 -import { cva, type VariantProps } from "class-variance-authority";
4 -import { cn } from "@/lib/utils";
1 +import * as React from "react"
2 +import { Slot } from "@radix-ui/react-slot"
3 +import { cva, type VariantProps } from "class-variance-authority"
4 +
5 +import { cn } from "@/lib/utils"
6
7 const buttonVariants = cva(
7 - "inline-flex items-center justify-center overflow-hidden rounded-lg text-sm font-bold leading-normal tracking-[0.015em] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed",
8 + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9 {
10 variants: {
11 variant: {
11 - default: "bg-primary text-black hover:bg-primary/90",
12 - secondary: "bg-border text-foreground hover:bg-border/80",
13 - ghost: "hover:bg-border/80 text-foreground",
12 + default:
13 + "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 + destructive:
15 + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 + outline:
17 + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 + secondary:
19 + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 + ghost: "hover:bg-accent hover:text-accent-foreground",
21 + link: "text-primary underline-offset-4 hover:underline",
22 },
23 size: {
16 - default: "h-10 px-4",
17 - sm: "h-9 px-4",
18 - lg: "h-11 px-8",
19 - icon: "h-10 w-10",
24 + default: "h-9 px-4 py-2",
25 + sm: "h-8 rounded-md px-3 text-xs",
26 + lg: "h-10 rounded-md px-8",
27 + icon: "h-9 w-9",
28 },
29 },
30 defaultVariants: {
@@ -24,26 +32,26 @@ const buttonVariants = cva(
32 size: "default",
33 },
34 }
27 -);
35 +)
36
37 export interface ButtonProps
38 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39 VariantProps<typeof buttonVariants> {
32 - asChild?: boolean;
40 + asChild?: boolean
41 }
42
43 const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44 ({ className, variant, size, asChild = false, ...props }, ref) => {
37 - const Comp = asChild ? Slot : "button";
45 + const Comp = asChild ? Slot : "button"
46 return (
47 <Comp
48 className={cn(buttonVariants({ variant, size, className }))}
49 ref={ref}
50 {...props}
51 />
44 - );
52 + )
53 }
46 -);
47 -Button.displayName = "Button";
54 +)
55 +Button.displayName = "Button"
56
49 -export { Button, buttonVariants };
57 +export { Button, buttonVariants }
cmd/relay-server/frontend/src/components/ui/dialog.tsx new
+119
@@ -0,0 +1,119 @@
1 +import * as React from "react"
2 +import * as DialogPrimitive from "@radix-ui/react-dialog"
3 +import { cn } from "@/lib/utils"
4 +import { Cross2Icon } from "@radix-ui/react-icons"
5 +
6 +const Dialog = DialogPrimitive.Root
7 +
8 +const DialogTrigger = DialogPrimitive.Trigger
9 +
10 +const DialogPortal = DialogPrimitive.Portal
11 +
12 +const DialogClose = DialogPrimitive.Close
13 +
14 +const DialogOverlay = React.forwardRef<
15 + React.ElementRef<typeof DialogPrimitive.Overlay>,
16 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
17 +>(({ className, ...props }, ref) => (
18 + <DialogPrimitive.Overlay
19 + ref={ref}
20 + className={cn(
21 + "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22 + className
23 + )}
24 + {...props}
25 + />
26 +))
27 +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
28 +
29 +const DialogContent = React.forwardRef<
30 + React.ElementRef<typeof DialogPrimitive.Content>,
31 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
32 +>(({ className, children, ...props }, ref) => (
33 + <DialogPortal>
34 + <DialogOverlay />
35 + <DialogPrimitive.Content
36 + ref={ref}
37 + className={cn(
38 + "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
39 + className
40 + )}
41 + {...props}
42 + >
43 + {children}
44 + <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
45 + <Cross2Icon className="h-4 w-4" />
46 + <span className="sr-only">Close</span>
47 + </DialogPrimitive.Close>
48 + </DialogPrimitive.Content>
49 + </DialogPortal>
50 +))
51 +DialogContent.displayName = DialogPrimitive.Content.displayName
52 +
53 +const DialogHeader = ({
54 + className,
55 + ...props
56 +}: React.HTMLAttributes<HTMLDivElement>) => (
57 + <div
58 + className={cn(
59 + "flex flex-col space-y-1.5 text-center sm:text-left",
60 + className
61 + )}
62 + {...props}
63 + />
64 +)
65 +DialogHeader.displayName = "DialogHeader"
66 +
67 +const DialogFooter = ({
68 + className,
69 + ...props
70 +}: React.HTMLAttributes<HTMLDivElement>) => (
71 + <div
72 + className={cn(
73 + "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
74 + className
75 + )}
76 + {...props}
77 + />
78 +)
79 +DialogFooter.displayName = "DialogFooter"
80 +
81 +const DialogTitle = React.forwardRef<
82 + React.ElementRef<typeof DialogPrimitive.Title>,
83 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
84 +>(({ className, ...props }, ref) => (
85 + <DialogPrimitive.Title
86 + ref={ref}
87 + className={cn(
88 + "text-lg font-semibold leading-none tracking-tight",
89 + className
90 + )}
91 + {...props}
92 + />
93 +))
94 +DialogTitle.displayName = DialogPrimitive.Title.displayName
95 +
96 +const DialogDescription = React.forwardRef<
97 + React.ElementRef<typeof DialogPrimitive.Description>,
98 + React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
99 +>(({ className, ...props }, ref) => (
100 + <DialogPrimitive.Description
101 + ref={ref}
102 + className={cn("text-sm text-muted-foreground", className)}
103 + {...props}
104 + />
105 +))
106 +DialogDescription.displayName = DialogPrimitive.Description.displayName
107 +
108 +export {
109 + Dialog,
110 + DialogPortal,
111 + DialogOverlay,
112 + DialogTrigger,
113 + DialogClose,
114 + DialogContent,
115 + DialogHeader,
116 + DialogFooter,
117 + DialogTitle,
118 + DialogDescription,
119 +}
cmd/relay-server/frontend/src/components/ui/input.tsx
+8 -10
@@ -1,24 +1,22 @@
1 -import * as React from "react";
2 -import { cn } from "@/lib/utils";
1 +import * as React from "react"
2
4 -export interface InputProps
5 - extends React.InputHTMLAttributes<HTMLInputElement> {}
3 +import { cn } from "@/lib/utils"
4
7 -const Input = React.forwardRef<HTMLInputElement, InputProps>(
5 +const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
6 ({ className, type, ...props }, ref) => {
7 return (
8 <input
9 type={type}
10 className={cn(
13 - "flex w-full min-w-0 flex-1 resize-none overflow-hidden rounded-r-lg text-foreground focus:outline-0 focus:ring-2 focus:ring-primary/50 border-none bg-border h-full placeholder:text-text-muted px-4 pl-2 text-base font-normal leading-normal",
11 + "flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12 className
13 )}
14 ref={ref}
15 {...props}
16 />
19 - );
17 + )
18 }
21 -);
22 -Input.displayName = "Input";
19 +)
20 +Input.displayName = "Input"
21
24 -export { Input };
22 +export { Input }
cmd/relay-server/frontend/src/components/ui/scroll-area.tsx
+10 -10
@@ -1,7 +1,7 @@
1 -import * as React from "react";
2 -import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
1 +import * as React from "react"
2 +import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3
4 -import { cn } from "@/lib/utils";
4 +import { cn } from "@/lib/utils"
5
6 const ScrollArea = React.forwardRef<
7 React.ElementRef<typeof ScrollAreaPrimitive.Root>,
@@ -18,8 +18,8 @@ const ScrollArea = React.forwardRef<
18 <ScrollBar />
19 <ScrollAreaPrimitive.Corner />
20 </ScrollAreaPrimitive.Root>
21 -));
22 -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
21 +))
22 +ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23
24 const ScrollBar = React.forwardRef<
25 React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
@@ -31,16 +31,16 @@ const ScrollBar = React.forwardRef<
31 className={cn(
32 "flex touch-none select-none transition-colors",
33 orientation === "vertical" &&
34 - "h-full w-2.5 border-l border-l-transparent p-px",
34 + "h-full w-2.5 border-l border-l-transparent p-[1px]",
35 orientation === "horizontal" &&
36 - "h-2.5 flex-col border-t border-t-transparent p-px",
36 + "h-2.5 flex-col border-t border-t-transparent p-[1px]",
37 className
38 )}
39 {...props}
40 >
41 <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
42 </ScrollAreaPrimitive.ScrollAreaScrollbar>
43 -));
44 -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
43 +))
44 +ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45
46 -export { ScrollArea, ScrollBar };
46 +export { ScrollArea, ScrollBar }
cmd/relay-server/frontend/src/components/ui/select.tsx
+32 -35
@@ -1,13 +1,13 @@
1 -import * as React from "react";
2 -import * as SelectPrimitive from "@radix-ui/react-select";
3 -import { Check, ChevronDown, ChevronUp } from "lucide-react";
4 -import { cn } from "@/lib/utils";
1 +import * as React from "react"
2 +import * as SelectPrimitive from "@radix-ui/react-select"
3 +import { cn } from "@/lib/utils"
4 +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "@radix-ui/react-icons"
5
6 -const Select = SelectPrimitive.Root;
6 +const Select = SelectPrimitive.Root
7
8 -const SelectGroup = SelectPrimitive.Group;
8 +const SelectGroup = SelectPrimitive.Group
9
10 -const SelectValue = SelectPrimitive.Value;
10 +const SelectValue = SelectPrimitive.Value
11
12 const SelectTrigger = React.forwardRef<
13 React.ElementRef<typeof SelectPrimitive.Trigger>,
@@ -16,18 +16,18 @@ const SelectTrigger = React.forwardRef<
16 <SelectPrimitive.Trigger
17 ref={ref}
18 className={cn(
19 - "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-lg bg-border px-3 py-2 text-sm text-foreground shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary/50 cursor-pointer disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
19 + "flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
20 className
21 )}
22 {...props}
23 >
24 {children}
25 <SelectPrimitive.Icon asChild>
26 - <ChevronDown className="h-4 w-4 opacity-50" />
26 + <ChevronDownIcon className="h-4 w-4 opacity-50" />
27 </SelectPrimitive.Icon>
28 </SelectPrimitive.Trigger>
29 -));
30 -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
29 +))
30 +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
31
32 const SelectScrollUpButton = React.forwardRef<
33 React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
@@ -41,10 +41,10 @@ const SelectScrollUpButton = React.forwardRef<
41 )}
42 {...props}
43 >
44 - <ChevronUp className="h-4 w-4" />
44 + <ChevronUpIcon className="h-4 w-4" />
45 </SelectPrimitive.ScrollUpButton>
46 -));
47 -SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
46 +))
47 +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
48
49 const SelectScrollDownButton = React.forwardRef<
50 React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
@@ -58,11 +58,11 @@ const SelectScrollDownButton = React.forwardRef<
58 )}
59 {...props}
60 >
61 - <ChevronDown className="h-4 w-4" />
61 + <ChevronDownIcon className="h-4 w-4" />
62 </SelectPrimitive.ScrollDownButton>
63 -));
63 +))
64 SelectScrollDownButton.displayName =
65 - SelectPrimitive.ScrollDownButton.displayName;
65 + SelectPrimitive.ScrollDownButton.displayName
66
67 const SelectContent = React.forwardRef<
68 React.ElementRef<typeof SelectPrimitive.Content>,
@@ -72,7 +72,7 @@ const SelectContent = React.forwardRef<
72 <SelectPrimitive.Content
73 ref={ref}
74 className={cn(
75 - "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-lg bg-card text-foreground shadow-md 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",
75 + "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md 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 origin-[--radix-select-content-transform-origin]",
76 position === "popper" &&
77 "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
78 className
@@ -85,7 +85,7 @@ const SelectContent = React.forwardRef<
85 className={cn(
86 "p-1",
87 position === "popper" &&
88 - "h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width)"
88 + "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
89 )}
90 >
91 {children}
@@ -93,8 +93,8 @@ const SelectContent = React.forwardRef<
93 <SelectScrollDownButton />
94 </SelectPrimitive.Content>
95 </SelectPrimitive.Portal>
96 -));
97 -SelectContent.displayName = SelectPrimitive.Content.displayName;
96 +))
97 +SelectContent.displayName = SelectPrimitive.Content.displayName
98
99 const SelectLabel = React.forwardRef<
100 React.ElementRef<typeof SelectPrimitive.Label>,
@@ -102,14 +102,11 @@ const SelectLabel = React.forwardRef<
102 >(({ className, ...props }, ref) => (
103 <SelectPrimitive.Label
104 ref={ref}
105 - className={cn(
106 - "px-2 py-1.5 text-sm font-semibold text-text-muted",
107 - className
108 - )}
105 + className={cn("px-2 py-1.5 text-sm font-semibold", className)}
106 {...props}
107 />
111 -));
112 -SelectLabel.displayName = SelectPrimitive.Label.displayName;
108 +))
109 +SelectLabel.displayName = SelectPrimitive.Label.displayName
110
111 const SelectItem = React.forwardRef<
112 React.ElementRef<typeof SelectPrimitive.Item>,
@@ -118,20 +115,20 @@ const SelectItem = React.forwardRef<
115 <SelectPrimitive.Item
116 ref={ref}
117 className={cn(
121 - "relative flex w-full cursor-pointer select-none items-center rounded-md py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-border focus:text-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
118 + "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
119 className
120 )}
121 {...props}
122 >
123 <span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
124 <SelectPrimitive.ItemIndicator>
128 - <Check className="h-4 w-4" />
125 + <CheckIcon className="h-4 w-4" />
126 </SelectPrimitive.ItemIndicator>
127 </span>
128 <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
129 </SelectPrimitive.Item>
133 -));
134 -SelectItem.displayName = SelectPrimitive.Item.displayName;
130 +))
131 +SelectItem.displayName = SelectPrimitive.Item.displayName
132
133 const SelectSeparator = React.forwardRef<
134 React.ElementRef<typeof SelectPrimitive.Separator>,
@@ -139,11 +136,11 @@ const SelectSeparator = React.forwardRef<
136 >(({ className, ...props }, ref) => (
137 <SelectPrimitive.Separator
138 ref={ref}
142 - className={cn("-mx-1 my-1 h-px bg-border", className)}
139 + className={cn("-mx-1 my-1 h-px bg-muted", className)}
140 {...props}
141 />
145 -));
146 -SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
142 +))
143 +SelectSeparator.displayName = SelectPrimitive.Separator.displayName
144
145 export {
146 Select,
@@ -156,4 +153,4 @@ export {
153 SelectSeparator,
154 SelectScrollUpButton,
155 SelectScrollDownButton,
159 -};
156 +}
cmd/relay-server/frontend/update-shadcn.cjs new
+107
@@ -0,0 +1,107 @@
1 +#!/usr/bin/env node
2 +
3 +const fs = require("fs");
4 +const path = require("path");
5 +const { execSync } = require("child_process");
6 +
7 +/**
8 + * shadcn 컴포넌트를 자동으로 업데이트하는 스크립트
9 + * components.json 파일의 aliases.components 경로를 읽어서
10 + * 해당 경로의 ui 폴더 안에 있는 모든 컴포넌트를 업데이트합니다.
11 + */
12 +
13 +function main() {
14 + try {
15 + // components.json 파일 읽기
16 + const componentsJsonPath = path.join(process.cwd(), "components.json");
17 +
18 + if (!fs.existsSync(componentsJsonPath)) {
19 + console.error("❌ components.json 파일을 찾을 수 없습니다.");
20 + process.exit(1);
21 + }
22 +
23 + const componentsConfig = JSON.parse(
24 + fs.readFileSync(componentsJsonPath, "utf8")
25 + );
26 +
27 + // aliases.components 경로 가져오기
28 + const componentsPath = componentsConfig.aliases?.components;
29 + if (!componentsPath) {
30 + console.error(
31 + "❌ components.json에서 aliases.components 경로를 찾을 수 없습니다."
32 + );
33 + process.exit(1);
34 + }
35 +
36 + // ui 폴더 경로 구성
37 + const uiPath = path.join(
38 + process.cwd(),
39 + "src",
40 + componentsPath.replace("@/", ""),
41 + "ui"
42 + );
43 +
44 + if (!fs.existsSync(uiPath)) {
45 + console.error(`❌ UI 컴포넌트 폴더를 찾을 수 없습니다: ${uiPath}`);
46 + process.exit(1);
47 + }
48 +
49 + // ui 폴더의 모든 .tsx 파일 찾기
50 + const files = fs
51 + .readdirSync(uiPath)
52 + .filter((file) => file.endsWith(".tsx"))
53 + .map((file) => path.basename(file, ".tsx"));
54 +
55 + if (files.length === 0) {
56 + console.log("📁 ui 폴더에서 .tsx 파일을 찾을 수 없습니다.");
57 + return;
58 + }
59 +
60 + console.log(`🔍 발견된 shadcn 컴포넌트들 (${files.length}개):`);
61 + files.forEach((file) => console.log(` - ${file}`));
62 + console.log("");
63 +
64 + // 각 컴포넌트 업데이트
65 + let successCount = 0;
66 + let failCount = 0;
67 +
68 + for (const componentName of files) {
69 + try {
70 + console.log(`🔄 업데이트 중: ${componentName}...`);
71 +
72 + const command = `npx shadcn@latest add -o -y ${componentName}`;
73 + execSync(command, {
74 + stdio: "pipe",
75 + encoding: "utf8",
76 + env: { ...process.env, npm_config_legacy_peer_deps: "true" },
77 + });
78 +
79 + console.log(`✅ ${componentName} 업데이트 완료`);
80 + successCount++;
81 + } catch (error) {
82 + console.error(`❌ ${componentName} 업데이트 실패:`, error.message);
83 + failCount++;
84 + }
85 + }
86 +
87 + console.log("");
88 + console.log("📊 업데이트 결과:");
89 + console.log(` ✅ 성공: ${successCount}개`);
90 + console.log(` ❌ 실패: ${failCount}개`);
91 + console.log(` 📁 총 컴포넌트: ${files.length}개`);
92 +
93 + if (failCount === 0) {
94 + console.log("🎉 모든 shadcn 컴포넌트가 성공적으로 업데이트되었습니다!");
95 + }
96 + } catch (error) {
97 + console.error("❌ 스크립트 실행 중 오류가 발생했습니다:", error.message);
98 + process.exit(1);
99 + }
100 +}
101 +
102 +// 스크립트가 직접 실행될 때만 main 함수 호출
103 +if (require.main === module) {
104 + main();
105 +}
106 +
107 +module.exports = { main };