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:
YoVinchen
2025-11-21 09:31:36 +08:00
parent 977185e2d5
commit 17cf701bad
11 changed files with 1006 additions and 813 deletions

View File

@@ -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

View File

@@ -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)",
},
}),
);
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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}

View File

@@ -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,
)}

View File

@@ -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;

View File

@@ -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: [],