fix(components): refactor to use design system

This commit is contained in:
Austin Pickett
2026-04-28 13:03:05 -04:00
parent 663602f6b0
commit 1285172aca
20 changed files with 243 additions and 597 deletions

View File

@@ -42,7 +42,12 @@ import {
X,
Zap,
} from "lucide-react";
import { Button, SelectionSwitcher, Typography } from "@nous-research/ui";
import {
Button,
ListItem,
SelectionSwitcher,
Typography,
} from "@nous-research/ui";
import { cn } from "@/lib/utils";
import { Backdrop } from "@/components/Backdrop";
import { SidebarFooter } from "@/components/SidebarFooter";
@@ -391,13 +396,13 @@ export default function App() {
</header>
{mobileOpen && (
<button
type="button"
<Button
ghost
aria-label={t.app.closeNavigation}
onClick={closeMobile}
className={cn(
"lg:hidden fixed inset-0 z-40",
"bg-black/60 backdrop-blur-sm cursor-pointer",
"lg:hidden fixed inset-0 z-40 p-0 block",
"bg-black/60 backdrop-blur-sm",
)}
/>
)}
@@ -660,21 +665,19 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) {
return (
<li key={action}>
<button
type="button"
<ListItem
onClick={() => handleClick(action)}
disabled={disabled}
aria-busy={busy}
active={busy}
className={cn(
"group relative flex w-full items-center gap-3",
"px-5 py-1.5",
"gap-3 px-5 py-1.5 whitespace-nowrap",
"font-mondwest text-[0.75rem] tracking-[0.1em]",
"text-left whitespace-nowrap transition-opacity cursor-pointer",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
"transition-opacity",
busy
? "text-midground opacity-100"
: "opacity-60 hover:opacity-100",
"disabled:cursor-not-allowed disabled:opacity-30",
"disabled:opacity-30",
)}
>
{isPending ? (
@@ -703,7 +706,7 @@ function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) {
style={{ mixBlendMode: "plus-lighter" }}
/>
)}
</button>
</ListItem>
</li>
);
})}

View File

@@ -1,7 +1,6 @@
import { Select, SelectOption } from "@nous-research/ui";
import { Select, SelectOption, Switch } from "@nous-research/ui";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
function FieldHint({ schema, schemaKey }: { schema: Record<string, unknown>; schemaKey: string }) {
const keyPath = schemaKey.includes(".") ? schemaKey : "";

View File

@@ -313,19 +313,21 @@ export function ChatSidebar({ channel, className }: ChatSidebarProps) {
model
</div>
<button
type="button"
<Button
ghost
size="sm"
disabled={!canPickModel}
onClick={() => setModelOpen(true)}
className="flex items-center gap-1 truncate text-sm font-medium hover:underline disabled:cursor-not-allowed disabled:opacity-60 disabled:no-underline"
suffix={
canPickModel ? (
<ChevronDown className="opacity-60" />
) : undefined
}
className="self-start min-w-0 px-0 py-0 normal-case tracking-normal text-sm font-medium hover:underline disabled:no-underline"
title={info.model ?? "switch model"}
>
<span className="truncate">{modelLabel}</span>
{canPickModel && (
<ChevronDown className="h-3 w-3 shrink-0 opacity-60" />
)}
</button>
</Button>
</div>
<Badge tone={STATE_TONE[state]}>{STATE_LABEL[state]}</Badge>

View File

@@ -1,4 +1,4 @@
import { Typography } from "@nous-research/ui";
import { Button, Typography } from "@nous-research/ui";
import { useI18n } from "@/i18n/context";
/**
@@ -11,22 +11,25 @@ export function LanguageSwitcher() {
const toggle = () => setLocale(locale === "en" ? "zh" : "en");
return (
<button
type="button"
<Button
ghost
onClick={toggle}
className="group relative inline-flex items-center gap-1.5 px-2 py-1 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
title={t.language.switchTo}
aria-label={t.language.switchTo}
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-muted-foreground hover:text-foreground"
>
<span className="text-base leading-none">
{locale === "en" ? "🇬🇧" : "🇨🇳"}
<span className="inline-flex items-center gap-1.5">
<span className="text-base leading-none">
{locale === "en" ? "🇬🇧" : "🇨🇳"}
</span>
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
>
{locale === "en" ? "EN" : "中文"}
</Typography>
</span>
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
>
{locale === "en" ? "EN" : "中文"}
</Typography>
</button>
</Button>
);
}

View File

@@ -1,4 +1,4 @@
import { Button } from "@nous-research/ui";
import { Button, ListItem } from "@nous-research/ui";
import { Input } from "@/components/ui/input";
import type { GatewayClient } from "@/lib/gatewayClient";
import { Check, Loader2, Search, X } from "lucide-react";
@@ -280,14 +280,12 @@ function ProviderColumn({
{providers.map((p) => {
const active = p.slug === selectedSlug;
return (
<button
<ListItem
key={p.slug}
type="button"
active={active}
onClick={() => onSelect(p.slug)}
className={`w-full text-left px-3 py-2 text-xs border-l-2 transition-colors cursor-pointer flex items-start gap-2 ${
active
? "bg-primary/10 border-l-primary text-foreground"
: "border-l-transparent text-muted-foreground hover:text-foreground hover:bg-muted/40"
className={`items-start text-xs border-l-2 ${
active ? "border-l-primary" : "border-l-transparent"
}`}
>
<div className="flex-1 min-w-0">
@@ -299,7 +297,7 @@ function ProviderColumn({
{p.slug} · {p.total_models ?? p.models?.length ?? 0} models
</div>
</div>
</button>
</ListItem>
);
})}
</div>
@@ -360,23 +358,19 @@ function ModelColumn({
m === currentModel && provider.slug === currentProviderSlug;
return (
<button
<ListItem
key={m}
type="button"
active={active}
onClick={() => onSelect(m)}
onDoubleClick={() => onConfirm(m)}
className={`w-full text-left px-3 py-1.5 text-xs font-mono transition-colors cursor-pointer flex items-center gap-2 ${
active
? "bg-primary/15 text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-muted/40"
}`}
className="px-3 py-1.5 text-xs font-mono"
>
<Check
className={`h-3 w-3 shrink-0 ${active ? "text-primary" : "text-transparent"}`}
/>
<span className="flex-1 truncate">{m}</span>
{isCurrent && <CurrentTag />}
</button>
</ListItem>
);
})
)}

View File

@@ -1,4 +1,5 @@
import type { GatewayClient } from "@/lib/gatewayClient";
import { ListItem } from "@nous-research/ui";
import { ChevronRight } from "lucide-react";
import {
forwardRef,
@@ -139,18 +140,14 @@ export const SlashPopover = forwardRef<SlashPopoverHandle, Props>(
const active = i === selected;
return (
<button
<ListItem
key={`${it.text}-${i}`}
type="button"
active={active}
role="option"
aria-selected={active}
onMouseEnter={() => setSelected(i)}
onClick={() => apply(it)}
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left cursor-pointer transition-colors ${
active
? "bg-primary/10 text-foreground"
: "text-muted-foreground hover:bg-muted/60"
}`}
className="px-3 py-1.5"
>
<ChevronRight
className={`h-3 w-3 shrink-0 ${active ? "text-primary" : "text-transparent"}`}
@@ -165,7 +162,7 @@ export const SlashPopover = forwardRef<SlashPopoverHandle, Props>(
{it.meta}
</span>
)}
</button>
</ListItem>
);
})}
</div>

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Palette, Check } from "lucide-react";
import { Typography } from "@nous-research/ui";
import { Button, ListItem, Typography } from "@nous-research/ui";
import { BUILTIN_THEMES, useTheme } from "@/themes";
import { useI18n } from "@/i18n";
import { cn } from "@/lib/utils";
@@ -50,27 +50,26 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
return (
<div ref={wrapperRef} className="relative">
<button
type="button"
<Button
ghost
onClick={() => setOpen((o) => !o)}
className={cn(
"group relative inline-flex items-center gap-1.5 px-2 py-1 text-xs",
"text-muted-foreground hover:text-foreground transition-colors cursor-pointer",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
)}
className="px-2 py-1 normal-case tracking-normal font-normal text-xs text-muted-foreground hover:text-foreground"
title={t.theme?.switchTheme ?? "Switch theme"}
aria-label={t.theme?.switchTheme ?? "Switch theme"}
aria-expanded={open}
aria-haspopup="listbox"
>
<Palette className="h-3.5 w-3.5" />
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
>
{label}
</Typography>
</button>
<span className="inline-flex items-center gap-1.5">
<Palette className="h-3.5 w-3.5" />
<Typography
mondwest
className="hidden sm:inline tracking-wide uppercase text-[0.65rem]"
>
{label}
</Typography>
</span>
</Button>
{open && (
<div
@@ -97,20 +96,16 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
const preset = BUILTIN_THEMES[th.name];
return (
<button
<ListItem
key={th.name}
type="button"
active={isActive}
role="option"
aria-selected={isActive}
onClick={() => {
setTheme(th.name);
close();
}}
className={cn(
"flex w-full items-center gap-3 px-3 py-2 text-left transition-colors cursor-pointer",
"hover:bg-midground/10",
isActive ? "text-midground" : "text-midground/60",
)}
className="gap-3"
>
{preset ? (
<ThemeSwatch theme={preset.name} />
@@ -138,7 +133,7 @@ export function ThemeSwitcher({ dropUp = false }: ThemeSwitcherProps) {
isActive ? "opacity-100" : "opacity-0",
)}
/>
</button>
</ListItem>
);
})}
</div>

View File

@@ -1,3 +1,4 @@
import { ListItem } from "@nous-research/ui";
import {
AlertCircle,
Check,
@@ -87,12 +88,11 @@ export function ToolCall({ tool }: { tool: ToolEntry }) {
<div
className={`rounded-md border overflow-hidden ${STATUS_TONE[tool.status]}`}
>
<button
type="button"
<ListItem
onClick={() => setUserOverride(!open)}
disabled={!hasBody}
aria-expanded={open}
className="w-full flex items-center gap-2 px-2.5 py-1.5 text-left text-xs hover:bg-foreground/2 disabled:cursor-default cursor-pointer transition-colors"
className="px-2.5 py-1.5 text-xs hover:bg-foreground/2 disabled:cursor-default"
>
{hasBody ? (
<Chevron className="h-3 w-3 shrink-0 text-muted-foreground" />
@@ -132,7 +132,7 @@ export function ToolCall({ tool }: { tool: ToolEntry }) {
{elapsed}
</span>
)}
</button>
</ListItem>
{open && hasBody && (
<div className="border-t border-border/60 px-3 py-2 space-y-2 text-xs font-mono">

View File

@@ -1,80 +0,0 @@
import { cn } from "@/lib/utils";
export function Segmented<T extends string>({
className,
onChange,
options,
size = "sm",
value,
}: SegmentedProps<T>) {
return (
<div
role="radiogroup"
className={cn(
"inline-flex border border-border bg-background/30",
className,
)}
>
{options.map((opt) => {
const active = opt.value === value;
return (
<button
key={opt.value}
type="button"
role="radio"
aria-checked={active}
onClick={() => onChange(opt.value)}
className={cn(
"font-mondwest tracking-[0.1em] uppercase",
"transition-colors cursor-pointer whitespace-nowrap",
"border-r border-border last:border-r-0",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30",
size === "sm" && "h-7 px-2.5 text-[0.65rem]",
size === "md" && "h-8 px-3 text-xs",
active
? "bg-foreground/90 text-background"
: "text-muted-foreground hover:bg-foreground/10 hover:text-foreground",
)}
>
{opt.label}
</button>
);
})}
</div>
);
}
export function FilterGroup({
children,
className,
label,
}: FilterGroupProps) {
return (
<div className={cn("flex items-center gap-2", className)}>
<span className="font-mondwest text-[0.65rem] tracking-[0.12em] uppercase text-muted-foreground/70">
{label}
</span>
{children}
</div>
);
}
interface FilterGroupProps {
children: React.ReactNode;
className?: string;
label: string;
}
interface SegmentedOption<T extends string> {
label: string;
value: T;
}
interface SegmentedProps<T extends string> {
className?: string;
onChange: (value: T) => void;
options: SegmentedOption<T>[];
size?: "sm" | "md";
value: T;
}

View File

@@ -1,40 +0,0 @@
import { cn } from "@/lib/utils";
export function Switch({
checked,
onCheckedChange,
className,
disabled,
id,
}: {
checked: boolean;
onCheckedChange: (v: boolean) => void;
className?: string;
disabled?: boolean;
id?: string;
}) {
return (
<button
type="button"
id={id}
role="switch"
aria-checked={checked}
disabled={disabled}
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center border border-border transition-colors",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30",
"disabled:cursor-not-allowed disabled:opacity-50",
checked ? "bg-foreground/15 border-foreground/30" : "bg-background",
className,
)}
onClick={() => onCheckedChange(!checked)}
>
<span
className={cn(
"pointer-events-none block h-3.5 w-3.5 transition-transform",
checked ? "translate-x-4 bg-foreground" : "translate-x-0.5 bg-muted-foreground",
)}
/>
</button>
);
}

View File

@@ -1,51 +0,0 @@
import { useState } from "react";
import { cn } from "@/lib/utils";
export function Tabs({
defaultValue,
children,
className,
}: {
defaultValue: string;
children: (active: string, setActive: (v: string) => void) => React.ReactNode;
className?: string;
}) {
const [active, setActive] = useState(defaultValue);
return <div className={cn("flex flex-col gap-4", className)}>{children(active, setActive)}</div>;
}
export function TabsList({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"inline-flex h-9 items-center justify-start border-b border-border text-muted-foreground",
className,
)}
{...props}
/>
);
}
export function TabsTrigger({
active,
value,
onClick,
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement> & { active: boolean; value: string }) {
return (
<button
type="button"
className={cn(
"relative inline-flex items-center justify-center whitespace-nowrap px-3 py-1.5 font-mondwest text-xs tracking-[0.1em] uppercase transition-all cursor-pointer",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
active
? "text-foreground after:absolute after:bottom-0 after:left-0 after:right-0 after:h-px after:bg-foreground"
: "hover:text-foreground",
className,
)}
onClick={onClick}
{...props}
/>
);
}

View File

@@ -192,22 +192,22 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
return;
}
setEnd(
<button
type="button"
<Button
ghost
onClick={() => setMobilePanelOpenRaw(true)}
className={cn(
"inline-flex items-center gap-1.5 rounded border border-current/20",
"px-2 py-1 text-[0.65rem] font-medium tracking-wide normal-case",
"text-midground/80 hover:text-midground hover:bg-midground/5",
"focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground",
"shrink-0 cursor-pointer",
)}
aria-expanded={mobilePanelOpen}
aria-controls="chat-side-panel"
className={cn(
"shrink-0 rounded border border-current/20",
"px-2 py-1 text-[0.65rem] font-medium tracking-wide normal-case",
"text-midground/80 hover:text-midground hover:bg-midground/5",
)}
>
<PanelRight className="h-3 w-3 shrink-0" />
{modelToolsLabel}
</button>,
<span className="inline-flex items-center gap-1.5">
<PanelRight className="h-3 w-3 shrink-0" />
{modelToolsLabel}
</span>
</Button>,
);
return () => setEnd(null);
}, [isActive, narrow, mobilePanelOpen, modelToolsLabel, setEnd]);
@@ -690,13 +690,13 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
createPortal(
<>
{mobilePanelOpen && (
<button
type="button"
<Button
ghost
aria-label={t.app.closeModelTools}
onClick={closeMobilePanel}
className={cn(
"fixed inset-0 z-[55]",
"bg-black/60 backdrop-blur-sm cursor-pointer",
"fixed inset-0 z-[55] p-0 block",
"bg-black/60 backdrop-blur-sm",
)}
/>
)}
@@ -783,29 +783,29 @@ export default function ChatPage({ isActive = true }: { isActive?: boolean }) {
className="hermes-chat-xterm-host min-h-0 min-w-0 flex-1"
/>
<button
type="button"
<Button
ghost
onClick={handleCopyLast}
title="Copy last assistant response as raw markdown"
aria-label="Copy last assistant response"
className={cn(
"absolute z-10 flex items-center gap-1.5",
"absolute z-10",
"rounded border border-current/30",
"bg-black/20 backdrop-blur-sm",
"opacity-60 hover:opacity-100 hover:border-current/60",
"transition-opacity duration-150",
"focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-current",
"cursor-pointer",
"transition-opacity duration-150 normal-case font-normal tracking-normal",
"bottom-2 right-2 px-2 py-1 text-[0.65rem] sm:bottom-3 sm:right-3 sm:px-2.5 sm:py-1.5 sm:text-xs",
"lg:bottom-4 lg:right-4",
)}
style={{ color: TERMINAL_THEME.foreground }}
>
<Copy className="h-3 w-3 shrink-0" />
<span className="hidden min-[400px]:inline tracking-wide">
{copyState === "copied" ? "copied" : "copy last response"}
<span className="inline-flex items-center gap-1.5">
<Copy className="h-3 w-3 shrink-0" />
<span className="hidden min-[400px]:inline tracking-wide">
{copyState === "copied" ? "copied" : "copy last response"}
</span>
</span>
</button>
</Button>
</div>
{!narrow && (

View File

@@ -33,7 +33,7 @@ import { getNestedValue, setNestedValue } from "@/lib/nested";
import { useToast } from "@/hooks/useToast";
import { Toast } from "@/components/Toast";
import { AutoField } from "@/components/AutoField";
import { Button } from "@nous-research/ui";
import { Button, ListItem } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@nous-research/ui";
@@ -118,18 +118,20 @@ export default function ConfigPage() {
onChange={(e) => setSearchQuery(e.target.value)}
/>
{searchQuery && (
<button
type="button"
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
<Button
ghost
size="xs"
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setSearchQuery("")}
aria-label={t.common.clear}
>
<X className="h-3 w-3" />
</button>
<X />
</Button>
)}
</div>,
);
return () => setEnd(null);
}, [config, schema, searchQuery, setEnd, t.common.search]);
}, [config, schema, searchQuery, setEnd, t.common.clear, t.common.search]);
function prettyCategoryName(cat: string): string {
const key = cat as keyof typeof t.config.categories;
@@ -507,23 +509,14 @@ export default function ConfigPage() {
const isActive = !isSearching && activeCategory === cat;
return (
<button
<ListItem
key={cat}
type="button"
active={isActive}
onClick={() => {
setSearchQuery("");
setActiveCategory(cat);
}}
className={`
group flex items-center gap-2 px-2 py-1
rounded-sm text-left text-[11px] cursor-pointer whitespace-nowrap
transition-colors
${
isActive
? "bg-foreground/10 text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-foreground/5"
}
`}
className="rounded-sm whitespace-nowrap px-2 py-1 text-[11px]"
>
<CategoryIcon
category={cat}
@@ -541,7 +534,7 @@ export default function ConfigPage() {
>
{categoryCounts[cat] || 0}
</span>
</button>
</ListItem>
);
})}
</div>

View File

@@ -21,7 +21,7 @@ import { Toast } from "@/components/Toast";
import { useConfirmDelete } from "@/hooks/useConfirmDelete";
import { useToast } from "@/hooks/useToast";
import { OAuthProvidersCard } from "@/components/OAuthProvidersCard";
import { Button } from "@nous-research/ui";
import { Button, ListItem } from "@nous-research/ui";
import {
Card,
CardContent,
@@ -368,10 +368,10 @@ function ProviderGroupCard({
return (
<div className="border border-border">
{/* Header — always visible */}
<button
type="button"
<ListItem
onClick={() => setExpanded(!expanded)}
className="flex w-full items-center justify-between gap-3 px-4 py-3 cursor-pointer hover:bg-primary/5 transition-colors"
aria-expanded={expanded}
className="justify-between gap-3 px-4 py-3 hover:bg-primary/5"
>
<div className="flex items-center gap-3 min-w-0">
{expanded ? (
@@ -406,7 +406,7 @@ function ProviderGroupCard({
.replace("{s}", group.entries.length !== 1 ? "s" : "")}
</span>
</div>
</button>
</ListItem>
{expanded && (
<div className="border-t border-border px-4 py-3 grid gap-2">
@@ -823,20 +823,16 @@ function CollapsibleUnset({
return (
<>
<button
type="button"
className="flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors cursor-pointer pt-1"
<Button
ghost
size="sm"
prefix={collapsed ? <ChevronRight /> : <ChevronDown />}
onClick={() => setCollapsed(!collapsed)}
aria-expanded={!collapsed}
className="self-start mt-1 normal-case tracking-normal text-xs text-muted-foreground hover:text-foreground"
>
{collapsed ? (
<ChevronRight className="h-3 w-3" />
) : (
<ChevronDown className="h-3 w-3" />
)}
<span>
{t.env.notConfigured.replace("{count}", String(unsetEntries.length))}
</span>
</button>
{t.env.notConfigured.replace("{count}", String(unsetEntries.length))}
</Button>
{!collapsed &&
unsetEntries.map(([key, info]) => (

View File

@@ -7,12 +7,15 @@ import {
} from "react";
import { FileText, RefreshCw } from "lucide-react";
import { api } from "@/lib/api";
import { Button } from "@nous-research/ui";
import {
Badge,
Button,
FilterGroup,
Segmented,
Switch,
} from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@nous-research/ui";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { FilterGroup, Segmented } from "@/components/ui/segmented";
import { useI18n } from "@/i18n";
import { usePageHeader } from "@/contexts/usePageHeader";
import { PluginSlot } from "@/plugins";

View File

@@ -36,7 +36,7 @@ import { timeAgo } from "@/lib/utils";
import { Markdown } from "@/components/Markdown";
import { PlatformsCard } from "@/components/PlatformsCard";
import { Toast } from "@/components/Toast";
import { Button } from "@nous-research/ui";
import { Button, ListItem } from "@nous-research/ui";
import { Badge } from "@nous-research/ui";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog";
@@ -105,11 +105,11 @@ function ToolCallBlock({
return (
<div className="mt-2 border border-warning/20 bg-warning/5">
<button
type="button"
className="flex w-full items-center gap-2 px-3 py-2 text-xs text-warning cursor-pointer hover:bg-warning/10 transition-colors"
<ListItem
onClick={() => setOpen(!open)}
aria-label={`${open ? t.common.collapse : t.common.expand} tool call ${toolCall.function.name}`}
aria-expanded={open}
className="px-3 py-2 text-xs text-warning hover:bg-warning/10 hover:text-warning"
>
{open ? (
<ChevronDown className="h-3 w-3" />
@@ -120,7 +120,7 @@ function ToolCallBlock({
{toolCall.function.name}
</span>
<span className="text-warning/50 ml-auto">{toolCall.id}</span>
</button>
</ListItem>
{open && (
<pre className="border-t border-warning/20 px-3 py-2 text-xs text-warning/80 overflow-x-auto whitespace-pre-wrap font-mono">
{args}
@@ -455,13 +455,15 @@ export default function SessionsPage() {
className="h-8 pr-7 pl-8 text-xs"
/>
{search && (
<button
type="button"
className="absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer text-muted-foreground hover:text-foreground"
<Button
ghost
size="xs"
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setSearch("")}
aria-label={t.common.clear}
>
<X className="h-3 w-3" />
</button>
<X />
</Button>
)}
</div>,
);
@@ -475,6 +477,7 @@ export default function SessionsPage() {
searching,
setAfterTitle,
setEnd,
t.common.clear,
t.sessions.searchPlaceholder,
total,
]);

View File

@@ -20,9 +20,9 @@ import type { SkillInfo, ToolsetInfo } from "@/lib/api";
import { useToast } from "@/hooks/useToast";
import { Toast } from "@/components/Toast";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@nous-research/ui";
import { Badge, Button, ListItem, Switch } from "@nous-research/ui";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/i18n";
import { usePageHeader } from "@/contexts/usePageHeader";
import { PluginSlot } from "@/plugins";
@@ -207,13 +207,15 @@ export default function SkillsPage() {
onChange={(e) => setSearch(e.target.value)}
/>
{search && (
<button
type="button"
className="absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
<Button
ghost
size="xs"
className="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setSearch("")}
aria-label={t.common.clear}
>
<X className="h-3 w-3" />
</button>
<X />
</Button>
)}
</div>,
);
@@ -297,22 +299,13 @@ export default function SkillsPage() {
const isActive = activeCategory === key;
return (
<button
<ListItem
key={key}
type="button"
active={isActive}
onClick={() =>
setActiveCategory(isActive ? null : key)
}
className={`
group flex items-center gap-2 px-2 py-1
rounded-sm text-left text-[11px] cursor-pointer
transition-colors
${
isActive
? "bg-foreground/10 text-foreground"
: "text-muted-foreground hover:text-foreground hover:bg-foreground/5"
}
`}
className="rounded-sm px-2 py-1 text-[11px]"
>
<span className="flex-1 truncate">{name}</span>
<span
@@ -324,7 +317,7 @@ export default function SkillsPage() {
>
{count}
</span>
</button>
</ListItem>
);
})}
</div>
@@ -535,24 +528,18 @@ function SkillRow({
function PanelItem({ active, icon: Icon, label, onClick }: PanelItemProps) {
return (
<button
type="button"
<ListItem
active={active}
onClick={onClick}
className={`
group flex items-center gap-2 px-2.5 py-1.5
font-mondwest text-[0.7rem] tracking-[0.08em] uppercase
rounded-sm text-left cursor-pointer whitespace-nowrap
transition-colors
${
active
? "bg-foreground/90 text-background"
: "text-muted-foreground hover:text-foreground hover:bg-foreground/10"
}
`}
className={cn(
"rounded-sm whitespace-nowrap px-2.5 py-1.5",
"font-mondwest text-[0.7rem] tracking-[0.08em] uppercase",
active && "bg-foreground/90 text-background hover:text-background",
)}
>
<Icon className="h-3.5 w-3.5 shrink-0" />
<span className="flex-1 truncate">{label}</span>
</button>
</ListItem>
);
}

View File

@@ -24,7 +24,7 @@ import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Tabs, TabsList, TabsTrigger } from "@nous-research/ui";
import { useI18n } from "@/i18n";
import { registerSlot, PluginSlot } from "./slots";