refactor: replace inline layout patterns with SettingsCard/FieldRow/StatusBanner (PR3)

Settings pages:
- GeneralSection: 3 inline cards → SettingsCard, 3 field rows → FieldRow,
  warning banner → StatusBanner
- AppearanceSection: card → SettingsCard, mode/family rows → FieldRow,
  native <button> in ThemeModePills → Button, UIPreview buttons → Button

Bridge pages:
- BridgeSection: 5 inline cards → SettingsCard, master toggle/autostart → FieldRow,
  active hint → StatusBanner
- TelegramBridgeSection: 3 cards → SettingsCard, verify result → StatusBanner
- DiscordBridgeSection: 4 cards → SettingsCard, require mention/stream → FieldRow,
  verify result → StatusBanner
- FeishuBridgeSection: 4 cards → SettingsCard, require mention → FieldRow,
  verify result → StatusBanner
- QqBridgeSection: 4 cards → SettingsCard, image toggle → FieldRow,
  verify result → StatusBanner

Pure UI replacement — no data logic changes. All files retain identical
behavior and visual output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-11 21:50:20 +08:00
1 parent 6ea323b92f
commit d9f2e6d614
7 files changed
+210 -341

No files matched your search

+26 -33
View File
@@ -17,7 +17,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { SettingsCard } from "@/components/patterns/SettingsCard";
import { FieldRow } from "@/components/patterns/FieldRow";
// ── Theme Mode Pill Selector ────────────────────────────────────────
@@ -40,22 +43,23 @@ function ThemeModePills({
{MODE_OPTIONS.map((opt) => {
const selected = value === opt.value;
return (
<button
<Button
key={opt.value}
variant="ghost"
size="sm"
role="radio"
aria-checked={selected}
onClick={() => onChange(opt.value)}
className={cn(
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
"gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all h-auto",
selected
? "bg-primary text-primary-foreground shadow-sm"
? "bg-primary text-primary-foreground shadow-sm hover:bg-primary hover:text-primary-foreground"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<opt.icon size={14} />
{t(opt.labelKey)}
</button>
</Button>
);
})}
</div>
@@ -113,15 +117,9 @@ function ShikiCodePreview({ isDark }: { isDark: boolean }) {
function UIPreview() {
return (
<div className="flex flex-wrap gap-2">
<button className="rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
Primary
</button>
<button className="rounded-md bg-secondary px-3 py-1 text-xs font-medium text-secondary-foreground">
Secondary
</button>
<button className="rounded-md bg-destructive px-3 py-1 text-xs font-medium text-primary-foreground">
Destructive
</button>
<Button size="sm" className="text-xs h-auto py-1">Primary</Button>
<Button size="sm" variant="secondary" className="text-xs h-auto py-1">Secondary</Button>
<Button size="sm" variant="destructive" className="text-xs h-auto py-1">Destructive</Button>
<span className="inline-flex items-center rounded-full bg-accent px-2.5 py-0.5 text-[10px] font-medium text-accent-foreground">
Badge
</span>
@@ -159,15 +157,14 @@ export function AppearanceSection() {
<p className="text-xs text-muted-foreground">{t("settings.appearanceDesc")}</p>
</div>
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
<SettingsCard>
{/* Mode */}
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<h3 className="text-xs font-medium">{t("settings.themeMode")}</h3>
<p className="text-[11px] text-muted-foreground">{t("settings.themeModeDesc")}</p>
</div>
<FieldRow
label={t("settings.themeMode")}
description={t("settings.themeModeDesc")}
>
<ThemeModePills value={theme || "system"} onChange={setTheme} />
</div>
</FieldRow>
{theme === "system" && resolvedTheme && (
<p className="text-[11px] text-muted-foreground pl-1">
@@ -175,14 +172,12 @@ export function AppearanceSection() {
</p>
)}
<div className="border-t border-border/30" />
{/* Family */}
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<h3 className="text-xs font-medium">{t("settings.themeFamily")}</h3>
<p className="text-[11px] text-muted-foreground">{t("settings.themeFamilyDesc")}</p>
</div>
<FieldRow
label={t("settings.themeFamily")}
description={t("settings.themeFamilyDesc")}
separator
>
<Select value={family} onValueChange={setFamily}>
<SelectTrigger className="w-[200px]">
<SelectValue />
@@ -209,17 +204,15 @@ export function AppearanceSection() {
))}
</SelectContent>
</Select>
</div>
<div className="border-t border-border/30" />
</FieldRow>
{/* Preview */}
<div className="space-y-3">
<div className="border-t border-border/30 pt-4 space-y-3">
<h3 className="text-xs font-medium text-muted-foreground">Preview</h3>
<UIPreview />
<ShikiCodePreview isDark={isDark} />
</div>
</div>
</SettingsCard>
</div>
);
}
+29 -34
View File
@@ -19,6 +19,9 @@ import { useTranslation } from "@/hooks/useTranslation";
import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
import type { TranslationKey } from "@/i18n";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { SettingsCard } from "@/components/patterns/SettingsCard";
import { FieldRow } from "@/components/patterns/FieldRow";
import { StatusBanner } from "@/components/patterns/StatusBanner";
import { AppearanceSection } from "./AppearanceSection";
function UpdateCard() {
@@ -30,7 +33,7 @@ function UpdateCard() {
&& updateInfo.downloadProgress != null;
return (
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<SettingsCard>
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.codepilot')}</h2>
@@ -114,7 +117,7 @@ function UpdateCard() {
)}
</div>
)}
</div>
</SettingsCard>
);
}
@@ -210,36 +213,31 @@ export function GeneralSection() {
<UpdateCard />
{/* General settings card */}
<div className={`rounded-lg border p-4 space-y-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
<SettingsCard className={skipPermissions ? "border-orange-500/50 bg-orange-500/5" : undefined}>
{/* Auto-approve toggle */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.autoApproveTitle')}</h2>
<p className="text-xs text-muted-foreground">
{t('settings.autoApproveDesc')}
</p>
</div>
<FieldRow
label={t('settings.autoApproveTitle')}
description={t('settings.autoApproveDesc')}
>
<Switch
checked={skipPermissions}
onCheckedChange={handleSkipPermToggle}
disabled={skipPermSaving}
/>
</div>
</FieldRow>
{skipPermissions && (
<div className="flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500" />
<StatusBanner variant="warning">
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500 inline-block mr-1" />
{t('settings.autoApproveWarning')}
</div>
</StatusBanner>
)}
<div className="border-t border-border/30" />
{/* Language picker */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.language')}</h2>
<p className="text-xs text-muted-foreground">{t('settings.languageDesc')}</p>
</div>
<FieldRow
label={t('settings.language')}
description={t('settings.languageDesc')}
separator
>
<Select value={locale} onValueChange={(v) => setLocale(v as Locale)}>
<SelectTrigger className="w-[140px]">
<SelectValue />
@@ -250,16 +248,14 @@ export function GeneralSection() {
))}
</SelectContent>
</Select>
</div>
<div className="border-t border-border/30" />
</FieldRow>
{/* Thinking mode */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.thinkingMode' as TranslationKey)}</h2>
<p className="text-xs text-muted-foreground">{t('settings.thinkingModeDesc' as TranslationKey)}</p>
</div>
<FieldRow
label={t('settings.thinkingMode' as TranslationKey)}
description={t('settings.thinkingModeDesc' as TranslationKey)}
separator
>
<Select value={thinkingMode} onValueChange={saveThinkingMode}>
<SelectTrigger className="w-[140px]">
<SelectValue />
@@ -270,16 +266,15 @@ export function GeneralSection() {
<SelectItem value="disabled">{t('settings.thinkingDisabled' as TranslationKey)}</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</FieldRow>
</SettingsCard>
{/* Appearance */}
<AppearanceSection />
{/* Account info */}
{accountInfo && (
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<h2 className="text-sm font-medium mb-2">{t('settings.accountInfo' as TranslationKey)}</h2>
<SettingsCard title={t('settings.accountInfo' as TranslationKey)}>
<div className="space-y-1">
{accountInfo.email && (
<p className="text-xs text-muted-foreground">
@@ -297,7 +292,7 @@ export function GeneralSection() {
</p>
)}
</div>
</div>
</SettingsCard>
)}
{/* Skip-permissions warning dialog */}