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:
1 parent
6ea323b92f
commit
d9f2e6d614
7 files changed
+210
-341
No files matched your search
@@ -15,6 +15,9 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { SpinnerGap, CheckCircle, Warning, TelegramLogo, ChatTeardrop, GameController, ChatsCircle } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SettingsCard } from "@/components/patterns/SettingsCard";
|
||||
import { FieldRow } from "@/components/patterns/FieldRow";
|
||||
import { StatusBanner } from "@/components/patterns/StatusBanner";
|
||||
import type { ProviderModelGroup } from "@/types";
|
||||
|
||||
interface AdapterStatus {
|
||||
@@ -253,37 +256,28 @@ export function BridgeSection() {
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Enable/Disable Master Toggle */}
|
||||
<div
|
||||
className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${
|
||||
isEnabled
|
||||
? "border-primary/50 bg-primary/5"
|
||||
: "border-border/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("bridge.title")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.description")}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsCard className={isEnabled ? "border-primary/50 bg-primary/5" : undefined}>
|
||||
<FieldRow
|
||||
label={t("bridge.title")}
|
||||
description={t("bridge.description")}
|
||||
>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
onCheckedChange={handleToggleEnabled}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
{isEnabled && (
|
||||
<div className="mt-3 flex items-center gap-2 rounded-md bg-primary/10 px-3 py-2 text-xs text-primary">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
|
||||
<StatusBanner variant="info" className="bg-primary/10 text-primary">
|
||||
<span className="h-2 w-2 shrink-0 rounded-full bg-primary inline-block mr-1" />
|
||||
{t("bridge.activeHint")}
|
||||
</div>
|
||||
</StatusBanner>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Bridge Status + Start/Stop */}
|
||||
{isEnabled && (
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<SettingsCard>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("bridge.status")}</h2>
|
||||
@@ -338,31 +332,22 @@ export function BridgeSection() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* Channel Toggles */}
|
||||
{isEnabled && (
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("bridge.channels")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.channelsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("bridge.channels")}
|
||||
description={t("bridge.channelsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<TelegramLogo
|
||||
size={16}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<TelegramLogo size={16} className="text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm">{t("bridge.telegramChannel")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.telegramChannelDesc")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("bridge.telegramChannelDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -374,15 +359,10 @@ export function BridgeSection() {
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border/30 pt-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<ChatTeardrop
|
||||
size={16}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<ChatTeardrop size={16} className="text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm">{t("bridge.feishuChannel")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.feishuChannelDesc")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("bridge.feishuChannelDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -394,15 +374,10 @@ export function BridgeSection() {
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border/30 pt-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<GameController
|
||||
size={16}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<GameController size={16} className="text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm">{t("bridge.discordChannel")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.discordChannelDesc")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("bridge.discordChannelDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -414,15 +389,10 @@ export function BridgeSection() {
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border/30 pt-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<ChatsCircle
|
||||
size={16}
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<ChatsCircle size={16} className="text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm">{t("bridge.qqChannel")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.qqChannelDesc")}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t("bridge.qqChannelDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
@@ -432,33 +402,27 @@ export function BridgeSection() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border/30 pt-3">
|
||||
<div>
|
||||
<p className="text-sm">{t("bridge.autoStart")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.autoStartDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<FieldRow
|
||||
label={t("bridge.autoStart")}
|
||||
description={t("bridge.autoStartDesc")}
|
||||
separator
|
||||
>
|
||||
<Switch
|
||||
checked={isAutoStart}
|
||||
onCheckedChange={handleToggleAutoStart}
|
||||
disabled={saving}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* Adapter Status */}
|
||||
{isEnabled && isRunning && adapterCount > 0 && (
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("bridge.adapters")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.adaptersDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("bridge.adapters")}
|
||||
description={t("bridge.adaptersDesc")}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
{bridgeStatus?.adapters.map((adapter) => (
|
||||
<div
|
||||
@@ -494,19 +458,15 @@ export function BridgeSection() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
|
||||
{/* Default Settings */}
|
||||
{isEnabled && (
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("bridge.defaults")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.defaultsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("bridge.defaults")}
|
||||
description={t("bridge.defaultsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -579,7 +539,7 @@ export function BridgeSection() {
|
||||
>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SpinnerGap, CheckCircle, Warning } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SettingsCard } from "@/components/patterns/SettingsCard";
|
||||
import { FieldRow } from "@/components/patterns/FieldRow";
|
||||
import { StatusBanner } from "@/components/patterns/StatusBanner";
|
||||
|
||||
interface DiscordBridgeSettings {
|
||||
bridge_discord_bot_token: string;
|
||||
@@ -159,14 +162,10 @@ export function DiscordBridgeSection() {
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Bot Token */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("discord.credentials")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("discord.credentialsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("discord.credentials")}
|
||||
description={t("discord.credentialsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -203,30 +202,20 @@ export function DiscordBridgeSection() {
|
||||
</div>
|
||||
|
||||
{verifyResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md px-3 py-2 text-xs ${
|
||||
verifyResult.ok
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
: "bg-red-500/10 text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
<StatusBanner
|
||||
variant={verifyResult.ok ? "success" : "error"}
|
||||
icon={verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
>
|
||||
{verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
{verifyResult.message}
|
||||
</div>
|
||||
</StatusBanner>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Allowed Users / Channels */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t("discord.allowedUsers")}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("discord.allowedUsersDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("discord.allowedUsers")}
|
||||
description={t("discord.allowedUsersDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -258,19 +247,13 @@ export function DiscordBridgeSection() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Guild & Group Settings */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t("discord.guildSettings")}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("discord.guildSettingsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("discord.guildSettings")}
|
||||
description={t("discord.guildSettingsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -306,44 +289,35 @@ export function DiscordBridgeSection() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm">{t("discord.requireMention")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("discord.requireMentionDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<FieldRow
|
||||
label={t("discord.requireMention")}
|
||||
description={t("discord.requireMentionDesc")}
|
||||
>
|
||||
<Switch
|
||||
checked={requireMention}
|
||||
onCheckedChange={setRequireMention}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
|
||||
<div className="flex items-center justify-between border-t border-border/30 pt-3">
|
||||
<div>
|
||||
<p className="text-sm">{t("discord.streamPreview")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("discord.streamPreviewDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<FieldRow
|
||||
label={t("discord.streamPreview")}
|
||||
description={t("discord.streamPreviewDesc")}
|
||||
separator
|
||||
>
|
||||
<Switch
|
||||
checked={streamEnabled}
|
||||
onCheckedChange={setStreamEnabled}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</div>
|
||||
|
||||
<Button size="sm" onClick={handleSaveGroupSettings} disabled={saving}>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Setup Guide */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<h2 className="text-sm font-medium">
|
||||
{t("discord.setupGuide")}
|
||||
</h2>
|
||||
|
||||
<SettingsCard title={t("discord.setupGuide")}>
|
||||
<div>
|
||||
<h3 className="text-xs font-medium mb-1.5">
|
||||
{t("discord.setupBotTitle")}
|
||||
@@ -370,7 +344,7 @@ export function DiscordBridgeSection() {
|
||||
<li>{t("discord.stepGuildId")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SpinnerGap, CheckCircle, Warning } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SettingsCard } from "@/components/patterns/SettingsCard";
|
||||
import { FieldRow } from "@/components/patterns/FieldRow";
|
||||
import { StatusBanner } from "@/components/patterns/StatusBanner";
|
||||
|
||||
interface FeishuBridgeSettings {
|
||||
bridge_feishu_app_id: string;
|
||||
@@ -160,14 +163,10 @@ export function FeishuBridgeSection() {
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* App Credentials */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("feishu.credentials")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("feishu.credentialsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("feishu.credentials")}
|
||||
description={t("feishu.credentialsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -238,30 +237,20 @@ export function FeishuBridgeSection() {
|
||||
</div>
|
||||
|
||||
{verifyResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md px-3 py-2 text-xs ${
|
||||
verifyResult.ok
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
: "bg-red-500/10 text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
<StatusBanner
|
||||
variant={verifyResult.ok ? "success" : "error"}
|
||||
icon={verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
>
|
||||
{verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
{verifyResult.message}
|
||||
</div>
|
||||
</StatusBanner>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Allowed Users */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t("feishu.allowedUsers")}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("feishu.allowedUsersDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("feishu.allowedUsers")}
|
||||
description={t("feishu.allowedUsersDesc")}
|
||||
>
|
||||
<div>
|
||||
<Input
|
||||
value={allowedUsers}
|
||||
@@ -273,19 +262,13 @@ export function FeishuBridgeSection() {
|
||||
{t("feishu.allowedUsersHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Group Chat Settings */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">
|
||||
{t("feishu.groupSettings")}
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("feishu.groupSettingsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("feishu.groupSettings")}
|
||||
description={t("feishu.groupSettingsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -326,30 +309,24 @@ export function FeishuBridgeSection() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm">{t("feishu.requireMention")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("feishu.requireMentionDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<FieldRow
|
||||
label={t("feishu.requireMention")}
|
||||
description={t("feishu.requireMentionDesc")}
|
||||
>
|
||||
<Switch
|
||||
checked={requireMention}
|
||||
onCheckedChange={setRequireMention}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
</div>
|
||||
|
||||
<Button size="sm" onClick={handleSaveGroupSettings} disabled={saving}>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Setup Guide */}
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<h2 className="text-sm font-medium mb-2">
|
||||
{t("feishu.setupGuide")}
|
||||
</h2>
|
||||
<SettingsCard title={t("feishu.setupGuide")}>
|
||||
<ol className="text-xs text-muted-foreground space-y-1.5 list-decimal pl-4">
|
||||
<li>{t("feishu.step1")}</li>
|
||||
<li>{t("feishu.step2")}</li>
|
||||
@@ -358,7 +335,7 @@ export function FeishuBridgeSection() {
|
||||
<li>{t("feishu.step5")}</li>
|
||||
<li>{t("feishu.step6")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,9 @@ import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { SpinnerGap, CheckCircle, Warning } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SettingsCard } from "@/components/patterns/SettingsCard";
|
||||
import { FieldRow } from "@/components/patterns/FieldRow";
|
||||
import { StatusBanner } from "@/components/patterns/StatusBanner";
|
||||
|
||||
interface QqBridgeSettings {
|
||||
bridge_qq_app_id: string;
|
||||
@@ -144,14 +147,10 @@ export function QqBridgeSection() {
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* App Credentials */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("qq.credentials")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("qq.credentialsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("qq.credentials")}
|
||||
description={t("qq.credentialsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -200,28 +199,20 @@ export function QqBridgeSection() {
|
||||
</div>
|
||||
|
||||
{verifyResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md px-3 py-2 text-xs ${
|
||||
verifyResult.ok
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
: "bg-red-500/10 text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
<StatusBanner
|
||||
variant={verifyResult.ok ? "success" : "error"}
|
||||
icon={verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
>
|
||||
{verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
{verifyResult.message}
|
||||
</div>
|
||||
</StatusBanner>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Allowed Users */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("qq.allowedUsers")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("qq.allowedUsersDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("qq.allowedUsers")}
|
||||
description={t("qq.allowedUsersDesc")}
|
||||
>
|
||||
<div>
|
||||
<Input
|
||||
value={allowedUsers}
|
||||
@@ -237,30 +228,23 @@ export function QqBridgeSection() {
|
||||
<Button size="sm" onClick={handleSaveAllowedUsers} disabled={saving}>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Image Settings */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("qq.imageSettings")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("qq.imageSettingsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("qq.imageSettings")}
|
||||
description={t("qq.imageSettingsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm">{t("qq.imageEnabled")}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("qq.imageEnabledDesc")}
|
||||
</p>
|
||||
</div>
|
||||
<FieldRow
|
||||
label={t("qq.imageEnabled")}
|
||||
description={t("qq.imageEnabledDesc")}
|
||||
>
|
||||
<Switch
|
||||
checked={imageEnabled}
|
||||
onCheckedChange={setImageEnabled}
|
||||
/>
|
||||
</div>
|
||||
</FieldRow>
|
||||
|
||||
{imageEnabled && (
|
||||
<div>
|
||||
@@ -284,11 +268,10 @@ export function QqBridgeSection() {
|
||||
<Button size="sm" onClick={handleSaveImageSettings} disabled={saving}>
|
||||
{saving ? t("common.loading") : t("common.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Setup Guide */}
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<h2 className="text-sm font-medium mb-2">{t("qq.setupGuide")}</h2>
|
||||
<SettingsCard title={t("qq.setupGuide")}>
|
||||
<ol className="text-xs text-muted-foreground space-y-1.5 list-decimal pl-4">
|
||||
<li>{t("qq.step1")}</li>
|
||||
<li>{t("qq.step2")}</li>
|
||||
@@ -296,7 +279,7 @@ export function QqBridgeSection() {
|
||||
<li>{t("qq.step4")}</li>
|
||||
<li>{t("qq.step5")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SpinnerGap, CheckCircle, Warning } from "@phosphor-icons/react";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import { SettingsCard } from "@/components/patterns/SettingsCard";
|
||||
import { StatusBanner } from "@/components/patterns/StatusBanner";
|
||||
|
||||
interface TelegramBridgeSettings {
|
||||
telegram_bot_token: string;
|
||||
@@ -172,14 +174,10 @@ export function TelegramBridgeSection() {
|
||||
return (
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Bot Credentials */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("telegram.credentials")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("telegram.credentialsDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("telegram.credentials")}
|
||||
description={t("telegram.credentialsDesc")}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="text-xs font-medium text-muted-foreground mb-1 block">
|
||||
@@ -252,28 +250,20 @@ export function TelegramBridgeSection() {
|
||||
</div>
|
||||
|
||||
{verifyResult && (
|
||||
<div
|
||||
className={`flex items-center gap-2 rounded-md px-3 py-2 text-xs ${
|
||||
verifyResult.ok
|
||||
? "bg-green-500/10 text-green-600 dark:text-green-400"
|
||||
: "bg-red-500/10 text-red-600 dark:text-red-400"
|
||||
}`}
|
||||
<StatusBanner
|
||||
variant={verifyResult.ok ? "success" : "error"}
|
||||
icon={verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
>
|
||||
{verifyResult.ok ? <CheckCircle size={16} className="shrink-0" /> : <Warning size={16} className="shrink-0" />}
|
||||
{verifyResult.message}
|
||||
</div>
|
||||
</StatusBanner>
|
||||
)}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Allowed Users */}
|
||||
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
|
||||
<div>
|
||||
<h2 className="text-sm font-medium">{t("bridge.allowedUsers")}</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("bridge.allowedUsersDesc")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsCard
|
||||
title={t("bridge.allowedUsers")}
|
||||
description={t("bridge.allowedUsersDesc")}
|
||||
>
|
||||
<div>
|
||||
<Input
|
||||
value={allowedUsers}
|
||||
@@ -285,13 +275,10 @@ export function TelegramBridgeSection() {
|
||||
{t("bridge.allowedUsersHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
{/* Setup Guide */}
|
||||
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
|
||||
<h2 className="text-sm font-medium mb-2">
|
||||
{t("telegram.setupGuide")}
|
||||
</h2>
|
||||
<SettingsCard title={t("telegram.setupGuide")}>
|
||||
<ol className="text-xs text-muted-foreground space-y-1.5 list-decimal pl-4">
|
||||
<li>{t("telegram.step1")}</li>
|
||||
<li>{t("telegram.step2")}</li>
|
||||
@@ -300,7 +287,7 @@ export function TelegramBridgeSection() {
|
||||
<li>{t("telegram.step5")}</li>
|
||||
<li>{t("telegram.step6")}</li>
|
||||
</ol>
|
||||
</div>
|
||||
</SettingsCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
Reference in new issue
Block a user