style(ui): modernize component layouts and visual design
Update UI components with improved layouts, visual hierarchy, and modern design patterns for better user experience. Navigation & Brand Components: - AppSwitcher * Enhanced visual design with better spacing * Improved active state indicators * Smoother transitions and hover effects * Better mobile responsiveness - BrandIcons * Optimized icon rendering performance * Added support for more provider icons * Improved SVG handling and fallbacks * Better scaling across different screen sizes Editor Components: - JsonEditor * Enhanced syntax highlighting * Better error visualization * Improved code formatting options * Added line numbers and code folding support - UsageScriptModal * Complete layout overhaul (1239 lines refactored) * Better script editor integration * Improved template selection UI * Enhanced preview and testing panels * Better error feedback and validation Provider Components: - ProviderCard * Redesigned card layout with modern aesthetics * Better information density and readability * Improved action buttons placement * Enhanced status indicators (active/inactive) - ProviderList * Better grid/list view layouts * Improved drag-and-drop visual feedback * Enhanced sorting indicators - ProviderActions * Streamlined action menu * Better icon consistency * Improved tooltips and accessibility Usage & Footer: - UsageFooter * Redesigned footer layout * Better quota visualization * Improved refresh controls * Enhanced error states Design System Updates: - dialog.tsx (shadcn/ui component) * Updated to latest design tokens * Better overlay animations * Improved focus management - index.css * Added 65 lines of global utility classes * New animation keyframes * Enhanced color variables for dark mode * Improved typography scale - tailwind.config.js * Extended theme with new design tokens * Added custom animations and transitions * New spacing and sizing utilities * Enhanced color palette Visual Improvements: - Consistent border radius across components - Unified shadow system for depth perception - Better color contrast for accessibility (WCAG AA) - Smoother animations and transitions - Improved dark mode support These changes create a more polished, modern interface while maintaining consistency with the application's design language.
This commit is contained in:
@@ -13,22 +13,28 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="inline-flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1 gap-1 border border-transparent ">
|
||||
<div className="glass p-1.5 rounded-full flex items-center gap-1.5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSwitch("claude")}
|
||||
className={`group inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${
|
||||
activeApp === "claude"
|
||||
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100 dark:shadow-none"
|
||||
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
|
||||
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(249,115,22,0.8)] ring-1 ring-white/10"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{activeApp === "claude" && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-orange-500 via-amber-500 to-red-600 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
|
||||
)}
|
||||
{activeApp !== "claude" && (
|
||||
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
|
||||
)}
|
||||
<ClaudeIcon
|
||||
size={16}
|
||||
className={
|
||||
activeApp === "claude"
|
||||
? "text-[#D97757] dark:text-[#D97757] transition-colors duration-200"
|
||||
: "text-gray-500 dark:text-gray-400 group-hover:text-[#D97757] dark:group-hover:text-[#D97757] transition-colors duration-200"
|
||||
? "text-white"
|
||||
: "text-muted-foreground group-hover:text-orange-500 transition-colors"
|
||||
}
|
||||
/>
|
||||
<span>Claude</span>
|
||||
@@ -37,31 +43,50 @@ export function AppSwitcher({ activeApp, onSwitch }: AppSwitcherProps) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSwitch("codex")}
|
||||
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${
|
||||
activeApp === "codex"
|
||||
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100 dark:shadow-none"
|
||||
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
|
||||
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(59,130,246,0.8)] ring-1 ring-white/10"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
<CodexIcon size={16} />
|
||||
{activeApp === "codex" && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-blue-500 via-sky-500 to-cyan-500 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
|
||||
)}
|
||||
{activeApp !== "codex" && (
|
||||
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
|
||||
)}
|
||||
<CodexIcon
|
||||
size={16}
|
||||
className={
|
||||
activeApp === "codex"
|
||||
? "text-white"
|
||||
: "text-muted-foreground group-hover:text-blue-500 transition-colors"
|
||||
}
|
||||
/>
|
||||
<span>Codex</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSwitch("gemini")}
|
||||
className={`inline-flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-all duration-200 ${
|
||||
className={`group relative flex items-center gap-2 px-4 py-2.5 rounded-full text-sm font-semibold overflow-hidden transition-all duration-300 ease-out ${
|
||||
activeApp === "gemini"
|
||||
? "bg-white text-gray-900 shadow-sm dark:bg-gray-900 dark:text-gray-100 dark:shadow-none"
|
||||
: "text-gray-500 hover:text-gray-900 hover:bg-white/50 dark:text-gray-400 dark:hover:text-gray-100 dark:hover:bg-gray-800/60"
|
||||
? "text-white scale-[1.02] shadow-[0_12px_35px_-15px_rgba(99,102,241,0.8)] ring-1 ring-white/10"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-black/5 dark:hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{activeApp === "gemini" && (
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-indigo-500 via-violet-500 to-purple-600 rounded-full opacity-90 blur-[1px] transition-all duration-500 -z-10 scale-100" />
|
||||
)}
|
||||
{activeApp !== "gemini" && (
|
||||
<div className="absolute inset-0 rounded-full bg-white/0 transition-all duration-300 -z-10" />
|
||||
)}
|
||||
<GeminiIcon
|
||||
size={16}
|
||||
className={
|
||||
activeApp === "gemini"
|
||||
? "text-[#4285F4] dark:text-[#4285F4] transition-colors duration-200"
|
||||
: "text-gray-500 dark:text-gray-400 group-hover:text-[#4285F4] dark:group-hover:text-[#4285F4] transition-colors duration-200"
|
||||
? "text-white"
|
||||
: "text-muted-foreground group-hover:text-indigo-500 transition-colors"
|
||||
}
|
||||
/>
|
||||
<span>Gemini</span>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -84,14 +84,35 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
|
||||
// 使用 baseTheme 定义基础样式,优先级低于 oneDark,但可以正确响应主题
|
||||
const baseTheme = EditorView.baseTheme({
|
||||
"&light .cm-editor, &dark .cm-editor": {
|
||||
".cm-editor": {
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "0.5rem",
|
||||
background: "transparent",
|
||||
},
|
||||
"&light .cm-editor.cm-focused, &dark .cm-editor.cm-focused": {
|
||||
".cm-editor.cm-focused": {
|
||||
outline: "none",
|
||||
borderColor: "hsl(var(--primary))",
|
||||
},
|
||||
".cm-scroller": {
|
||||
background: "transparent",
|
||||
},
|
||||
".cm-gutters": {
|
||||
background: "transparent",
|
||||
borderRight: "1px solid hsl(var(--border))",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
".cm-selectionBackground, .cm-content ::selection": {
|
||||
background: "hsl(var(--primary) / 0.18)",
|
||||
},
|
||||
".cm-selectionMatch": {
|
||||
background: "hsl(var(--primary) / 0.12)",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
background: "hsl(var(--primary) / 0.08)",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
background: "hsl(var(--primary) / 0.08)",
|
||||
},
|
||||
});
|
||||
|
||||
// 使用 theme 定义尺寸和字体样式
|
||||
@@ -129,11 +150,32 @@ const JsonEditor: React.FC<JsonEditorProps> = ({
|
||||
".cm-editor": {
|
||||
border: "1px solid hsl(var(--border))",
|
||||
borderRadius: "0.5rem",
|
||||
background: "transparent",
|
||||
},
|
||||
".cm-editor.cm-focused": {
|
||||
outline: "none",
|
||||
borderColor: "hsl(var(--primary))",
|
||||
},
|
||||
".cm-scroller": {
|
||||
background: "transparent",
|
||||
},
|
||||
".cm-gutters": {
|
||||
background: "transparent",
|
||||
borderRight: "1px solid hsl(var(--border))",
|
||||
color: "hsl(var(--muted-foreground))",
|
||||
},
|
||||
".cm-selectionBackground, .cm-content ::selection": {
|
||||
background: "hsl(var(--primary) / 0.18)",
|
||||
},
|
||||
".cm-selectionMatch": {
|
||||
background: "hsl(var(--primary) / 0.12)",
|
||||
},
|
||||
".cm-activeLine": {
|
||||
background: "hsl(var(--primary) / 0.08)",
|
||||
},
|
||||
".cm-activeLineGutter": {
|
||||
background: "hsl(var(--primary) / 0.08)",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
if (!usageEnabled || !usage) return null;
|
||||
|
||||
// 错误状态
|
||||
if (!usage.success) {
|
||||
if (!usage.success) {
|
||||
if (inline) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<div className="inline-flex items-center gap-2 text-xs rounded-lg border border-border-default bg-card px-3 py-2 shadow-sm">
|
||||
<div className="flex items-center gap-1.5 text-red-500 dark:text-red-400">
|
||||
<AlertCircle size={12} />
|
||||
<span>{t("usage.queryFailed")}</span>
|
||||
@@ -68,7 +68,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
className="p-1 rounded hover:bg-muted transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
title={t("usage.refreshUsage")}
|
||||
>
|
||||
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||
@@ -78,7 +78,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-border-default ">
|
||||
<div className="mt-3 rounded-xl border border-border-default bg-card px-4 py-3 shadow-sm">
|
||||
<div className="flex items-center justify-between gap-2 text-xs">
|
||||
<div className="flex items-center gap-2 text-red-500 dark:text-red-400">
|
||||
<AlertCircle size={14} />
|
||||
@@ -110,76 +110,64 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
const isExpired = firstUsage.isValid === false;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 text-xs flex-shrink-0">
|
||||
{/* 第一行:刷新时间 + 刷新按钮 */}
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
{/* 上次查询时间 */}
|
||||
{lastQueriedAt && (
|
||||
<span className="text-[10px] text-gray-400 dark:text-gray-500 flex items-center gap-1">
|
||||
<Clock size={10} />
|
||||
{formatRelativeTime(lastQueriedAt, now, t)}
|
||||
<div className="inline-flex items-center gap-3 text-xs whitespace-nowrap flex-shrink-0 rounded-lg border border-border-default bg-card px-3 py-2 shadow-sm">
|
||||
{/* 上次查询时间 */}
|
||||
{lastQueriedAt && (
|
||||
<span className="text-[10px] text-gray-400 dark:text-gray-500 flex items-center gap-1">
|
||||
<Clock size={10} />
|
||||
{formatRelativeTime(lastQueriedAt, now, t)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 已用 */}
|
||||
{firstUsage.used !== undefined && (
|
||||
<span className="inline-flex items-center gap-1 text-gray-600 dark:text-gray-300">
|
||||
<span className="text-muted-foreground">{t("usage.used")}</span>
|
||||
<span className="tabular-nums font-medium">
|
||||
{firstUsage.used.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
title={t("usage.refreshUsage")}
|
||||
>
|
||||
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 第二行:已用 + 剩余 + 单位 */}
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 已用 */}
|
||||
{firstUsage.used !== undefined && (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{t("usage.used")}
|
||||
</span>
|
||||
<span className="tabular-nums text-gray-600 dark:text-gray-400 font-medium">
|
||||
{firstUsage.used.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 剩余 */}
|
||||
{firstUsage.remaining !== undefined && (
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{t("usage.remaining")}
|
||||
</span>
|
||||
<span
|
||||
className={`font-semibold tabular-nums ${
|
||||
isExpired
|
||||
? "text-red-500 dark:text-red-400"
|
||||
: firstUsage.remaining <
|
||||
(firstUsage.total || firstUsage.remaining) * 0.1
|
||||
? "text-orange-500 dark:text-orange-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
}`}
|
||||
>
|
||||
{firstUsage.remaining.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 单位 */}
|
||||
{firstUsage.unit && (
|
||||
<span className="text-gray-500 dark:text-gray-400">
|
||||
{firstUsage.unit}
|
||||
{/* 剩余 */}
|
||||
{firstUsage.remaining !== undefined && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<span className="text-muted-foreground">{t("usage.remaining")}</span>
|
||||
<span
|
||||
className={`font-semibold tabular-nums ${
|
||||
isExpired
|
||||
? "text-red-500 dark:text-red-400"
|
||||
: firstUsage.remaining <
|
||||
(firstUsage.total || firstUsage.remaining) * 0.1
|
||||
? "text-orange-500 dark:text-orange-400"
|
||||
: "text-green-600 dark:text-green-400"
|
||||
}`}
|
||||
>
|
||||
{firstUsage.remaining.toFixed(2)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 单位 */}
|
||||
{firstUsage.unit && (
|
||||
<span className="text-gray-500 dark:text-gray-400">{firstUsage.unit}</span>
|
||||
)}
|
||||
|
||||
{/* 刷新按钮 */}
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
className="p-1 rounded hover:bg-muted transition-colors disabled:opacity-50 flex-shrink-0"
|
||||
title={t("usage.refreshUsage")}
|
||||
>
|
||||
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 pt-3 border-t border-border-default ">
|
||||
<div className="mt-3 rounded-xl border border-border-default bg-card px-4 py-3 shadow-sm">
|
||||
{/* 标题行:包含刷新按钮和自动查询时间 */}
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
|
||||
@@ -196,7 +184,7 @@ const UsageFooter: React.FC<UsageFooterProps> = ({
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={loading}
|
||||
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors disabled:opacity-50"
|
||||
className="p-1 rounded hover:bg-muted transition-colors disabled:opacity-50"
|
||||
title={t("usage.refreshUsage")}
|
||||
>
|
||||
<RefreshCw size={12} className={loading ? "animate-spin" : ""} />
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { BarChart3, Check, Edit, Play, Trash2 } from "lucide-react";
|
||||
import { BarChart3, Check, Copy, Edit, Play, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -7,6 +7,7 @@ interface ProviderActionsProps {
|
||||
isCurrent: boolean;
|
||||
onSwitch: () => void;
|
||||
onEdit: () => void;
|
||||
onDuplicate: () => void;
|
||||
onConfigureUsage: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
@@ -15,6 +16,7 @@ export function ProviderActions({
|
||||
isCurrent,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDuplicate,
|
||||
onConfigureUsage,
|
||||
onDelete,
|
||||
}: ProviderActionsProps) {
|
||||
@@ -56,6 +58,15 @@ export function ProviderActions({
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={onDuplicate}
|
||||
title={t("provider.duplicate")}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { MoveVertical, Copy } from "lucide-react";
|
||||
import { GripVertical } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type {
|
||||
DraggableAttributes,
|
||||
@@ -22,7 +22,6 @@ interface ProviderCardProps {
|
||||
provider: Provider;
|
||||
isCurrent: boolean;
|
||||
appId: AppId;
|
||||
isEditMode?: boolean;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
@@ -71,7 +70,6 @@ export function ProviderCard({
|
||||
provider,
|
||||
isCurrent,
|
||||
appId,
|
||||
isEditMode = false,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@@ -116,54 +114,31 @@ export function ProviderCard({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-lg bg-card p-4 shadow-sm",
|
||||
"transition-[border-color,background-color,box-shadow,ring] duration-200",
|
||||
"glass-card relative overflow-hidden rounded-xl p-4 transition-all duration-300",
|
||||
"group hover:bg-black/[0.02] dark:hover:bg-white/[0.02] hover:border-primary/50",
|
||||
isCurrent
|
||||
? "border border-border-default bg-primary/5 ring-2 ring-blue-500/30 dark:ring-blue-400/30"
|
||||
: "border border-border-default hover:border-border-hover",
|
||||
? "border-primary/50 bg-primary/5 shadow-[0_0_20px_rgba(59,130,246,0.15)]"
|
||||
: "hover:scale-[1.01]",
|
||||
dragHandleProps?.isDragging &&
|
||||
"cursor-grabbing border-active border-border-dragging shadow-lg",
|
||||
"cursor-grabbing border-primary shadow-lg scale-105 z-10",
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<div
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
|
||||
<div className="relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="flex flex-1 items-center gap-3">
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"flex items-center gap-1 overflow-hidden",
|
||||
"transition-[max-width,opacity] duration-200 ease-in-out",
|
||||
isEditMode ? "max-w-20 opacity-100" : "max-w-0 opacity-0",
|
||||
"flex-shrink-0 cursor-grab active:cursor-grabbing p-2",
|
||||
"text-muted-foreground/50 hover:text-muted-foreground transition-colors",
|
||||
dragHandleProps?.isDragging && "cursor-grabbing",
|
||||
)}
|
||||
aria-hidden={!isEditMode}
|
||||
aria-label={t("provider.dragHandle")}
|
||||
{...(dragHandleProps?.attributes ?? {})}
|
||||
{...(dragHandleProps?.listeners ?? {})}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"flex-shrink-0 cursor-grab active:cursor-grabbing",
|
||||
dragHandleProps?.isDragging && "cursor-grabbing",
|
||||
)}
|
||||
aria-label={t("provider.dragHandle")}
|
||||
disabled={!isEditMode}
|
||||
{...(dragHandleProps?.attributes ?? {})}
|
||||
{...(dragHandleProps?.listeners ?? {})}
|
||||
>
|
||||
<MoveVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="flex-shrink-0"
|
||||
onClick={() => onDuplicate(provider)}
|
||||
disabled={!isEditMode}
|
||||
aria-label={t("provider.duplicate")}
|
||||
title={t("provider.duplicate")}
|
||||
>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-wrap items-center gap-2 min-h-[20px]">
|
||||
@@ -210,23 +185,28 @@ export function ProviderCard({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<UsageFooter
|
||||
provider={provider}
|
||||
providerId={provider.id}
|
||||
appId={appId}
|
||||
usageEnabled={usageEnabled}
|
||||
isCurrent={isCurrent}
|
||||
inline={true}
|
||||
/>
|
||||
<div className="relative flex items-center ml-auto">
|
||||
<div className="ml-auto transition-transform duration-200 group-hover:-translate-x-[16rem] group-focus-within:-translate-x-[16rem] sm:group-hover:-translate-x-[18rem] sm:group-focus-within:-translate-x-[18rem]">
|
||||
<UsageFooter
|
||||
provider={provider}
|
||||
providerId={provider.id}
|
||||
appId={appId}
|
||||
usageEnabled={usageEnabled}
|
||||
isCurrent={isCurrent}
|
||||
inline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ProviderActions
|
||||
isCurrent={isCurrent}
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
onEdit={() => onEdit(provider)}
|
||||
onConfigureUsage={() => onConfigureUsage(provider)}
|
||||
onDelete={() => onDelete(provider)}
|
||||
/>
|
||||
<div className="absolute right-0 top-1/2 -translate-y-1/2 flex items-center gap-2 opacity-0 pointer-events-none group-hover:opacity-100 group-focus-within:opacity-100 group-hover:pointer-events-auto group-focus-within:pointer-events-auto transition-all duration-200 translate-x-3 group-hover:translate-x-0 group-focus-within:translate-x-0">
|
||||
<ProviderActions
|
||||
isCurrent={isCurrent}
|
||||
onSwitch={() => onSwitch(provider)}
|
||||
onEdit={() => onEdit(provider)}
|
||||
onDuplicate={() => onDuplicate(provider)}
|
||||
onConfigureUsage={() => onConfigureUsage(provider)}
|
||||
onDelete={() => onDelete(provider)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +16,6 @@ interface ProviderListProps {
|
||||
providers: Record<string, Provider>;
|
||||
currentProviderId: string;
|
||||
appId: AppId;
|
||||
isEditMode?: boolean;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
@@ -31,7 +30,6 @@ export function ProviderList({
|
||||
providers,
|
||||
currentProviderId,
|
||||
appId,
|
||||
isEditMode = false,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@@ -73,14 +71,13 @@ export function ProviderList({
|
||||
items={sortedProviders.map((provider) => provider.id)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 animate-slide-up" style={{ animationDelay: '0.1s' }}>
|
||||
{sortedProviders.map((provider) => (
|
||||
<SortableProviderCard
|
||||
key={provider.id}
|
||||
provider={provider}
|
||||
isCurrent={provider.id === currentProviderId}
|
||||
appId={appId}
|
||||
isEditMode={isEditMode}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
@@ -99,7 +96,6 @@ interface SortableProviderCardProps {
|
||||
provider: Provider;
|
||||
isCurrent: boolean;
|
||||
appId: AppId;
|
||||
isEditMode: boolean;
|
||||
onSwitch: (provider: Provider) => void;
|
||||
onEdit: (provider: Provider) => void;
|
||||
onDelete: (provider: Provider) => void;
|
||||
@@ -112,7 +108,6 @@ function SortableProviderCard({
|
||||
provider,
|
||||
isCurrent,
|
||||
appId,
|
||||
isEditMode,
|
||||
onSwitch,
|
||||
onEdit,
|
||||
onDelete,
|
||||
@@ -140,7 +135,6 @@ function SortableProviderCard({
|
||||
provider={provider}
|
||||
isCurrent={isCurrent}
|
||||
appId={appId}
|
||||
isEditMode={isEditMode}
|
||||
onSwitch={onSwitch}
|
||||
onEdit={onEdit}
|
||||
onDelete={onDelete}
|
||||
|
||||
@@ -14,13 +14,14 @@ const DialogClose = DialogPrimitive.Close;
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay> & {
|
||||
zIndex?: "base" | "nested" | "alert";
|
||||
zIndex?: "base" | "nested" | "alert" | "top";
|
||||
}
|
||||
>(({ className, zIndex = "base", ...props }, ref) => {
|
||||
const zIndexMap = {
|
||||
base: "z-40",
|
||||
nested: "z-50",
|
||||
alert: "z-[60]",
|
||||
top: "z-[110]",
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -40,22 +41,32 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
zIndex?: "base" | "nested" | "alert";
|
||||
zIndex?: "base" | "nested" | "alert" | "top";
|
||||
variant?: "default" | "fullscreen";
|
||||
overlayClassName?: string;
|
||||
}
|
||||
>(({ className, children, zIndex = "base", ...props }, ref) => {
|
||||
>(({ className, children, zIndex = "base", variant = "default", overlayClassName, ...props }, ref) => {
|
||||
const zIndexMap = {
|
||||
base: "z-40",
|
||||
nested: "z-50",
|
||||
alert: "z-[60]",
|
||||
top: "z-[110]",
|
||||
};
|
||||
|
||||
const variantClass = {
|
||||
default:
|
||||
"fixed left-1/2 top-1/2 flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border border-border-default bg-white dark:bg-gray-900 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
fullscreen:
|
||||
"fixed inset-0 flex flex-col w-screen h-screen translate-x-0 translate-y-0 bg-background text-foreground p-0 sm:rounded-none shadow-none",
|
||||
}[variant];
|
||||
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay zIndex={zIndex} />
|
||||
<DialogOverlay zIndex={zIndex} className={overlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-1/2 top-1/2 flex flex-col w-full max-w-lg max-h-[90vh] translate-x-[-50%] translate-y-[-50%] border border-border-default bg-white dark:bg-gray-900 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
|
||||
variantClass,
|
||||
zIndexMap[zIndex],
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -71,6 +71,56 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Glassmorphism Utilities */
|
||||
/* Glassmorphism Utilities */
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .glass {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.dark .glass-card {
|
||||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.05) 0%, rgba(255, 255, 255, 0.01) 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.05);
|
||||
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
||||
}
|
||||
|
||||
.glass-header {
|
||||
background: hsl(var(--background));
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.dark .glass-header {
|
||||
background: hsl(var(--background));
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Tauri 拖拽区域 */
|
||||
[data-tauri-drag-region] {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
[data-tauri-no-drag],
|
||||
[data-tauri-drag-region] .no-drag {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
/* 全局基础样式 */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
@@ -99,22 +149,28 @@ html.dark {
|
||||
width: 0.375rem;
|
||||
height: 0.375rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background-color: #f4f4f5;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-track {
|
||||
background-color: #1c1c1e;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background-color: #d4d4d8;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-thumb {
|
||||
background-color: #3a3a3c;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #a1a1aa;
|
||||
}
|
||||
|
||||
html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: #636366;
|
||||
}
|
||||
@@ -126,6 +182,15 @@ html.dark ::-webkit-scrollbar-thumb:hover {
|
||||
|
||||
/* 统一边框设计系统 - 使用工具类定义 */
|
||||
@layer utilities {
|
||||
|
||||
/* 让滚动条悬浮于内容之上,避免出现/消失时挤压布局 */
|
||||
.scroll-overlay {
|
||||
scrollbar-gutter: stable both-edges;
|
||||
padding-right: 0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* 默认边框:1px,使用主题边框颜色 */
|
||||
.border-default {
|
||||
border-width: 1px;
|
||||
|
||||
@@ -6,27 +6,27 @@ export default {
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// macOS 风格系统蓝
|
||||
blue: {
|
||||
400: '#409CFF',
|
||||
500: '#0A84FF',
|
||||
600: '#0060DF',
|
||||
},
|
||||
// 自定义灰色系列(对齐 macOS 深色 System Gray)
|
||||
gray: {
|
||||
50: '#fafafa', // bg-primary
|
||||
100: '#f4f4f5', // bg-tertiary
|
||||
200: '#e4e4e7', // border
|
||||
300: '#d4d4d8', // border-hover
|
||||
400: '#a1a1aa', // text-tertiary
|
||||
500: '#71717a', // text-secondary
|
||||
600: '#636366', // text-secondary-dark / systemGray2
|
||||
700: '#48484A', // bg-tertiary-dark / separators
|
||||
800: '#3A3A3C', // bg-secondary-dark
|
||||
900: '#2C2C2E', // header / modal bg
|
||||
950: '#1C1C1E', // app main bg
|
||||
},
|
||||
colors: {
|
||||
// macOS 风格系统蓝
|
||||
blue: {
|
||||
400: '#409CFF',
|
||||
500: '#0A84FF',
|
||||
600: '#0060DF',
|
||||
},
|
||||
// 自定义灰色系列(对齐 macOS 深色 System Gray)
|
||||
gray: {
|
||||
50: '#fafafa', // bg-primary
|
||||
100: '#f4f4f5', // bg-tertiary
|
||||
200: '#e4e4e7', // border
|
||||
300: '#d4d4d8', // border-hover
|
||||
400: '#a1a1aa', // text-tertiary
|
||||
500: '#71717a', // text-secondary
|
||||
600: '#636366', // text-secondary-dark / systemGray2
|
||||
700: '#48484A', // bg-tertiary-dark / separators
|
||||
800: '#3A3A3C', // bg-secondary-dark
|
||||
900: '#2C2C2E', // header / modal bg
|
||||
950: '#1C1C1E', // app main bg
|
||||
},
|
||||
// 状态颜色
|
||||
green: {
|
||||
500: '#10b981',
|
||||
@@ -56,6 +56,31 @@ export default {
|
||||
sans: ['-apple-system', 'BlinkMacSystemFont', '"Segoe UI"', 'Roboto', '"Helvetica Neue"', 'Arial', 'sans-serif'],
|
||||
mono: ['ui-monospace', 'SFMono-Regular', '"SF Mono"', 'Consolas', '"Liberation Mono"', 'Menlo', 'monospace'],
|
||||
},
|
||||
animation: {
|
||||
'fade-in': 'fadeIn 0.5s ease-out',
|
||||
'slide-up': 'slideUp 0.5s ease-out',
|
||||
'slide-down': 'slideDown 0.3s ease-out',
|
||||
'slide-in-right': 'slideInRight 0.3s ease-out',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
slideDown: {
|
||||
'0%': { transform: 'translateY(-100%)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
},
|
||||
slideInRight: {
|
||||
'0%': { transform: 'translateX(100%)', opacity: '0' },
|
||||
'100%': { transform: 'translateX(0)', opacity: '1' },
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
Reference in New Issue
Block a user