refactor(frontend): unify theme tokens to single --dg-* source

- remove shadcn HSL dual-track vars (523->416->~300 lines index.css);
  tailwind semantic colors (border/background/card/primary/destructive/
  popover/muted/accent) now bridge directly to var(--dg-*) tokens
- drop dead darkMode:['class'] config and inert dark: variants in kept
  ui components; theme switching is data-theme based (4 themes intact)
- first-frame inline theme script in index.html (reads namespaced key
  with legacy fallback, resolves system, sets color-scheme) - no flash
- consolidate duplicated db-type brand colors into lib/constants.ts
  single source (dbTypeColorMap + dbTypeBrandColorMap, with pg/mongodb
  aliases); instance health stroke now uses theme tokens
- verified via browser: dark #0d1117 / light #f5f6f8 both resolve,
  tokens cascade correctly
This commit is contained in:
Logan.Li committed 2026-10-26 17:38:27 +08:00
1 parent a213d08e31
commit ac563d1c5e
7 files changed
+99 -223

No files matched your search

+29
View File
@@ -5,6 +5,35 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>OpsxSQL</title> <title>OpsxSQL</title>
<script>
// 首帧主题初始化(指南 06 章:读取要在 app 挂载前,防首屏闪烁)
// 与 useTheme.tsx 保持同一套取值与解析逻辑
(function () {
try {
var v = localStorage.getItem('opsxsql.theme.mode');
if (!v) {
var legacy = localStorage.getItem('dg-theme');
if (legacy) {
v = legacy === 'dark' || legacy === 'midnight-purple' || legacy === 'orange-vitality'
? 'ocean-deep'
: legacy === 'light' ? 'light-professional' : legacy;
localStorage.setItem('opsxsql.theme.mode', v);
localStorage.removeItem('dg-theme');
}
}
if (!/^(system|light-professional|ocean-deep|pine-green)$/.test(v || '')) v = 'system';
var resolved =
v === 'system'
? window.matchMedia('(prefers-color-scheme: dark)').matches
? 'ocean-deep'
: 'light-professional'
: v;
document.documentElement.setAttribute('data-theme', resolved);
document.documentElement.setAttribute('data-theme-choice', v);
document.documentElement.style.colorScheme = resolved === 'light-professional' ? 'light' : 'dark';
} catch (e) { /* noop */ }
})();
</script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+4 -4
View File
@@ -5,19 +5,19 @@ import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const buttonVariants = cva( const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20:ring-destructive/40 aria-invalid:border-destructive",
{ {
variants: { variants: {
variant: { variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90", default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20:ring-destructive/40",
outline: outline:
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground:bg-input/50",
secondary: secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80", "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: ghost:
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", "hover:bg-accent hover:text-accent-foreground:bg-accent/50",
link: "text-primary underline-offset-4 hover:underline", link: "text-primary underline-offset-4 hover:underline",
}, },
size: { size: {
+3 -146
View File
@@ -3,89 +3,10 @@
@tailwind utilities; @tailwind utilities;
@layer base { @layer base {
/* ===== Theme 1: Ocean Deep (深蓝专业) — Default ===== */ /* ===== Design Tokens(--dg-* 单一事实源;多主题经 data-theme 整组覆盖) =====
tailwind 语义色(border/background/card/primary/...)由 tailwind.config.js 桥接到这些变量 */
:root { :root {
--background: 220 13% 5%; --radius: 0.5rem; /* 卡片/按钮/输入框圆角主值 */
--foreground: 210 40% 98%;
--card: 220 13% 7%;
--card-foreground: 210 40% 98%;
--popover: 220 13% 7%;
--popover-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 13% 14%;
--secondary-foreground: 210 40% 98%;
--muted: 220 13% 14%;
--muted-foreground: 215 20% 55%;
--accent: 220 13% 14%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 18%;
--input: 220 13% 14%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 220 13% 7%;
--sidebar-foreground: 210 40% 90%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 13% 14%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 220 13% 18%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #0d1117;
--dg-bg-secondary: #161b22;
--dg-bg-tertiary: #21262d;
--dg-bg-hover: #30363d;
--dg-border: #30363d;
--dg-border-light: #21262d;
--dg-text-primary: #f0f6fc;
--dg-text-secondary: #8b949e;
--dg-text-muted: #6e7681;
--dg-blue: #58a6ff;
--dg-green: #3fb950;
--dg-red: #f85149;
--dg-orange: #f0883e;
--dg-purple: #7c8aa5;
--dg-cyan: #39c5cf;
--dg-yellow: #d29922;
--dg-card-hover-border: rgba(88, 166, 255, 0.5);
--dg-gradient-start: #2563eb;
--dg-gradient-end: #0ea5e9;
--dg-gradient-text-start: #60a5fa;
--dg-gradient-text-end: #93c5fd;
--dg-input-focus-ring: rgba(96, 165, 250, 0.2);
}
[data-theme="ocean-deep"] {
--background: 220 13% 5%;
--foreground: 210 40% 98%;
--card: 220 13% 7%;
--card-foreground: 210 40% 98%;
--popover: 220 13% 7%;
--popover-foreground: 210 40% 98%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 13% 14%;
--secondary-foreground: 210 40% 98%;
--muted: 220 13% 14%;
--muted-foreground: 215 20% 55%;
--accent: 220 13% 14%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62% 50%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 18%;
--input: 220 13% 14%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 220 13% 7%;
--sidebar-foreground: 210 40% 90%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 13% 14%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 220 13% 18%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #0d1117; --dg-bg-primary: #0d1117;
--dg-bg-secondary: #161b22; --dg-bg-secondary: #161b22;
--dg-bg-tertiary: #21262d; --dg-bg-tertiary: #21262d;
@@ -113,34 +34,6 @@
/* ===== Theme 2: Light Professional (浅色商务) ===== */ /* ===== Theme 2: Light Professional (浅色商务) ===== */
[data-theme="light-professional"], [data-theme="light-professional"],
[data-theme="light"] { [data-theme="light"] {
--background: 220 14% 97%;
--foreground: 220 14% 10%;
--card: 0 0% 100%;
--card-foreground: 220 14% 10%;
--popover: 0 0% 100%;
--popover-foreground: 220 14% 10%;
--primary: 217 91% 60%;
--primary-foreground: 0 0% 100%;
--secondary: 220 14% 96%;
--secondary-foreground: 220 14% 10%;
--muted: 220 14% 96%;
--muted-foreground: 220 10% 46%;
--accent: 220 14% 96%;
--accent-foreground: 220 14% 10%;
--destructive: 0 72% 51%;
--destructive-foreground: 0 0% 100%;
--border: 220 13% 91%;
--input: 220 13% 91%;
--ring: 217 91% 60%;
--radius: 0.625rem;
--sidebar-background: 0 0% 100%;
--sidebar-foreground: 220 14% 20%;
--sidebar-primary: 217 91% 60%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 220 14% 94%;
--sidebar-accent-foreground: 220 14% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-ring: 217 91% 60%;
--dg-bg-primary: #f5f6f8; --dg-bg-primary: #f5f6f8;
--dg-bg-secondary: #ffffff; --dg-bg-secondary: #ffffff;
--dg-bg-tertiary: #eef1f5; --dg-bg-tertiary: #eef1f5;
@@ -167,34 +60,6 @@
/* ===== Theme 4: Pine Green (松绿自然) ===== */ /* ===== Theme 4: Pine Green (松绿自然) ===== */
[data-theme="pine-green"] { [data-theme="pine-green"] {
--background: 150 25% 5%;
--foreground: 140 60% 97%;
--card: 150 25% 9%;
--card-foreground: 140 60% 97%;
--popover: 150 25% 9%;
--popover-foreground: 140 60% 97%;
--primary: 142 71% 45%;
--primary-foreground: 150 25% 5%;
--secondary: 150 20% 18%;
--secondary-foreground: 140 60% 97%;
--muted: 150 20% 18%;
--muted-foreground: 150 20% 55%;
--accent: 150 20% 18%;
--accent-foreground: 140 60% 97%;
--destructive: 0 90% 72%;
--destructive-foreground: 150 25% 5%;
--border: 150 20% 18%;
--input: 150 20% 18%;
--ring: 142 71% 45%;
--radius: 0.625rem;
--sidebar-background: 150 25% 7%;
--sidebar-foreground: 140 40% 88%;
--sidebar-primary: 142 71% 45%;
--sidebar-primary-foreground: 150 25% 5%;
--sidebar-accent: 150 20% 18%;
--sidebar-accent-foreground: 140 60% 97%;
--sidebar-border: 150 20% 18%;
--sidebar-ring: 142 71% 45%;
--dg-bg-primary: #0a1210; --dg-bg-primary: #0a1210;
--dg-bg-secondary: #111f1a; --dg-bg-secondary: #111f1a;
--dg-bg-tertiary: #192c24; --dg-bg-tertiary: #192c24;
@@ -289,14 +154,6 @@
.scrollbar-thin::-webkit-scrollbar-thumb:hover { .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--dg-text-muted); background: var(--dg-text-muted);
} }
[data-theme="light-professional"] .scrollbar-thin::-webkit-scrollbar-thumb,
[data-theme="light"] .scrollbar-thin::-webkit-scrollbar-thumb {
background: #d1d5db;
}
[data-theme="light-professional"] .scrollbar-thin::-webkit-scrollbar-thumb:hover,
[data-theme="light"] .scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
/* ===== Design System Utilities ===== */ /* ===== Design System Utilities ===== */
+21 -1
View File
@@ -65,9 +65,29 @@ export const statusColorMap: Record<string, string> = {
rollback: 'bg-orange-500/15 text-orange-400', rollback: 'bg-orange-500/15 text-orange-400',
}; };
export const dbTypeColorMap: Record<string, string> = { /**
* 数据库品牌色(刻意不随主题切换,是产品语义而非主题语义)
* 全站唯一源:徽章淡色版 dbTypeColorMap / 实色版 dbTypeBrandColorMap
*/
const DB_TYPE_BADGE = {
mysql: 'bg-[rgba(0,117,143,0.15)] text-[#26a69a]', mysql: 'bg-[rgba(0,117,143,0.15)] text-[#26a69a]',
postgres: 'bg-[rgba(51,103,145,0.15)] text-[#42a5f5]', postgres: 'bg-[rgba(51,103,145,0.15)] text-[#42a5f5]',
redis: 'bg-[rgba(220,50,32,0.15)] text-[#ef5350]', redis: 'bg-[rgba(220,50,32,0.15)] text-[#ef5350]',
mongo: 'bg-[rgba(71,162,72,0.15)] text-[#66bb6a]', mongo: 'bg-[rgba(71,162,72,0.15)] text-[#66bb6a]',
}; };
export const dbTypeColorMap: Record<string, string> = {
...DB_TYPE_BADGE,
postgresql: DB_TYPE_BADGE.postgres,
pg: DB_TYPE_BADGE.postgres,
mongodb: DB_TYPE_BADGE.mongo,
};
export const dbTypeBrandColorMap: Record<string, string> = {
mysql: 'bg-[#00758f]',
postgres: 'bg-[#336791]',
postgresql: 'bg-[#336791]',
redis: 'bg-[#dc382d]',
mongo: 'bg-[#47a248]',
mongodb: 'bg-[#47a248]',
};
+6 -9
View File
@@ -7,7 +7,7 @@ import {
FileText, ArrowUpRight, ArrowDownRight, FileText, ArrowUpRight, ArrowDownRight,
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { statusMap, statusColorMap, dbTypeColorMap } from '@/lib/constants'; import { statusMap, statusColorMap, dbTypeColorMap, dbTypeBrandColorMap } from '@/lib/constants';
import { useTheme } from '@/hooks/useTheme'; import { useTheme } from '@/hooks/useTheme';
import { useAuth } from '@/hooks/useAuth'; import { useAuth } from '@/hooks/useAuth';
import { dashboardApi } from '@/api/dashboard'; import { dashboardApi } from '@/api/dashboard';
@@ -378,10 +378,7 @@ export default function Dashboard() {
return ( return (
<div key={inst.id} className="flex items-center px-5 py-3 border-b border-[var(--dg-border-light)] last:border-0 hover:bg-[var(--dg-bg-hover)] transition-colors cursor-pointer group"> <div key={inst.id} className="flex items-center px-5 py-3 border-b border-[var(--dg-border-light)] last:border-0 hover:bg-[var(--dg-bg-hover)] transition-colors cursor-pointer group">
<div className={`w-9 h-9 rounded-lg flex items-center justify-center mr-3.5 flex-shrink-0 ${ <div className={`w-9 h-9 rounded-lg flex items-center justify-center mr-3.5 flex-shrink-0 ${
inst.dbType === 'mysql' ? 'bg-[rgba(0,117,143,0.2)] text-[#26a69a]' : dbTypeColorMap[inst.dbType] || dbTypeColorMap.mysql
inst.dbType === 'redis' ? 'bg-[rgba(220,50,32,0.2)] text-[#ef5350]' :
inst.dbType === 'postgres' ? 'bg-[rgba(51,103,145,0.2)] text-[#42a5f5]' :
'bg-[rgba(71,162,72,0.2)] text-[#66bb6a]'
}`}> }`}>
<Database className="w-4.5 h-4.5" /> <Database className="w-4.5 h-4.5" />
</div> </div>
@@ -529,10 +526,10 @@ export default function Dashboard() {
<div className="space-y-2.5"> <div className="space-y-2.5">
<div className="text-xs text-[var(--dg-text-muted)] font-medium">查询类型分布</div> <div className="text-xs text-[var(--dg-text-muted)] font-medium">查询类型分布</div>
{(queryStats?.type_distribution ?? [ {(queryStats?.type_distribution ?? [
{ label: 'MySQL', count: 0, color: 'bg-[#00758f]' }, { label: 'MySQL', count: 0, color: dbTypeBrandColorMap.mysql },
{ label: 'PostgreSQL', count: 0, color: 'bg-[#336791]' }, { label: 'PostgreSQL', count: 0, color: dbTypeBrandColorMap.postgres },
{ label: 'Redis', count: 0, color: 'bg-[#dc382d]' }, { label: 'Redis', count: 0, color: dbTypeBrandColorMap.redis },
{ label: 'MongoDB', count: 0, color: 'bg-[#47a248]' }, { label: 'MongoDB', count: 0, color: dbTypeBrandColorMap.mongodb },
]).map((item: any) => { ]).map((item: any) => {
const total = queryStats?.total || 1; const total = queryStats?.total || 1;
return ( return (
+14 -13
View File
@@ -9,6 +9,7 @@ import {
} from 'lucide-react'; } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useActionPermission } from '@/hooks/useAuth'; import { useActionPermission } from '@/hooks/useAuth';
import { dbTypeColorMap } from '@/lib/constants';
import PageHeader from '@/components/PageHeader'; import PageHeader from '@/components/PageHeader';
import EmptyState from '@/components/EmptyState'; import EmptyState from '@/components/EmptyState';
import { instanceApi } from '@/api/instance'; import { instanceApi } from '@/api/instance';
@@ -51,13 +52,13 @@ type ViewMode = 'grid' | 'list';
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* DB Brand Icons */ /* DB Brand Icons */
const dbTypeConfig: Record<string, { icon: React.ElementType; color: string; bg: string; label: string }> = { const dbTypeConfig: Record<string, { icon: React.ElementType; badge: string; label: string }> = {
mysql: { icon: MySQLIcon, color: 'text-[#26a69a]', bg: 'bg-[rgba(0,117,143,0.15)]', label: 'MySQL' }, mysql: { icon: MySQLIcon, badge: dbTypeColorMap.mysql, label: 'MySQL' },
postgres: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' }, postgres: { icon: PostgreSQLIcon, badge: dbTypeColorMap.postgres, label: 'PostgreSQL' },
postgresql: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' }, postgresql: { icon: PostgreSQLIcon, badge: dbTypeColorMap.postgresql, label: 'PostgreSQL' },
pg: { icon: PostgreSQLIcon, color: 'text-[#42a5f5]', bg: 'bg-[rgba(51,103,145,0.15)]', label: 'PostgreSQL' }, pg: { icon: PostgreSQLIcon, badge: dbTypeColorMap.pg, label: 'PostgreSQL' },
redis: { icon: RedisIcon, color: 'text-[#ef5350]', bg: 'bg-[rgba(220,50,32,0.15)]', label: 'Redis' }, redis: { icon: RedisIcon, badge: dbTypeColorMap.redis, label: 'Redis' },
mongo: { icon: MongoDBIcon, color: 'text-[#66bb6a]', bg: 'bg-[rgba(71,162,72,0.15)]', label: 'MongoDB' }, mongo: { icon: MongoDBIcon, badge: dbTypeColorMap.mongo, label: 'MongoDB' },
}; };
const envLabel: Record<string, string> = { const envLabel: Record<string, string> = {
@@ -541,7 +542,7 @@ export default function InstancePage() {
</div> </div>
<div className="flex items-start justify-between mb-4 pl-6"> <div className="flex items-start justify-between mb-4 pl-6">
<div className={`w-9 h-9 rounded-lg ${ic.bg} flex items-center justify-center`}> <div className={`w-9 h-9 rounded-lg ${ic.badge} flex items-center justify-center`}>
<IconComp className="w-5 h-5 object-contain" /> <IconComp className="w-5 h-5 object-contain" />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -689,7 +690,7 @@ export default function InstancePage() {
</td> </td>
<td className="px-4 py-3 font-medium">{instance.name}</td> <td className="px-4 py-3 font-medium">{instance.name}</td>
<td className="px-4 py-3"> <td className="px-4 py-3">
<span className={`flex items-center gap-1.5 ${ic.color}`}> <span className={`flex items-center gap-1.5 ${ic.badge} px-1.5 rounded-md`}>
<IconComp className="w-4 h-4 object-contain" /> <IconComp className="w-4 h-4 object-contain" />
<span className="text-xs">{ic.label}</span> <span className="text-xs">{ic.label}</span>
</span> </span>
@@ -923,7 +924,7 @@ function QuickImportDialog({ onClose, onImported }: { onClose: () => void; onImp
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete, onEdit, onClone }: { function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete, onEdit, onClone }: {
instance: InstanceItem; onBack: () => void; instance: InstanceItem; onBack: () => void;
config: { icon: React.ElementType; color: string; bg: string }; config: { icon: React.ElementType; badge: string };
canEdit: boolean; canDelete: boolean; onDelete: () => void; onEdit: () => void; onClone?: () => void; canEdit: boolean; canDelete: boolean; onDelete: () => void; onEdit: () => void; onClone?: () => void;
}) { }) {
const [testing, setTesting] = useState(false); const [testing, setTesting] = useState(false);
@@ -965,8 +966,8 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
<ChevronLeft className="w-4 h-4" /> <ChevronLeft className="w-4 h-4" />
</button> </button>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className={`w-10 h-10 rounded-xl ${config.bg} flex items-center justify-center`}> <div className={`w-10 h-10 rounded-xl ${config.badge} flex items-center justify-center`}>
<DetailIcon className={`w-5 h-5 ${config.color}`} /> <DetailIcon className="w-5 h-5" />
</div> </div>
<div> <div>
<h1 className="text-xl font-semibold flex items-center gap-3"> <h1 className="text-xl font-semibold flex items-center gap-3">
@@ -1045,7 +1046,7 @@ function InstanceDetail({ instance, onBack, config, canEdit, canDelete, onDelete
<circle cx="50" cy="50" r="42" fill="none" stroke="var(--dg-bg-tertiary)" strokeWidth="8" /> <circle cx="50" cy="50" r="42" fill="none" stroke="var(--dg-bg-tertiary)" strokeWidth="8" />
<circle <circle
cx="50" cy="50" r="42" fill="none" cx="50" cy="50" r="42" fill="none"
stroke={health >= 90 ? '#4ade80' : health >= 70 ? '#facc15' : health >= 50 ? '#fb923c' : '#f87171'} stroke={health >= 90 ? 'var(--dg-green)' : health >= 70 ? 'var(--dg-yellow)' : health >= 50 ? 'var(--dg-orange)' : 'var(--dg-red)'}
strokeWidth="8" strokeWidth="8"
strokeLinecap="round" strokeLinecap="round"
strokeDasharray={`${(health / 100) * 264} 264`} strokeDasharray={`${(health / 100) * 264} 264`}
+22 -50
View File
@@ -1,55 +1,46 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
darkMode: ["class"], // 多主题经 data-theme 属性切换(--dg-* 单源),无 class 暗色模式
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: { theme: {
extend: { extend: {
colors: { colors: {
border: "hsl(var(--border))", // 语义色全部派生自 --dg-* 设计 Token(唯一手写源)
input: "hsl(var(--input))", border: "var(--dg-border)",
ring: "hsl(var(--ring))", input: "var(--dg-border)",
background: "hsl(var(--background))", ring: "var(--dg-blue)",
foreground: "hsl(var(--foreground))", background: "var(--dg-bg-primary)",
foreground: "var(--dg-text-primary)",
primary: { primary: {
DEFAULT: "hsl(var(--primary))", DEFAULT: "var(--dg-blue)",
foreground: "hsl(var(--primary-foreground))", foreground: "#ffffff",
}, },
secondary: { secondary: {
DEFAULT: "hsl(var(--secondary))", DEFAULT: "var(--dg-bg-tertiary)",
foreground: "hsl(var(--secondary-foreground))", foreground: "var(--dg-text-primary)",
}, },
destructive: { destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)", DEFAULT: "var(--dg-red)",
foreground: "hsl(var(--destructive-foreground) / <alpha-value>)", foreground: "#ffffff",
}, },
muted: { muted: {
DEFAULT: "hsl(var(--muted))", DEFAULT: "var(--dg-bg-tertiary)",
foreground: "hsl(var(--muted-foreground))", foreground: "var(--dg-text-muted)",
}, },
accent: { accent: {
DEFAULT: "hsl(var(--accent))", DEFAULT: "var(--dg-bg-hover)",
foreground: "hsl(var(--accent-foreground))", foreground: "var(--dg-text-primary)",
}, },
popover: { popover: {
DEFAULT: "hsl(var(--popover))", DEFAULT: "var(--dg-bg-secondary)",
foreground: "hsl(var(--popover-foreground))", foreground: "var(--dg-text-primary)",
}, },
card: { card: {
DEFAULT: "hsl(var(--card))", DEFAULT: "var(--dg-bg-secondary)",
foreground: "hsl(var(--card-foreground))", foreground: "var(--dg-text-primary)",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
}, },
}, },
borderRadius: { borderRadius: {
xl: "calc(var(--radius) + 4px)", xl: "calc(var(--radius) + 4px)",
lg: "var(--radius)", lg: "var(--radius)",
md: "calc(var(--radius) - 2px)", md: "calc(var(--radius) - 2px)",
@@ -59,25 +50,6 @@ module.exports = {
boxShadow: { boxShadow: {
xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)", xs: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
}, },
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
}, },
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate")],