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 };