Skip to content

Commit ab8aed3

Browse files
committed
🤖 refactor: replace native UI elements with shadcn components
- Update components.json to use browser/components paths - Add shadcn context-menu, select, and dialog primitives - Update dialog.tsx with app-consistent styling (bg-dark, borders) - Update select.tsx with compact sizing and theme colors - Update button.tsx variants to use accent colors - Replace native <select> with shadcn Select in: - ThemeSelector, GeneralSection, ModelsSection - Create Select wrapper maintaining existing API - Replace native <button> with shadcn Button in: - Modal (CancelButton, PrimaryButton, DangerButton now wrap Button) - SettingsModal, ProvidersSection, ModelRow - ProjectCreateModal, SettingsButton, WorkspaceHeader _Generated with `mux`_
1 parent 66992a2 commit ab8aed3

18 files changed

+643
-166
lines changed

bun.lock

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@
1919
"@orpc/server": "^1.11.3",
2020
"@orpc/zod": "^1.11.3",
2121
"@radix-ui/react-checkbox": "^1.3.3",
22+
"@radix-ui/react-context-menu": "^2.2.16",
2223
"@radix-ui/react-dialog": "^1.1.15",
2324
"@radix-ui/react-dropdown-menu": "^2.1.16",
2425
"@radix-ui/react-label": "^2.1.8",
2526
"@radix-ui/react-scroll-area": "^1.2.10",
2627
"@radix-ui/react-select": "^2.2.6",
2728
"@radix-ui/react-separator": "^1.1.7",
28-
"@radix-ui/react-slot": "^1.2.3",
29+
"@radix-ui/react-slot": "^1.2.4",
2930
"@radix-ui/react-tabs": "^1.1.13",
3031
"@radix-ui/react-toggle-group": "^1.1.11",
3132
"@radix-ui/react-tooltip": "^1.2.8",
@@ -849,6 +850,8 @@
849850

850851
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
851852

853+
"@radix-ui/react-context-menu": ["@radix-ui/react-context-menu@2.2.16", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-menu": "2.1.16", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww=="],
854+
852855
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
853856

854857
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
@@ -3731,6 +3734,8 @@
37313734

37323735
"@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
37333736

3737+
"@radix-ui/react-context-menu/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
3738+
37343739
"@radix-ui/react-dialog/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
37353740

37363741
"@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
@@ -4201,6 +4206,8 @@
42014206

42024207
"@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
42034208

4209+
"@radix-ui/react-context-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
4210+
42044211
"@radix-ui/react-dismissable-layer/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
42054212

42064213
"@radix-ui/react-dropdown-menu/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],

components.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@
55
"tsx": true,
66
"tailwind": {
77
"config": "",
8-
"css": "src/styles/globals.css",
8+
"css": "src/browser/styles/globals.css",
99
"baseColor": "slate",
1010
"cssVariables": true,
1111
"prefix": ""
1212
},
1313
"aliases": {
14-
"components": "@/components",
15-
"utils": "@/lib/utils"
14+
"components": "@/browser/components",
15+
"utils": "@/common/lib/utils"
1616
}
1717
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,14 @@
5959
"@orpc/server": "^1.11.3",
6060
"@orpc/zod": "^1.11.3",
6161
"@radix-ui/react-checkbox": "^1.3.3",
62+
"@radix-ui/react-context-menu": "^2.2.16",
6263
"@radix-ui/react-dialog": "^1.1.15",
6364
"@radix-ui/react-dropdown-menu": "^2.1.16",
6465
"@radix-ui/react-label": "^2.1.8",
6566
"@radix-ui/react-scroll-area": "^1.2.10",
6667
"@radix-ui/react-select": "^2.2.6",
6768
"@radix-ui/react-separator": "^1.1.7",
68-
"@radix-ui/react-slot": "^1.2.3",
69+
"@radix-ui/react-slot": "^1.2.4",
6970
"@radix-ui/react-tabs": "^1.1.13",
7071
"@radix-ui/react-toggle-group": "^1.1.11",
7172
"@radix-ui/react-tooltip": "^1.2.8",

src/browser/components/Modal.tsx

Lines changed: 12 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -125,53 +125,29 @@ export const WarningText: React.FC<{ children: React.ReactNode; className?: stri
125125
className,
126126
}) => <div className={cn("text-[13px] text-foreground leading-normal", className)}>{children}</div>;
127127

128-
// Button components
129-
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
128+
// Button components - thin wrappers around shadcn Button with semantic variants
129+
import { Button } from "@/browser/components/ui/button";
130+
131+
export { Button };
132+
133+
interface ModalButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
130134
children: React.ReactNode;
131135
}
132136

133-
export const Button: React.FC<ButtonProps> = ({ children, className, ...props }) => (
134-
<button
135-
className={cn(
136-
"px-5 py-2 border-none rounded cursor-pointer text-sm font-medium transition-all duration-200",
137-
"disabled:opacity-50 disabled:cursor-not-allowed",
138-
className
139-
)}
140-
{...props}
141-
>
142-
{children}
143-
</button>
144-
);
145-
146-
export const CancelButton: React.FC<ButtonProps> = ({ children, className, ...props }) => (
147-
<Button
148-
className={cn(
149-
"bg-border-medium text-foreground hover:bg-border-darker disabled:hover:bg-border-medium",
150-
className
151-
)}
152-
{...props}
153-
>
137+
export const CancelButton: React.FC<ModalButtonProps> = ({ children, className, ...props }) => (
138+
<Button variant="secondary" className={cn("px-5", className)} {...props}>
154139
{children}
155140
</Button>
156141
);
157142

158-
export const PrimaryButton: React.FC<ButtonProps> = ({ children, className, ...props }) => (
159-
<Button
160-
className={cn("bg-accent text-white hover:bg-accent-dark disabled:hover:bg-accent", className)}
161-
{...props}
162-
>
143+
export const PrimaryButton: React.FC<ModalButtonProps> = ({ children, className, ...props }) => (
144+
<Button className={cn("px-5", className)} {...props}>
163145
{children}
164146
</Button>
165147
);
166148

167-
export const DangerButton: React.FC<ButtonProps> = ({ children, className, ...props }) => (
168-
<Button
169-
className={cn(
170-
"bg-error text-white hover:brightness-110 disabled:hover:brightness-100",
171-
className
172-
)}
173-
{...props}
174-
>
149+
export const DangerButton: React.FC<ModalButtonProps> = ({ children, className, ...props }) => (
150+
<Button variant="destructive" className={cn("px-5", className)} {...props}>
175151
{children}
176152
</Button>
177153
);

src/browser/components/ProjectCreateModal.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, { useState, useCallback } from "react";
22
import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal";
33
import { DirectoryPickerModal } from "./DirectoryPickerModal";
4+
import { Button } from "@/browser/components/ui/button";
45
import type { ProjectConfig } from "@/node/config";
56
import { useAPI } from "@/browser/contexts/API";
67

@@ -146,14 +147,14 @@ export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
146147
className="bg-modal-bg border-border-medium focus:border-accent placeholder:text-muted text-foreground min-w-0 flex-1 rounded border px-3 py-2 font-mono text-sm focus:outline-none disabled:opacity-50"
147148
/>
148149
{(isDesktop || hasWebFsPicker) && (
149-
<button
150-
type="button"
150+
<Button
151+
variant="outline"
151152
onClick={handleBrowseClick}
152153
disabled={isCreating}
153-
className="bg-modal-bg border-border-medium text-muted hover:text-foreground hover:border-accent shrink-0 rounded border px-3 py-2 text-sm transition-colors disabled:opacity-50"
154+
className="shrink-0"
154155
>
155156
Browse…
156-
</button>
157+
</Button>
157158
)}
158159
</div>
159160
{error && <div className="text-error -mt-3 mb-3 text-xs">{error}</div>}

src/browser/components/Select.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11
import React from "react";
2+
import {
3+
Select as ShadcnSelect,
4+
SelectContent,
5+
SelectItem,
6+
SelectTrigger,
7+
SelectValue,
8+
} from "@/browser/components/ui/select";
29

310
interface SelectOption {
411
value: string;
@@ -17,7 +24,7 @@ interface SelectProps {
1724

1825
/**
1926
* Reusable select component with consistent styling
20-
* Centralizes select styling to avoid duplication and ensure consistent UX
27+
* Wraps shadcn Select with a simpler API for common use cases
2128
*/
2229
export function Select({
2330
value,
@@ -34,19 +41,17 @@ export function Select({
3441
);
3542

3643
return (
37-
<select
38-
id={id}
39-
value={value}
40-
onChange={(e) => onChange(e.target.value)}
41-
disabled={disabled}
42-
aria-label={ariaLabel}
43-
className={`bg-separator text-foreground border-border-medium focus:border-accent rounded border px-1 py-0.5 text-xs focus:outline-none disabled:opacity-50 ${className}`}
44-
>
45-
{normalizedOptions.map((opt) => (
46-
<option key={opt.value} value={opt.value}>
47-
{opt.label}
48-
</option>
49-
))}
50-
</select>
44+
<ShadcnSelect value={value} onValueChange={onChange} disabled={disabled}>
45+
<SelectTrigger id={id} className={className} aria-label={ariaLabel}>
46+
<SelectValue />
47+
</SelectTrigger>
48+
<SelectContent>
49+
{normalizedOptions.map((opt) => (
50+
<SelectItem key={opt.value} value={opt.value}>
51+
{opt.label}
52+
</SelectItem>
53+
))}
54+
</SelectContent>
55+
</ShadcnSelect>
5156
);
5257
}

src/browser/components/Settings/SettingsModal.tsx

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { matchesKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
66
import { GeneralSection } from "./sections/GeneralSection";
77
import { ProvidersSection } from "./sections/ProvidersSection";
88
import { ModelsSection } from "./sections/ModelsSection";
9+
import { Button } from "@/browser/components/ui/button";
910
import type { SettingsSection } from "./types";
1011

1112
const SECTIONS: SettingsSection[] = [
@@ -72,30 +73,31 @@ export function SettingsModal() {
7273
Settings
7374
</span>
7475
{/* Close button in header on mobile only */}
75-
<button
76-
type="button"
76+
<Button
77+
variant="ghost"
78+
size="icon"
7779
onClick={handleClose}
78-
className="text-muted hover:text-foreground rounded p-1 transition-colors md:hidden"
80+
className="h-6 w-6 md:hidden"
7981
aria-label="Close settings"
8082
>
8183
<X className="h-4 w-4" />
82-
</button>
84+
</Button>
8385
</div>
8486
<nav className="flex overflow-x-auto p-2 md:flex-1 md:flex-col md:overflow-y-auto">
8587
{SECTIONS.map((section) => (
86-
<button
88+
<Button
8789
key={section.id}
88-
type="button"
90+
variant="ghost"
8991
onClick={() => setActiveSection(section.id)}
90-
className={`flex shrink-0 items-center gap-2 rounded-md px-3 py-2 text-left text-sm whitespace-nowrap transition-colors md:w-full ${
92+
className={`flex h-auto shrink-0 items-center justify-start gap-2 rounded-md px-3 py-2 text-left text-sm whitespace-nowrap md:w-full ${
9193
activeSection === section.id
92-
? "bg-accent/20 text-accent"
94+
? "bg-accent/20 text-accent hover:bg-accent/20 hover:text-accent"
9395
: "text-muted hover:bg-hover hover:text-foreground"
9496
}`}
9597
>
9698
{section.icon}
9799
{section.label}
98-
</button>
100+
</Button>
99101
))}
100102
</nav>
101103
</div>
@@ -104,14 +106,15 @@ export function SettingsModal() {
104106
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
105107
<div className="border-border-medium hidden h-12 items-center justify-between border-b px-6 md:flex">
106108
<span className="text-foreground text-sm font-medium">{currentSection.label}</span>
107-
<button
108-
type="button"
109+
<Button
110+
variant="ghost"
111+
size="icon"
109112
onClick={handleClose}
110-
className="text-muted hover:text-foreground rounded p-1 transition-colors"
113+
className="h-6 w-6"
111114
aria-label="Close settings"
112115
>
113116
<X className="h-4 w-4" />
114-
</button>
117+
</Button>
115118
</div>
116119
<div className="flex-1 overflow-y-auto p-4 md:p-6">
117120
<SectionComponent />

src/browser/components/Settings/sections/GeneralSection.tsx

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
import React from "react";
22
import { useTheme, THEME_OPTIONS, type ThemeMode } from "@/browser/contexts/ThemeContext";
3+
import {
4+
Select,
5+
SelectContent,
6+
SelectItem,
7+
SelectTrigger,
8+
SelectValue,
9+
} from "@/browser/components/ui/select";
310

411
export function GeneralSection() {
512
const { theme, setTheme } = useTheme();
@@ -13,17 +20,18 @@ export function GeneralSection() {
1320
<div className="text-foreground text-sm">Theme</div>
1421
<div className="text-muted text-xs">Choose your preferred theme</div>
1522
</div>
16-
<select
17-
value={theme}
18-
onChange={(e) => setTheme(e.target.value as ThemeMode)}
19-
className="border-border-medium bg-background-secondary hover:bg-hover h-9 cursor-pointer rounded-md border px-3 text-sm transition-colors focus:outline-none"
20-
>
21-
{THEME_OPTIONS.map((option) => (
22-
<option key={option.value} value={option.value}>
23-
{option.label}
24-
</option>
25-
))}
26-
</select>
23+
<Select value={theme} onValueChange={(value) => setTheme(value as ThemeMode)}>
24+
<SelectTrigger className="border-border-medium bg-background-secondary hover:bg-hover h-9 w-auto cursor-pointer rounded-md border px-3 text-sm transition-colors">
25+
<SelectValue />
26+
</SelectTrigger>
27+
<SelectContent>
28+
{THEME_OPTIONS.map((option) => (
29+
<SelectItem key={option.value} value={option.value}>
30+
{option.label}
31+
</SelectItem>
32+
))}
33+
</SelectContent>
34+
</Select>
2735
</div>
2836
</div>
2937
</div>

0 commit comments

Comments
 (0)