Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
"@orpc/server": "^1.11.3",
"@orpc/zod": "^1.11.3",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
Expand Down Expand Up @@ -849,6 +850,8 @@

"@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=="],

"@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=="],

"@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=="],

"@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=="],
Expand Down Expand Up @@ -3731,6 +3734,8 @@

"@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=="],

"@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=="],

"@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=="],

"@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=="],
Expand Down Expand Up @@ -4201,6 +4206,8 @@

"@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=="],

"@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=="],

"@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=="],

"@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=="],
Expand Down
6 changes: 3 additions & 3 deletions components.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
"tsx": true,
"tailwind": {
"config": "",
"css": "src/styles/globals.css",
"css": "src/browser/styles/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
"components": "@/browser/components",
"utils": "@/common/lib/utils"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,14 @@
"@orpc/server": "^1.11.3",
"@orpc/zod": "^1.11.3",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
Expand Down
48 changes: 12 additions & 36 deletions src/browser/components/Modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,53 +125,29 @@ export const WarningText: React.FC<{ children: React.ReactNode; className?: stri
className,
}) => <div className={cn("text-[13px] text-foreground leading-normal", className)}>{children}</div>;

// Button components
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
// Button components - thin wrappers around shadcn Button with semantic variants
import { Button } from "@/browser/components/ui/button";

export { Button };

interface ModalButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
}

export const Button: React.FC<ButtonProps> = ({ children, className, ...props }) => (
<button
className={cn(
"px-5 py-2 border-none rounded cursor-pointer text-sm font-medium transition-all duration-200",
"disabled:opacity-50 disabled:cursor-not-allowed",
className
)}
{...props}
>
{children}
</button>
);

export const CancelButton: React.FC<ButtonProps> = ({ children, className, ...props }) => (
<Button
className={cn(
"bg-border-medium text-foreground hover:bg-border-darker disabled:hover:bg-border-medium",
className
)}
{...props}
>
export const CancelButton: React.FC<ModalButtonProps> = ({ children, className, ...props }) => (
<Button variant="secondary" className={cn("px-5", className)} {...props}>
{children}
</Button>
);

export const PrimaryButton: React.FC<ButtonProps> = ({ children, className, ...props }) => (
<Button
className={cn("bg-accent text-white hover:bg-accent-dark disabled:hover:bg-accent", className)}
{...props}
>
export const PrimaryButton: React.FC<ModalButtonProps> = ({ children, className, ...props }) => (
<Button className={cn("px-5", className)} {...props}>
{children}
</Button>
);

export const DangerButton: React.FC<ButtonProps> = ({ children, className, ...props }) => (
<Button
className={cn(
"bg-error text-white hover:brightness-110 disabled:hover:brightness-100",
className
)}
{...props}
>
export const DangerButton: React.FC<ModalButtonProps> = ({ children, className, ...props }) => (
<Button variant="destructive" className={cn("px-5", className)} {...props}>
{children}
</Button>
);
Expand Down
9 changes: 5 additions & 4 deletions src/browser/components/ProjectCreateModal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, { useState, useCallback } from "react";
import { Modal, ModalActions, CancelButton, PrimaryButton } from "./Modal";
import { DirectoryPickerModal } from "./DirectoryPickerModal";
import { Button } from "@/browser/components/ui/button";
import type { ProjectConfig } from "@/node/config";
import { useAPI } from "@/browser/contexts/API";

Expand Down Expand Up @@ -146,14 +147,14 @@ export const ProjectCreateModal: React.FC<ProjectCreateModalProps> = ({
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"
/>
{(isDesktop || hasWebFsPicker) && (
<button
type="button"
<Button
variant="outline"
onClick={handleBrowseClick}
disabled={isCreating}
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"
className="shrink-0"
>
Browse…
</button>
</Button>
)}
</div>
{error && <div className="text-error -mt-3 mb-3 text-xs">{error}</div>}
Expand Down
35 changes: 20 additions & 15 deletions src/browser/components/Select.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import React from "react";
import {
Select as ShadcnSelect,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/browser/components/ui/select";

interface SelectOption {
value: string;
Expand All @@ -17,7 +24,7 @@ interface SelectProps {

/**
* Reusable select component with consistent styling
* Centralizes select styling to avoid duplication and ensure consistent UX
* Wraps shadcn Select with a simpler API for common use cases
*/
export function Select({
value,
Expand All @@ -34,19 +41,17 @@ export function Select({
);

return (
<select
id={id}
value={value}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
aria-label={ariaLabel}
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}`}
>
{normalizedOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<ShadcnSelect value={value} onValueChange={onChange} disabled={disabled}>
<SelectTrigger id={id} className={className} aria-label={ariaLabel}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{normalizedOptions.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</ShadcnSelect>
);
}
29 changes: 16 additions & 13 deletions src/browser/components/Settings/SettingsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { matchesKeybind, KEYBINDS } from "@/browser/utils/ui/keybinds";
import { GeneralSection } from "./sections/GeneralSection";
import { ProvidersSection } from "./sections/ProvidersSection";
import { ModelsSection } from "./sections/ModelsSection";
import { Button } from "@/browser/components/ui/button";
import type { SettingsSection } from "./types";

const SECTIONS: SettingsSection[] = [
Expand Down Expand Up @@ -72,30 +73,31 @@ export function SettingsModal() {
Settings
</span>
{/* Close button in header on mobile only */}
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={handleClose}
className="text-muted hover:text-foreground rounded p-1 transition-colors md:hidden"
className="h-6 w-6 md:hidden"
aria-label="Close settings"
>
<X className="h-4 w-4" />
</button>
</Button>
</div>
<nav className="flex overflow-x-auto p-2 md:flex-1 md:flex-col md:overflow-y-auto">
{SECTIONS.map((section) => (
<button
<Button
key={section.id}
type="button"
variant="ghost"
onClick={() => setActiveSection(section.id)}
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 ${
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 ${
activeSection === section.id
? "bg-accent/20 text-accent"
? "bg-accent/20 text-accent hover:bg-accent/20 hover:text-accent"
: "text-muted hover:bg-hover hover:text-foreground"
}`}
>
{section.icon}
{section.label}
</button>
</Button>
))}
</nav>
</div>
Expand All @@ -104,14 +106,15 @@ export function SettingsModal() {
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
<div className="border-border-medium hidden h-12 items-center justify-between border-b px-6 md:flex">
<span className="text-foreground text-sm font-medium">{currentSection.label}</span>
<button
type="button"
<Button
variant="ghost"
size="icon"
onClick={handleClose}
className="text-muted hover:text-foreground rounded p-1 transition-colors"
className="h-6 w-6"
aria-label="Close settings"
>
<X className="h-4 w-4" />
</button>
</Button>
</div>
<div className="flex-1 overflow-y-auto p-4 md:p-6">
<SectionComponent />
Expand Down
30 changes: 19 additions & 11 deletions src/browser/components/Settings/sections/GeneralSection.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import React from "react";
import { useTheme, THEME_OPTIONS, type ThemeMode } from "@/browser/contexts/ThemeContext";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/browser/components/ui/select";

export function GeneralSection() {
const { theme, setTheme } = useTheme();
Expand All @@ -13,17 +20,18 @@ export function GeneralSection() {
<div className="text-foreground text-sm">Theme</div>
<div className="text-muted text-xs">Choose your preferred theme</div>
</div>
<select
value={theme}
onChange={(e) => setTheme(e.target.value as ThemeMode)}
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"
>
{THEME_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<Select value={theme} onValueChange={(value) => setTheme(value as ThemeMode)}>
<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">
<SelectValue />
</SelectTrigger>
<SelectContent>
{THEME_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
Expand Down
Loading