feat(ui): redesign theme system with CSS variable architecture
Refactor index.css to use CSS custom properties for all theme tokens. Add theme-vars.ts and color.ts utility modules. Update theme-presets to use new variable structure. Overhaul ThemeCustomization and ThemePresetSelector components.
This commit is contained in:
parent
3df7ae78c9
commit
f77955074e
@ -2,6 +2,7 @@
|
||||
import React from "react"
|
||||
|
||||
import { applyThemePreset } from "@/components/theme/ThemePresetSelector"
|
||||
import { loadThemeVars } from "@/lib/theme-vars"
|
||||
|
||||
type Theme = "dark" | "light" | "system"
|
||||
type ResolvedTheme = "dark" | "light"
|
||||
@ -121,6 +122,8 @@ export function ThemeProvider({
|
||||
applyThemePreset(storedPreset)
|
||||
}
|
||||
|
||||
loadThemeVars()
|
||||
|
||||
if (restoreTransitions) {
|
||||
restoreTransitions()
|
||||
}
|
||||
|
||||
@ -1,196 +1,208 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Loader2, RotateCcw, Check } from "lucide-react";
|
||||
import { THEME_VARIABLES, THEME_CATEGORIES, type ThemeVariable } from "@/config/theme-variables";
|
||||
import { t } from "@/i18n/T";
|
||||
|
||||
const STORAGE_KEY = "custom-theme-vars";
|
||||
|
||||
interface CustomTheme {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
function applyThemeVars(vars: CustomTheme) {
|
||||
const root = document.documentElement;
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}
|
||||
import { useEffect, useState } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { Loader2, RotateCcw, Check } from "lucide-react"
|
||||
import {
|
||||
THEME_VARIABLES,
|
||||
THEME_CATEGORIES,
|
||||
type ThemeVariable,
|
||||
} from "@/config/theme-variables"
|
||||
import { t } from "@/i18n/T"
|
||||
import {
|
||||
applyThemeVars,
|
||||
getStoredThemeVars,
|
||||
removeThemeVars,
|
||||
resetAllThemeVars,
|
||||
saveThemeVars,
|
||||
type ThemeVarMap,
|
||||
} from "@/lib/theme-vars"
|
||||
|
||||
export function useThemeCustomization() {
|
||||
const [customVars, setCustomVars] = useState<CustomTheme>(() => {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
return JSON.parse(stored) as CustomTheme;
|
||||
} catch {
|
||||
console.error("Failed to parse stored theme vars");
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
});
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [customVars, setCustomVars] = useState<ThemeVarMap>(() =>
|
||||
getStoredThemeVars()
|
||||
)
|
||||
const [hasChanges, setHasChanges] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
applyThemeVars(customVars);
|
||||
}, [customVars]);
|
||||
applyThemeVars(customVars)
|
||||
}, [customVars])
|
||||
|
||||
const updateVar = (key: string, value: string) => {
|
||||
setCustomVars((prev) => {
|
||||
const next = { ...prev, [key]: value };
|
||||
setHasChanges(true);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const next = { ...prev, [key]: value }
|
||||
setHasChanges(true)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const resetVar = (key: string) => {
|
||||
setCustomVars((prev) => {
|
||||
const next = { ...prev };
|
||||
delete next[key];
|
||||
document.documentElement.style.removeProperty(`--${key}`);
|
||||
setHasChanges(true);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
const next = { ...prev }
|
||||
delete next[key]
|
||||
removeThemeVars([key])
|
||||
setHasChanges(true)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
const save = () => {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(customVars));
|
||||
setHasChanges(false);
|
||||
};
|
||||
saveThemeVars(customVars)
|
||||
setHasChanges(false)
|
||||
}
|
||||
|
||||
const resetAll = () => {
|
||||
setCustomVars({});
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
THEME_VARIABLES.forEach((v) => {
|
||||
document.documentElement.style.removeProperty(`--${v.key}`);
|
||||
});
|
||||
setHasChanges(false);
|
||||
};
|
||||
setCustomVars({})
|
||||
resetAllThemeVars(customVars)
|
||||
setHasChanges(false)
|
||||
}
|
||||
|
||||
return { customVars, updateVar, resetVar, save, resetAll, hasChanges };
|
||||
return { customVars, updateVar, resetVar, save, resetAll, hasChanges }
|
||||
}
|
||||
|
||||
export function ThemeCustomization({ className }: { className?: string }) {
|
||||
const { customVars, updateVar, resetVar, save, resetAll, hasChanges } = useThemeCustomization();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [activeCategory, setActiveCategory] = useState<string>("surfaces");
|
||||
const { customVars, updateVar, resetVar, save, resetAll, hasChanges } =
|
||||
useThemeCustomization()
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [activeCategory, setActiveCategory] = useState<string>("surfaces")
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
save();
|
||||
setSaved(true);
|
||||
setSaving(false);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
};
|
||||
setSaving(true)
|
||||
save()
|
||||
setSaved(true)
|
||||
setSaving(false)
|
||||
setTimeout(() => setSaved(false), 2000)
|
||||
}
|
||||
|
||||
const getValue = (v: ThemeVariable): string => {
|
||||
return customVars[v.key] ?? "";
|
||||
};
|
||||
|
||||
const isModified = (key: string): boolean => key in customVars;
|
||||
const getValue = (v: ThemeVariable): string => customVars[v.key] ?? ""
|
||||
const isModified = (key: string): boolean => key in customVars
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{/* Category Tabs */}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{THEME_CATEGORIES.map((cat) => (
|
||||
<button
|
||||
key={cat.key}
|
||||
onClick={() => setActiveCategory(cat.key)}
|
||||
className="px-3 py-1.5 rounded-md text-[13px] transition-all"
|
||||
style={{
|
||||
backgroundColor: activeCategory === cat.key ? "var(--accent)" : "var(--surface-elevated)",
|
||||
color: activeCategory === cat.key ? "var(--accent-fg)" : "var(--text-secondary)",
|
||||
border: `1px solid ${activeCategory === cat.key ? "var(--accent)" : "var(--border-default)"}`,
|
||||
}}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{THEME_CATEGORIES.map((cat) => {
|
||||
const active = activeCategory === cat.key
|
||||
return (
|
||||
<button
|
||||
key={cat.key}
|
||||
onClick={() => setActiveCategory(cat.key)}
|
||||
className="rounded-full border px-3 py-1.5 text-[13px] transition-all"
|
||||
style={{
|
||||
backgroundColor: active
|
||||
? "var(--accent)"
|
||||
: "var(--surface-elevated)",
|
||||
color: active ? "var(--accent-fg)" : "var(--text-secondary)",
|
||||
border: `1px solid ${active ? "var(--accent)" : "var(--border-default)"}`,
|
||||
}}
|
||||
>
|
||||
{cat.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Variables Grid */}
|
||||
<div className="space-y-4">
|
||||
{THEME_VARIABLES.filter((v) => v.category === activeCategory).map((variable) => (
|
||||
<div
|
||||
key={variable.key}
|
||||
className="flex items-center gap-4 p-3 rounded-lg"
|
||||
style={{
|
||||
backgroundColor: "var(--surface-elevated)",
|
||||
border: `1px solid ${isModified(variable.key) ? "var(--accent)" : "var(--border-default)"}`,
|
||||
}}
|
||||
>
|
||||
{/* Preview */}
|
||||
<div
|
||||
className="w-10 h-10 rounded-md shrink-0 border"
|
||||
style={{ backgroundColor: variable.defaultLight }}
|
||||
title={variable.label}
|
||||
/>
|
||||
<Separator className="my-5" />
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[14px] font-medium" style={{ color: "var(--text-primary)" }}>
|
||||
{variable.label}
|
||||
</span>
|
||||
{isModified(variable.key) && (
|
||||
<span className="text-[11px] px-1.5 py-0.5 rounded" style={{ backgroundColor: "var(--accent-muted)", color: "var(--accent)" }}>
|
||||
已修改
|
||||
<div className="grid gap-3">
|
||||
{THEME_VARIABLES.filter((v) => v.category === activeCategory).map(
|
||||
(variable) => (
|
||||
<div
|
||||
key={variable.key}
|
||||
className="flex flex-col gap-4 rounded-2xl border p-4 lg:flex-row lg:items-center"
|
||||
style={{
|
||||
backgroundColor: "var(--surface-elevated)",
|
||||
borderColor: isModified(variable.key)
|
||||
? "var(--accent)"
|
||||
: "var(--border-default)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="size-10 shrink-0 rounded-xl border"
|
||||
style={{ backgroundColor: variable.defaultLight }}
|
||||
title={variable.label}
|
||||
/>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="text-[14px] font-medium"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{variable.label}
|
||||
</span>
|
||||
{isModified(variable.key) && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-full px-2 py-0.5 text-[11px]"
|
||||
>
|
||||
已修改
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<code
|
||||
className="text-[11px]"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
--{variable.key}
|
||||
</code>
|
||||
{variable.description && (
|
||||
<p
|
||||
className="mt-0.5 text-[12px]"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
{variable.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<code className="text-[11px]" style={{ color: "var(--text-muted)" }}>
|
||||
--{variable.key}
|
||||
</code>
|
||||
{variable.description && (
|
||||
<p className="text-[12px] mt-0.5" style={{ color: "var(--text-muted)" }}>
|
||||
{variable.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<Input
|
||||
value={getValue(variable)}
|
||||
onChange={(e) => updateVar(variable.key, e.target.value)}
|
||||
placeholder={variable.defaultLight}
|
||||
className="w-[200px] font-mono text-[13px]"
|
||||
style={{
|
||||
backgroundColor: "var(--input-bg)",
|
||||
borderColor: "var(--border-default)",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
/>
|
||||
{isModified(variable.key) && (
|
||||
<button
|
||||
onClick={() => resetVar(variable.key)}
|
||||
className="p-1.5 rounded"
|
||||
title={t("settings.appearance_page.theme_customization.reset_default")}
|
||||
style={{ color: "var(--text-muted)", backgroundColor: "transparent" }}
|
||||
>
|
||||
<RotateCcw className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Input
|
||||
value={getValue(variable)}
|
||||
onChange={(e) => updateVar(variable.key, e.target.value)}
|
||||
placeholder={variable.defaultLight}
|
||||
className="w-full max-w-[220px] font-mono text-[13px]"
|
||||
style={{
|
||||
backgroundColor: "var(--input-bg)",
|
||||
borderColor: "var(--border-default)",
|
||||
color: "var(--text-primary)",
|
||||
}}
|
||||
/>
|
||||
{isModified(variable.key) && (
|
||||
<button
|
||||
onClick={() => resetVar(variable.key)}
|
||||
className="rounded-full p-1.5 text-[var(--text-muted)] transition-colors hover:bg-[var(--hover-bg)] hover:text-[var(--text-primary)]"
|
||||
title={t(
|
||||
"settings.appearance_page.theme_customization.reset_default"
|
||||
)}
|
||||
>
|
||||
<RotateCcw className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-4 mt-6 pt-4" style={{ borderTop: "1px solid var(--border-subtle)" }}>
|
||||
<div className="mt-6 flex flex-wrap items-center gap-3 border-t border-[var(--border-subtle)] pt-4">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving || !hasChanges}
|
||||
size="sm"
|
||||
style={{ backgroundColor: "var(--accent)", color: "var(--accent-fg)" }}
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "var(--accent-fg)",
|
||||
}}
|
||||
>
|
||||
{saving && <Loader2 className="w-3.5 h-3.5 mr-2 animate-spin" />}
|
||||
{saved ? <Check className="w-3.5 h-3.5 mr-2" /> : null}
|
||||
{saved ? t("settings.appearance_page.theme_customization.saved") : t("settings.appearance_page.theme_customization.save_theme")}
|
||||
{saving && (
|
||||
<Loader2 data-icon="inline-start" className="animate-spin" />
|
||||
)}
|
||||
{saved ? <Check data-icon="inline-start" /> : null}
|
||||
{saved
|
||||
? t("settings.appearance_page.theme_customization.saved")
|
||||
: t("settings.appearance_page.theme_customization.save_theme")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={resetAll}
|
||||
@ -205,26 +217,5 @@ export function ThemeCustomization({ className }: { className?: string }) {
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
export function resetAllThemeVars() {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
THEME_VARIABLES.forEach((v) => {
|
||||
document.documentElement.style.removeProperty(`--${v.key}`);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadThemeVars() {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
try {
|
||||
const vars: CustomTheme = JSON.parse(stored);
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
document.documentElement.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
} catch {
|
||||
console.error("Failed to parse stored theme vars");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,10 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { getAllPresets } from "@/config/theme-presets";
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { getAllPresets } from "@/config/theme-presets"
|
||||
import { loadThemeVars } from "@/lib/theme-vars"
|
||||
|
||||
const STORAGE_KEY = "app-theme-preset";
|
||||
const DEFAULT_PRESET = "soft-mono";
|
||||
const STORAGE_KEY = "app-theme-preset"
|
||||
const DEFAULT_PRESET = "soft-mono"
|
||||
|
||||
const THEME_COLOR_ALIASES: Record<string, string> = {
|
||||
"color-background": "background",
|
||||
@ -31,119 +33,139 @@ const THEME_COLOR_ALIASES: Record<string, string> = {
|
||||
"color-sidebar-accent-foreground": "sidebar-accent-foreground",
|
||||
"color-sidebar-border": "sidebar-border",
|
||||
"color-sidebar-ring": "sidebar-ring",
|
||||
};
|
||||
}
|
||||
|
||||
const DERIVED_THEME_TOKENS: Record<string, string> = {
|
||||
"surface-secondary":
|
||||
"color-mix(in oklch, var(--surface-elevated) 72%, var(--surface-ground))",
|
||||
"accent-bg": "color-mix(in oklch, var(--accent) 8%, transparent)",
|
||||
"success-alpha10": "color-mix(in oklch, var(--success) 10%, transparent)",
|
||||
"warning-alpha10": "color-mix(in oklch, var(--warning) 10%, transparent)",
|
||||
"destructive-alpha10": "color-mix(in oklch, var(--destructive) 10%, transparent)",
|
||||
"destructive-alpha10":
|
||||
"color-mix(in oklch, var(--destructive) 10%, transparent)",
|
||||
"status-offline": "var(--text-muted)",
|
||||
"text-tertiary": "color-mix(in oklch, var(--text-muted) 70%, var(--surface-ground))",
|
||||
"heatmap-0": "color-mix(in oklch, var(--surface-ground) 82%, var(--border-default))",
|
||||
"text-tertiary":
|
||||
"color-mix(in oklch, var(--text-muted) 70%, var(--surface-ground))",
|
||||
"heatmap-0":
|
||||
"color-mix(in oklch, var(--surface-ground) 82%, var(--border-default))",
|
||||
"heatmap-1": "color-mix(in oklch, var(--success) 18%, var(--surface-ground))",
|
||||
"heatmap-2": "color-mix(in oklch, var(--success) 36%, var(--surface-ground))",
|
||||
"heatmap-3": "color-mix(in oklch, var(--success) 58%, var(--surface-ground))",
|
||||
"heatmap-4": "color-mix(in oklch, var(--success) 78%, var(--surface-ground))",
|
||||
};
|
||||
}
|
||||
|
||||
export function useThemePreset() {
|
||||
const [presetId, setPresetIdState] = useState<string>(() => {
|
||||
return localStorage.getItem(STORAGE_KEY) || DEFAULT_PRESET;
|
||||
});
|
||||
const [presetId, setPresetIdState] = useState<string>(
|
||||
() => localStorage.getItem(STORAGE_KEY) || DEFAULT_PRESET
|
||||
)
|
||||
|
||||
const setPresetId = useCallback((id: string) => {
|
||||
localStorage.setItem(STORAGE_KEY, id);
|
||||
setPresetIdState(id);
|
||||
}, []);
|
||||
localStorage.setItem(STORAGE_KEY, id)
|
||||
setPresetIdState(id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
applyThemePreset(presetId);
|
||||
}, [presetId]);
|
||||
applyThemePreset(presetId)
|
||||
loadThemeVars()
|
||||
}, [presetId])
|
||||
|
||||
return { presetId, setPresetId };
|
||||
return { presetId, setPresetId }
|
||||
}
|
||||
|
||||
export function applyThemePreset(presetId: string) {
|
||||
const presets = getAllPresets();
|
||||
const preset = presets.find((p) => p.id === presetId);
|
||||
if (!preset) return;
|
||||
const presets = getAllPresets()
|
||||
const preset = presets.find((p) => p.id === presetId)
|
||||
if (!preset) return
|
||||
|
||||
const isDark = document.documentElement.classList.contains("dark");
|
||||
const vars = isDark && preset.varsDark ? preset.varsDark : preset.vars;
|
||||
const isDark = document.documentElement.classList.contains("dark")
|
||||
const vars = isDark && preset.varsDark ? preset.varsDark : preset.vars
|
||||
|
||||
const root = document.documentElement;
|
||||
const root = document.documentElement
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
if (value) root.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
if (value) root.style.setProperty(`--${key}`, value)
|
||||
})
|
||||
|
||||
Object.entries(THEME_COLOR_ALIASES).forEach(([alias, source]) => {
|
||||
root.style.setProperty(`--${alias}`, `var(--${source})`);
|
||||
});
|
||||
if (alias in vars) return
|
||||
root.style.setProperty(`--${alias}`, `var(--${source})`)
|
||||
})
|
||||
|
||||
Object.entries(DERIVED_THEME_TOKENS).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
if (key in vars) return
|
||||
root.style.setProperty(`--${key}`, value)
|
||||
})
|
||||
}
|
||||
|
||||
export function ThemePresetSelector() {
|
||||
const { presetId, setPresetId } = useThemePreset();
|
||||
const presets = getAllPresets();
|
||||
const { presetId, setPresetId } = useThemePreset()
|
||||
const presets = getAllPresets()
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{presets.map((preset) => {
|
||||
const active = presetId === preset.id;
|
||||
const active = presetId === preset.id
|
||||
return (
|
||||
<button
|
||||
key={preset.id}
|
||||
onClick={() => setPresetId(preset.id)}
|
||||
className="flex items-center gap-3 p-3 rounded-lg border-2 text-left transition-all"
|
||||
className="flex items-center gap-3 rounded-2xl border p-3 text-left transition-all"
|
||||
style={{
|
||||
borderColor: active ? "var(--accent)" : "var(--border-default)",
|
||||
backgroundColor: active ? "var(--hover-bg-strong)" : "var(--surface-elevated)",
|
||||
backgroundColor: active
|
||||
? "var(--hover-bg-strong)"
|
||||
: "var(--surface-elevated)",
|
||||
boxShadow: active
|
||||
? "0 0 0 1px color-mix(in oklch, var(--accent) 18%, transparent)"
|
||||
: "none",
|
||||
}}
|
||||
>
|
||||
{/* Preview swatches: primary (button), accent, bg */}
|
||||
<div className="flex shrink-0 gap-0.5 items-stretch self-stretch">
|
||||
<div className="flex shrink-0 items-stretch gap-0.5 self-stretch">
|
||||
<div
|
||||
className="w-7 h-8 rounded-l-md flex items-center justify-center"
|
||||
style={{ backgroundColor: preset.preview.bg, border: "1px solid var(--border-subtle)" }}
|
||||
className="flex w-7 items-center justify-center rounded-l-md"
|
||||
style={{
|
||||
backgroundColor: preset.preview.bg,
|
||||
border: "1px solid var(--border-subtle)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-4 h-3 rounded-sm"
|
||||
className="size-4 rounded-sm"
|
||||
style={{ backgroundColor: preset.preview.fg }}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="w-7 h-8 flex items-center justify-center"
|
||||
className="flex w-7 items-center justify-center"
|
||||
style={{ backgroundColor: preset.preview.accent }}
|
||||
>
|
||||
<div
|
||||
className="w-4 h-3 rounded-sm"
|
||||
className="size-4 rounded-sm"
|
||||
style={{ backgroundColor: "rgba(255,255,255,0.85)" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="text-[14px] font-medium" style={{ color: "var(--text-primary)" }}>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div
|
||||
className="text-[14px] font-medium"
|
||||
style={{ color: "var(--text-primary)" }}
|
||||
>
|
||||
{preset.name}
|
||||
</div>
|
||||
<div className="text-[12px] truncate" style={{ color: "var(--text-muted)" }}>
|
||||
<div
|
||||
className="truncate text-[12px]"
|
||||
style={{ color: "var(--text-muted)" }}
|
||||
>
|
||||
{preset.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{active && (
|
||||
<div
|
||||
className="ml-auto w-2.5 h-2.5 rounded-full shrink-0"
|
||||
style={{ backgroundColor: "var(--accent)" }}
|
||||
/>
|
||||
<Badge className="ml-auto rounded-full px-2 py-0.5 text-[11px]">
|
||||
Selected
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@ -30,13 +30,13 @@ export type ThemeVarKey =
|
||||
| "sidebar-border" | "sidebar-ring"
|
||||
// Semantic surfaces
|
||||
| "surface-rail" | "surface-sidebar" | "surface-ground"
|
||||
| "surface-elevated" | "surface-overlay"
|
||||
| "surface-elevated" | "surface-secondary" | "surface-overlay"
|
||||
// Semantic borders
|
||||
| "border-subtle" | "border-default" | "border-strong"
|
||||
// Semantic text
|
||||
| "text-primary" | "text-secondary" | "text-muted" | "text-inverse"
|
||||
| "text-primary" | "text-secondary" | "text-muted" | "text-tertiary" | "text-inverse"
|
||||
// Accent variants
|
||||
| "accent-hover" | "accent-fg" | "accent-muted"
|
||||
| "accent-hover" | "accent-fg" | "accent-muted" | "accent-bg"
|
||||
// Status indicators
|
||||
| "status-online" | "status-idle" | "status-dnd" | "status-offline"
|
||||
// Feedback tokens
|
||||
@ -111,6 +111,7 @@ const BASE_LIGHT = Object.freeze<ThemeVars>({
|
||||
"surface-sidebar": "oklch(0.97 0 0)",
|
||||
"surface-ground": "oklch(1 0 0)",
|
||||
"surface-elevated": "oklch(1 0 0)",
|
||||
"surface-secondary": "oklch(0.99 0 0)",
|
||||
"surface-overlay": "oklch(1 0 0 / 90%)",
|
||||
// Semantic borders
|
||||
"border-subtle": "oklch(0.90 0 0 / 40%)",
|
||||
@ -120,11 +121,13 @@ const BASE_LIGHT = Object.freeze<ThemeVars>({
|
||||
"text-primary": "oklch(0.13 0 0)",
|
||||
"text-secondary": "oklch(0.40 0 0)",
|
||||
"text-muted": "oklch(0.55 0 0)",
|
||||
"text-tertiary": "oklch(0.70 0 0)",
|
||||
"text-inverse": "oklch(0.985 0 0)",
|
||||
// Accent variants
|
||||
"accent-hover": "oklch(0.15 0 0)",
|
||||
"accent-fg": "oklch(1 0 0)",
|
||||
"accent-muted": "oklch(0.13 0 0 / 12%)",
|
||||
"accent-bg": "oklch(0.13 0 0 / 8%)",
|
||||
// Status
|
||||
"status-online": "oklch(0.55 0.15 155)",
|
||||
"status-idle": "oklch(0.68 0.15 80)",
|
||||
@ -204,6 +207,7 @@ const BASE_DARK = Object.freeze<ThemeVars>({
|
||||
"surface-sidebar": "oklch(0.15 0 0)",
|
||||
"surface-ground": "oklch(0.13 0 0)",
|
||||
"surface-elevated": "oklch(0.18 0 0)",
|
||||
"surface-secondary": "oklch(0.16 0 0)",
|
||||
"surface-overlay": "oklch(0.10 0 0 / 95%)",
|
||||
// Semantic borders
|
||||
"border-subtle": "oklch(0.30 0 0 / 30%)",
|
||||
@ -213,11 +217,13 @@ const BASE_DARK = Object.freeze<ThemeVars>({
|
||||
"text-primary": "oklch(0.97 0 0)",
|
||||
"text-secondary": "oklch(0.80 0 0)",
|
||||
"text-muted": "oklch(0.65 0 0)",
|
||||
"text-tertiary": "oklch(0.55 0 0)",
|
||||
"text-inverse": "oklch(0.13 0 0)",
|
||||
// Accent variants
|
||||
"accent-hover": "oklch(0.78 0.15 264)",
|
||||
"accent-fg": "oklch(0.10 0 0)",
|
||||
"accent-muted": "oklch(0.70 0.15 264 / 20%)",
|
||||
"accent-bg": "oklch(0.70 0.15 264 / 8%)",
|
||||
// Status
|
||||
"status-online": "oklch(0.72 0.17 155)",
|
||||
"status-idle": "oklch(0.78 0.15 80)",
|
||||
@ -270,7 +276,7 @@ const PRESETS: ThemePreset[] = [
|
||||
id: "pure-bw",
|
||||
name: "纯黑纯白",
|
||||
description: "高对比度黑白主题",
|
||||
preview: {bg: "#FFFFFF", fg: "#1A1A1A", accent: "#000000"},
|
||||
preview: {bg: "#FAFAFA", fg: "#111111", accent: "#111111"},
|
||||
vars: light({
|
||||
foreground: "oklch(0 0 0)",
|
||||
"card-foreground": "oklch(0 0 0)",
|
||||
@ -296,16 +302,19 @@ const PRESETS: ThemePreset[] = [
|
||||
"sidebar-ring": "oklch(0 0 0)",
|
||||
// Semantic
|
||||
"surface-overlay": "oklch(1 0 0 / 96%)",
|
||||
"surface-secondary": "oklch(0.96 0 0)",
|
||||
"border-subtle": "oklch(0.75 0 0 / 40%)",
|
||||
"border-default": "oklch(0.15 0 0)",
|
||||
"border-strong": "oklch(0 0 0)",
|
||||
"text-primary": "oklch(0 0 0)",
|
||||
"text-secondary": "oklch(0.20 0 0)",
|
||||
"text-muted": "oklch(0.40 0 0)",
|
||||
"text-tertiary": "oklch(0.58 0 0)",
|
||||
"text-inverse": "oklch(1 0 0)",
|
||||
"accent-hover": "oklch(0.10 0 0)",
|
||||
"accent-fg": "oklch(1 0 0)",
|
||||
"accent-muted": "oklch(0 0 0 / 12%)",
|
||||
"accent-bg": "oklch(0 0 0 / 8%)",
|
||||
"hover-bg": "oklch(0.92 0 0)",
|
||||
"hover-bg-strong": "oklch(0.84 0 0)",
|
||||
"input-bg": "oklch(0.97 0 0)",
|
||||
@ -352,15 +361,18 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-sidebar": "oklch(0.05 0 0)",
|
||||
"surface-ground": "oklch(0 0 0)",
|
||||
"surface-elevated": "oklch(0.06 0 0)",
|
||||
"surface-secondary": "oklch(0.08 0 0)",
|
||||
"border-default": "oklch(1 0 0 / 15%)",
|
||||
"border-strong": "oklch(1 0 0 / 30%)",
|
||||
"text-primary": "oklch(1 0 0)",
|
||||
"text-secondary": "oklch(0.80 0 0)",
|
||||
"text-muted": "oklch(0.60 0 0)",
|
||||
"text-tertiary": "oklch(0.48 0 0)",
|
||||
"text-inverse": "oklch(0 0 0)",
|
||||
"accent-hover": "oklch(0.90 0 0)",
|
||||
"accent-fg": "oklch(0 0 0)",
|
||||
"accent-muted": "oklch(1 0 0 / 12%)",
|
||||
"accent-bg": "oklch(1 0 0 / 6%)",
|
||||
"hover-bg": "oklch(1 0 0 / 6%)",
|
||||
"hover-bg-strong": "oklch(1 0 0 / 12%)",
|
||||
"input-bg": "oklch(0.03 0 0)",
|
||||
@ -389,7 +401,7 @@ const PRESETS: ThemePreset[] = [
|
||||
id: "soft-mono",
|
||||
name: "柔和灰度",
|
||||
description: "层次分明的灰度主题,默认推荐",
|
||||
preview: {bg: "#FFFFFF", fg: "#1A1A1A", accent: "#1A1A1A"},
|
||||
preview: {bg: "#F7F7F6", fg: "#222222", accent: "#222222"},
|
||||
vars: light(),
|
||||
varsDark: dark(),
|
||||
},
|
||||
@ -423,11 +435,13 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-rail": "oklch(0.08 0 0)",
|
||||
"surface-sidebar": "oklch(0.10 0 0)",
|
||||
"surface-elevated": "oklch(0.16 0 0)",
|
||||
"surface-secondary": "oklch(0.14 0 0)",
|
||||
"border-default": "oklch(0.28 0 0)",
|
||||
"text-inverse": "oklch(0.10 0 0)",
|
||||
"accent-hover": "oklch(0.85 0 0)",
|
||||
"accent-fg": "oklch(0.08 0 0)",
|
||||
"accent-muted": "oklch(0.985 0 0 / 12%)",
|
||||
"accent-bg": "oklch(0.985 0 0 / 6%)",
|
||||
"input-bg": "oklch(0.15 0 0)",
|
||||
"input-ring": "oklch(0.985 0 0)",
|
||||
}),
|
||||
@ -453,11 +467,13 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-rail": "oklch(0.08 0 0)",
|
||||
"surface-sidebar": "oklch(0.10 0 0)",
|
||||
"surface-elevated": "oklch(0.16 0 0)",
|
||||
"surface-secondary": "oklch(0.14 0 0)",
|
||||
"border-default": "oklch(0.28 0 0)",
|
||||
"text-inverse": "oklch(0.10 0 0)",
|
||||
"accent-hover": "oklch(0.85 0 0)",
|
||||
"accent-fg": "oklch(0.08 0 0)",
|
||||
"accent-muted": "oklch(0.985 0 0 / 12%)",
|
||||
"accent-bg": "oklch(0.985 0 0 / 6%)",
|
||||
"input-bg": "oklch(0.15 0 0)",
|
||||
"input-ring": "oklch(0.985 0 0)",
|
||||
}),
|
||||
@ -824,6 +840,7 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-rail": "oklch(0.94 0.020 95)",
|
||||
"surface-sidebar": "oklch(0.96 0.020 95)",
|
||||
"surface-ground": "oklch(0.98 0.022 95)",
|
||||
"surface-secondary": "oklch(0.95 0.018 95)",
|
||||
"surface-overlay": "oklch(0.96 0.018 95 / 95%)",
|
||||
"border-subtle": "oklch(0.88 0.018 95 / 50%)",
|
||||
"border-default": "oklch(0.86 0.018 95)",
|
||||
@ -831,10 +848,12 @@ const PRESETS: ThemePreset[] = [
|
||||
"text-primary": "oklch(0.47 0.035 205)",
|
||||
"text-secondary": "oklch(0.57 0.028 205)",
|
||||
"text-muted": "oklch(0.65 0.018 200)",
|
||||
"text-tertiary": "oklch(0.62 0.020 200)",
|
||||
"text-inverse": "oklch(0.98 0.022 95)",
|
||||
"accent-hover": "oklch(0.45 0.22 250)",
|
||||
"accent-fg": "oklch(0.98 0.022 95)",
|
||||
"accent-muted": "oklch(0.52 0.22 250 / 15%)",
|
||||
"accent-bg": "oklch(0.52 0.22 250 / 8%)",
|
||||
"hover-bg": "oklch(0.93 0.018 95 / 70%)",
|
||||
"hover-bg-strong": "oklch(0.89 0.018 95)",
|
||||
"input-bg": "oklch(0.99 0.010 95)",
|
||||
@ -887,6 +906,7 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-sidebar": "oklch(0.18 0.040 200)",
|
||||
"surface-ground": "oklch(0.22 0.042 200)",
|
||||
"surface-elevated": "oklch(0.26 0.035 200)",
|
||||
"surface-secondary": "oklch(0.29 0.035 200)",
|
||||
"surface-overlay": "oklch(0.14 0.040 200 / 95%)",
|
||||
"border-subtle": "oklch(1 0 0 / 5%)",
|
||||
"border-default": "oklch(1 0 0 / 10%)",
|
||||
@ -894,10 +914,12 @@ const PRESETS: ThemePreset[] = [
|
||||
"text-primary": "oklch(0.78 0.025 200)",
|
||||
"text-secondary": "oklch(0.65 0.020 200)",
|
||||
"text-muted": "oklch(0.55 0.018 200)",
|
||||
"text-tertiary": "oklch(0.50 0.018 200)",
|
||||
"text-inverse": "oklch(0.22 0.042 200)",
|
||||
"accent-hover": "oklch(0.60 0.22 250)",
|
||||
"accent-fg": "oklch(0.22 0.042 200)",
|
||||
"accent-muted": "oklch(0.55 0.22 250 / 20%)",
|
||||
"accent-bg": "oklch(0.55 0.22 250 / 8%)",
|
||||
"hover-bg": "oklch(1 0 0 / 5%)",
|
||||
"hover-bg-strong": "oklch(1 0 0 / 10%)",
|
||||
"input-bg": "oklch(0.20 0.040 200)",
|
||||
@ -955,6 +977,7 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-sidebar": "oklch(0.15 0.012 110)",
|
||||
"surface-ground": "oklch(0.18 0.012 110)",
|
||||
"surface-elevated": "oklch(0.22 0.010 110)",
|
||||
"surface-secondary": "oklch(0.20 0.010 110)",
|
||||
"surface-overlay": "oklch(0.10 0.012 110 / 95%)",
|
||||
"border-subtle": "oklch(1 0 0 / 5%)",
|
||||
"border-default": "oklch(1 0 0 / 10%)",
|
||||
@ -962,10 +985,12 @@ const PRESETS: ThemePreset[] = [
|
||||
"text-primary": "oklch(0.97 0.008 95)",
|
||||
"text-secondary": "oklch(0.82 0.014 110)",
|
||||
"text-muted": "oklch(0.64 0.018 110)",
|
||||
"text-tertiary": "oklch(0.56 0.016 110)",
|
||||
"text-inverse": "oklch(0.12 0.012 110)",
|
||||
"accent-hover": "oklch(0.84 0.21 140)",
|
||||
"accent-fg": "oklch(0.08 0.012 110)",
|
||||
"accent-muted": "oklch(0.78 0.20 140 / 18%)",
|
||||
"accent-bg": "oklch(0.78 0.20 140 / 8%)",
|
||||
"hover-bg": "oklch(1 0 0 / 5%)",
|
||||
"hover-bg-strong": "oklch(1 0 0 / 10%)",
|
||||
"input-bg": "oklch(0.16 0.012 110)",
|
||||
@ -1112,6 +1137,7 @@ const PRESETS: ThemePreset[] = [
|
||||
"surface-sidebar": "oklch(0.13 0.03 30)",
|
||||
"surface-ground": "oklch(0.15 0.03 30)",
|
||||
"surface-elevated": "oklch(0.20 0.03 30)",
|
||||
"surface-secondary": "oklch(0.18 0.03 30)",
|
||||
"surface-overlay": "oklch(0.10 0.03 30 / 95%)",
|
||||
// Semantic borders
|
||||
"border-subtle": "oklch(0.30 0.02 30 / 30%)",
|
||||
@ -1121,11 +1147,13 @@ const PRESETS: ThemePreset[] = [
|
||||
"text-primary": "oklch(0.95 0.01 80)",
|
||||
"text-secondary": "oklch(0.80 0.015 80)",
|
||||
"text-muted": "oklch(0.65 0.02 30)",
|
||||
"text-tertiary": "oklch(0.58 0.018 30)",
|
||||
"text-inverse": "oklch(0.15 0.03 30)",
|
||||
// Accent variants
|
||||
"accent-hover": "oklch(0.77 0.22 49)",
|
||||
"accent-fg": "oklch(0.15 0.03 30)",
|
||||
"accent-muted": "oklch(0.72 0.22 49 / 20%)",
|
||||
"accent-bg": "oklch(0.72 0.22 49 / 8%)",
|
||||
// Hover overlays
|
||||
"hover-bg": "oklch(1 0 0 / 5%)",
|
||||
"hover-bg-strong": "oklch(1 0 0 / 10%)",
|
||||
@ -1162,4 +1190,4 @@ export function getPreset(id: string): ThemePreset | undefined {
|
||||
|
||||
export function getAllPresets(): readonly ThemePreset[] {
|
||||
return PRESETS;
|
||||
}
|
||||
}
|
||||
|
||||
656
src/index.css
656
src/index.css
@ -9,123 +9,121 @@
|
||||
@plugin "@tailwindcss/typography";
|
||||
|
||||
body {
|
||||
font-family: Inter,
|
||||
system-ui,
|
||||
sans-serif;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: 'Geist Variable', sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
--font-heading: var(--font-sans);
|
||||
--font-sans: "Geist Variable", sans-serif;
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: calc(var(--radius) * 0.6);
|
||||
--radius-md: calc(var(--radius) * 0.8);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) * 1.4);
|
||||
--radius-2xl: calc(var(--radius) * 1.8);
|
||||
--radius-3xl: calc(var(--radius) * 2.2);
|
||||
--radius-4xl: calc(var(--radius) * 2.6);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.97 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.87 0 0);
|
||||
--chart-2: oklch(0.556 0 0);
|
||||
--chart-3: oklch(0.439 0 0);
|
||||
--chart-4: oklch(0.371 0 0);
|
||||
--chart-5: oklch(0.269 0 0);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
@ -134,211 +132,246 @@ body {
|
||||
to OKLCH — auto light/dark via .dark class
|
||||
───────────────────────────────────────────── */
|
||||
:root {
|
||||
/* Surfaces (layer hierarchy, 3 layers only) */
|
||||
--surface-rail: oklch(0.98 0 0);
|
||||
--surface-sidebar: oklch(0.97 0 0);
|
||||
--surface-ground: oklch(1 0 0);
|
||||
--surface-elevated: oklch(1 0 0);
|
||||
--surface-overlay: oklch(1 0 0 / 90%);
|
||||
/* Surfaces (layer hierarchy, 3 layers only) */
|
||||
--surface-rail: oklch(0.98 0 0);
|
||||
--surface-sidebar: oklch(0.97 0 0);
|
||||
--surface-ground: oklch(1 0 0);
|
||||
--surface-elevated: oklch(1 0 0);
|
||||
--surface-secondary: color-mix(
|
||||
in oklch,
|
||||
var(--surface-elevated) 72%,
|
||||
var(--surface-ground)
|
||||
);
|
||||
--surface-overlay: oklch(1 0 0 / 90%);
|
||||
|
||||
/* Borders */
|
||||
--border-subtle: oklch(0.90 0 0 / 40%);
|
||||
--border-default: oklch(0.88 0 0);
|
||||
--border-strong: oklch(0.80 0 0);
|
||||
/* Borders */
|
||||
--border-subtle: oklch(0.9 0 0 / 40%);
|
||||
--border-default: oklch(0.88 0 0);
|
||||
--border-strong: oklch(0.8 0 0);
|
||||
|
||||
/* Text */
|
||||
--text-primary: oklch(0.13 0 0);
|
||||
--text-secondary: oklch(0.40 0 0);
|
||||
--text-muted: oklch(0.55 0 0);
|
||||
--text-inverse: oklch(0.985 0 0);
|
||||
--text-tertiary: oklch(0.70 0 0);
|
||||
/* Text */
|
||||
--text-primary: oklch(0.13 0 0);
|
||||
--text-secondary: oklch(0.4 0 0);
|
||||
--text-muted: oklch(0.55 0 0);
|
||||
--text-inverse: oklch(0.985 0 0);
|
||||
--text-tertiary: oklch(0.7 0 0);
|
||||
|
||||
/* Brand accent — Discord blurple as reference */
|
||||
--accent: oklch(0.25 0 0);
|
||||
--accent-hover: oklch(0.15 0 0);
|
||||
--accent-fg: oklch(0.985 0 0);
|
||||
--accent-muted: oklch(0.25 0 0 / 15%);
|
||||
--accent-bg: oklch(0.25 0 0 / 8%);
|
||||
--accent-rgb: 88, 101, 242;
|
||||
/* Brand accent — Discord blurple as reference */
|
||||
--accent: oklch(0.25 0 0);
|
||||
--accent-hover: oklch(0.15 0 0);
|
||||
--accent-fg: oklch(0.985 0 0);
|
||||
--accent-muted: oklch(0.25 0 0 / 15%);
|
||||
--accent-bg: oklch(0.25 0 0 / 8%);
|
||||
--accent-rgb: 88, 101, 242;
|
||||
|
||||
/* Status */
|
||||
--status-online: oklch(0.55 0 0);
|
||||
--status-idle: oklch(0.60 0 0);
|
||||
--status-dnd: oklch(0.50 0 0);
|
||||
--status-offline: oklch(0.60 0 0);
|
||||
/* Status */
|
||||
--status-online: oklch(0.55 0 0);
|
||||
--status-idle: oklch(0.6 0 0);
|
||||
--status-dnd: oklch(0.5 0 0);
|
||||
--status-offline: oklch(0.6 0 0);
|
||||
|
||||
/* Semantic */
|
||||
--success: oklch(0.50 0 0);
|
||||
--success-alpha10: oklch(0.50 0 0 / 10%);
|
||||
--warning: oklch(0.60 0 0);
|
||||
--warning-alpha10: oklch(0.60 0 0 / 10%);
|
||||
--destructive: oklch(0.45 0 0);
|
||||
--destructive-alpha10: oklch(0.45 0 0 / 10%);
|
||||
--info: oklch(0.50 0 0);
|
||||
/* Semantic */
|
||||
--success: oklch(0.5 0 0);
|
||||
--success-alpha10: oklch(0.5 0 0 / 10%);
|
||||
--warning: oklch(0.6 0 0);
|
||||
--warning-alpha10: oklch(0.6 0 0 / 10%);
|
||||
--destructive: oklch(0.45 0 0);
|
||||
--destructive-alpha10: oklch(0.45 0 0 / 10%);
|
||||
--info: oklch(0.5 0 0);
|
||||
|
||||
/* Role colors (from Discord) */
|
||||
--role-red: oklch(0.50 0 0);
|
||||
--role-orange: oklch(0.55 0 0);
|
||||
--role-yellow: oklch(0.60 0 0);
|
||||
--role-green: oklch(0.50 0 0);
|
||||
--role-blue: oklch(0.55 0 0);
|
||||
--role-purple: oklch(0.55 0 0);
|
||||
--role-pink: oklch(0.55 0 0);
|
||||
--role-gray: oklch(0.50 0 0);
|
||||
/* Role colors (from Discord) */
|
||||
--role-red: oklch(0.5 0 0);
|
||||
--role-orange: oklch(0.55 0 0);
|
||||
--role-yellow: oklch(0.6 0 0);
|
||||
--role-green: oklch(0.5 0 0);
|
||||
--role-blue: oklch(0.55 0 0);
|
||||
--role-purple: oklch(0.55 0 0);
|
||||
--role-pink: oklch(0.55 0 0);
|
||||
--role-gray: oklch(0.5 0 0);
|
||||
|
||||
/* Interactive */
|
||||
--interactive: oklch(0.97 0 0);
|
||||
--interactive-hover: oklch(0.92 0 0);
|
||||
--interactive-active: oklch(0.88 0 0);
|
||||
/* Interactive */
|
||||
--interactive: oklch(0.97 0 0);
|
||||
--interactive-hover: oklch(0.92 0 0);
|
||||
--interactive-active: oklch(0.88 0 0);
|
||||
|
||||
/* Hover */
|
||||
--hover-bg: oklch(0.95 0 0 / 70%);
|
||||
--hover-bg-strong: oklch(0.90 0 0);
|
||||
/* Hover */
|
||||
--hover-bg: oklch(0.95 0 0 / 70%);
|
||||
--hover-bg-strong: oklch(0.9 0 0);
|
||||
|
||||
/* Input */
|
||||
--input-bg: oklch(0.98 0 0);
|
||||
--input-placeholder: oklch(0.50 0 0);
|
||||
--input-ring: oklch(0.30 0 0);
|
||||
/* Input */
|
||||
--input-bg: oklch(0.98 0 0);
|
||||
--input-placeholder: oklch(0.5 0 0);
|
||||
--input-ring: oklch(0.3 0 0);
|
||||
|
||||
/* Heatmap (contribution graph) */
|
||||
--heatmap-0: oklch(0.93 0 0);
|
||||
--heatmap-1: oklch(0.75 0.10 155);
|
||||
--heatmap-2: oklch(0.62 0.15 155);
|
||||
--heatmap-3: oklch(0.50 0.15 155);
|
||||
--heatmap-4: oklch(0.38 0.15 155);
|
||||
/* Heatmap (contribution graph) */
|
||||
--heatmap-0: oklch(0.93 0 0);
|
||||
--heatmap-1: oklch(0.75 0.1 155);
|
||||
--heatmap-2: oklch(0.62 0.15 155);
|
||||
--heatmap-3: oklch(0.5 0.15 155);
|
||||
--heatmap-4: oklch(0.38 0.15 155);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--surface-rail: oklch(0.12 0 0);
|
||||
--surface-sidebar: oklch(0.15 0 0);
|
||||
--surface-ground: oklch(0.13 0 0);
|
||||
--surface-elevated: oklch(0.18 0 0);
|
||||
--surface-overlay: oklch(0.10 0 0 / 95%);
|
||||
--surface-rail: oklch(0.12 0 0);
|
||||
--surface-sidebar: oklch(0.15 0 0);
|
||||
--surface-ground: oklch(0.13 0 0);
|
||||
--surface-elevated: oklch(0.18 0 0);
|
||||
--surface-secondary: color-mix(
|
||||
in oklch,
|
||||
var(--surface-elevated) 72%,
|
||||
var(--surface-ground)
|
||||
);
|
||||
--surface-overlay: oklch(0.1 0 0 / 95%);
|
||||
|
||||
--border-subtle: oklch(0.30 0 0 / 30%);
|
||||
--border-default: oklch(0.35 0 0);
|
||||
--border-strong: oklch(0.50 0 0);
|
||||
--border-subtle: oklch(0.3 0 0 / 30%);
|
||||
--border-default: oklch(0.35 0 0);
|
||||
--border-strong: oklch(0.5 0 0);
|
||||
|
||||
--text-primary: oklch(0.97 0 0);
|
||||
--text-secondary: oklch(0.80 0 0);
|
||||
--text-muted: oklch(0.65 0 0);
|
||||
--text-inverse: oklch(0.13 0 0);
|
||||
--text-tertiary: oklch(0.55 0 0);
|
||||
--text-primary: oklch(0.97 0 0);
|
||||
--text-secondary: oklch(0.8 0 0);
|
||||
--text-muted: oklch(0.65 0 0);
|
||||
--text-inverse: oklch(0.13 0 0);
|
||||
--text-tertiary: oklch(0.55 0 0);
|
||||
|
||||
--accent: oklch(0.70 0.15 264);
|
||||
--accent-hover: oklch(0.78 0.15 264);
|
||||
--accent-fg: oklch(0.10 0 0);
|
||||
--accent-muted: oklch(0.70 0.15 264 / 20%);
|
||||
--accent-bg: oklch(0.70 0.15 264 / 12%);
|
||||
--accent-rgb: 88, 101, 242;
|
||||
--accent: oklch(0.7 0.15 264);
|
||||
--accent-hover: oklch(0.78 0.15 264);
|
||||
--accent-fg: oklch(0.1 0 0);
|
||||
--accent-muted: oklch(0.7 0.15 264 / 20%);
|
||||
--accent-bg: oklch(0.7 0.15 264 / 12%);
|
||||
--accent-rgb: 88, 101, 242;
|
||||
|
||||
--status-online: oklch(0.72 0.17 155);
|
||||
--status-idle: oklch(0.78 0.15 80);
|
||||
--status-dnd: oklch(0.65 0.20 25);
|
||||
--status-offline: oklch(0.55 0 0);
|
||||
--status-online: oklch(0.72 0.17 155);
|
||||
--status-idle: oklch(0.78 0.15 80);
|
||||
--status-dnd: oklch(0.65 0.2 25);
|
||||
--status-offline: oklch(0.55 0 0);
|
||||
|
||||
--success: oklch(0.72 0.17 155);
|
||||
--success-alpha10: oklch(0.72 0.17 155 / 10%);
|
||||
--warning: oklch(0.78 0.15 90);
|
||||
--warning-alpha10: oklch(0.78 0.15 90 / 10%);
|
||||
--destructive: oklch(0.70 0.20 25);
|
||||
--destructive-alpha10: oklch(0.70 0.20 25 / 10%);
|
||||
--info: oklch(0.70 0.15 250);
|
||||
--success: oklch(0.72 0.17 155);
|
||||
--success-alpha10: oklch(0.72 0.17 155 / 10%);
|
||||
--warning: oklch(0.78 0.15 90);
|
||||
--warning-alpha10: oklch(0.78 0.15 90 / 10%);
|
||||
--destructive: oklch(0.7 0.2 25);
|
||||
--destructive-alpha10: oklch(0.7 0.2 25 / 10%);
|
||||
--info: oklch(0.7 0.15 250);
|
||||
|
||||
--role-red: oklch(0.65 0.20 20);
|
||||
--role-orange: oklch(0.72 0.18 50);
|
||||
--role-yellow: oklch(0.78 0.16 85);
|
||||
--role-green: oklch(0.70 0.17 155);
|
||||
--role-blue: oklch(0.70 0.20 250);
|
||||
--role-purple: oklch(0.65 0.20 290);
|
||||
--role-pink: oklch(0.65 0.20 340);
|
||||
--role-gray: oklch(0.58 0 0);
|
||||
--role-red: oklch(0.65 0.2 20);
|
||||
--role-orange: oklch(0.72 0.18 50);
|
||||
--role-yellow: oklch(0.78 0.16 85);
|
||||
--role-green: oklch(0.7 0.17 155);
|
||||
--role-blue: oklch(0.7 0.2 250);
|
||||
--role-purple: oklch(0.65 0.2 290);
|
||||
--role-pink: oklch(0.65 0.2 340);
|
||||
--role-gray: oklch(0.58 0 0);
|
||||
|
||||
--interactive: oklch(0.18 0 0);
|
||||
--interactive-hover: oklch(0.25 0 0);
|
||||
--interactive-active: oklch(0.32 0 0);
|
||||
--interactive: oklch(0.18 0 0);
|
||||
--interactive-hover: oklch(0.25 0 0);
|
||||
--interactive-active: oklch(0.32 0 0);
|
||||
|
||||
--hover-bg: oklch(0.22 0 0 / 60%);
|
||||
--hover-bg-strong: oklch(0.28 0 0);
|
||||
--hover-bg: oklch(0.22 0 0 / 60%);
|
||||
--hover-bg-strong: oklch(0.28 0 0);
|
||||
|
||||
--input-bg: oklch(0.15 0 0);
|
||||
--input-placeholder: oklch(0.55 0 0);
|
||||
--input-ring: oklch(0.70 0.15 264);
|
||||
--input-bg: oklch(0.15 0 0);
|
||||
--input-placeholder: oklch(0.55 0 0);
|
||||
--input-ring: oklch(0.7 0.15 264);
|
||||
|
||||
/* Heatmap (contribution graph) */
|
||||
--heatmap-0: oklch(0.20 0 0);
|
||||
--heatmap-1: oklch(0.30 0.08 155);
|
||||
--heatmap-2: oklch(0.45 0.12 155);
|
||||
--heatmap-3: oklch(0.60 0.15 155);
|
||||
--heatmap-4: oklch(0.75 0.15 155);
|
||||
/* Heatmap (contribution graph) */
|
||||
--heatmap-0: oklch(0.2 0 0);
|
||||
--heatmap-1: oklch(0.3 0.08 155);
|
||||
--heatmap-2: oklch(0.45 0.12 155);
|
||||
--heatmap-3: oklch(0.6 0.15 155);
|
||||
--heatmap-4: oklch(0.75 0.15 155);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground antialiased;
|
||||
min-height: 100vh;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-attachment: fixed;
|
||||
background-image:
|
||||
radial-gradient(
|
||||
circle at top left,
|
||||
color-mix(in oklch, var(--accent) 7%, transparent) 0,
|
||||
transparent 28%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at top right,
|
||||
color-mix(in oklch, var(--accent) 5%, transparent) 0,
|
||||
transparent 24%
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
color-mix(in oklch, var(--surface-ground) 92%, var(--accent) 8%),
|
||||
var(--surface-ground)
|
||||
);
|
||||
}
|
||||
|
||||
html {
|
||||
@apply font-sans;
|
||||
}
|
||||
html {
|
||||
@apply scroll-smooth font-sans;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in oklch, var(--accent) 22%, transparent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Settings Modal open/close animation ─── */
|
||||
|
||||
.settings-dialog[data-state="open"] {
|
||||
animation: settings-modal-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation: settings-modal-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
.settings-dialog[data-state="closed"] {
|
||||
animation: settings-modal-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation: settings-modal-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
@keyframes settings-modal-open {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes settings-modal-close {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
[data-slot="dialog-overlay"][data-state="open"] {
|
||||
animation: settings-overlay-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation: settings-overlay-open 0.2s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
[data-slot="dialog-overlay"][data-state="closed"] {
|
||||
animation: settings-overlay-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
animation: settings-overlay-close 0.15s cubic-bezier(0.4, 0, 0.2, 1) both;
|
||||
}
|
||||
|
||||
@keyframes settings-overlay-open {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes settings-overlay-close {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─────────────────────────────────────────────
|
||||
@ -347,81 +380,82 @@ body {
|
||||
dark mode text never falls back to defaults
|
||||
───────────────────────────────────────────── */
|
||||
:root .prose {
|
||||
--tw-prose-body: var(--text-primary);
|
||||
--tw-prose-headings: var(--text-primary);
|
||||
--tw-prose-lead: var(--text-secondary);
|
||||
--tw-prose-links: var(--accent);
|
||||
--tw-prose-bold: var(--text-primary);
|
||||
--tw-prose-counters: var(--text-secondary);
|
||||
--tw-prose-bullets: var(--text-muted);
|
||||
--tw-prose-hr: var(--border-default);
|
||||
--tw-prose-quotes: var(--text-secondary);
|
||||
--tw-prose-quote-borders: var(--border-default);
|
||||
--tw-prose-captions: var(--text-muted);
|
||||
--tw-prose-kbd: var(--text-primary);
|
||||
--tw-prose-kbd-shadows: oklch(0.13 0 0 / 10%);
|
||||
--tw-prose-code: var(--accent);
|
||||
--tw-prose-pre-code: var(--text-primary);
|
||||
--tw-prose-pre-bg: var(--surface-elevated);
|
||||
--tw-prose-th-borders: var(--border-default);
|
||||
--tw-prose-td-borders: var(--border-subtle);
|
||||
--tw-prose-body: var(--text-primary);
|
||||
--tw-prose-headings: var(--text-primary);
|
||||
--tw-prose-lead: var(--text-secondary);
|
||||
--tw-prose-links: var(--accent);
|
||||
--tw-prose-bold: var(--text-primary);
|
||||
--tw-prose-counters: var(--text-secondary);
|
||||
--tw-prose-bullets: var(--text-muted);
|
||||
--tw-prose-hr: var(--border-default);
|
||||
--tw-prose-quotes: var(--text-secondary);
|
||||
--tw-prose-quote-borders: var(--border-default);
|
||||
--tw-prose-captions: var(--text-muted);
|
||||
--tw-prose-kbd: var(--text-primary);
|
||||
--tw-prose-kbd-shadows: oklch(0.13 0 0 / 10%);
|
||||
--tw-prose-code: var(--accent);
|
||||
--tw-prose-pre-code: var(--text-primary);
|
||||
--tw-prose-pre-bg: var(--surface-elevated);
|
||||
--tw-prose-th-borders: var(--border-default);
|
||||
--tw-prose-td-borders: var(--border-subtle);
|
||||
}
|
||||
|
||||
.dark .prose {
|
||||
--tw-prose-body: var(--text-primary);
|
||||
--tw-prose-headings: var(--text-primary);
|
||||
--tw-prose-lead: var(--text-secondary);
|
||||
--tw-prose-links: var(--accent);
|
||||
--tw-prose-bold: var(--text-primary);
|
||||
--tw-prose-counters: var(--text-secondary);
|
||||
--tw-prose-bullets: var(--text-muted);
|
||||
--tw-prose-hr: var(--border-default);
|
||||
--tw-prose-quotes: var(--text-secondary);
|
||||
--tw-prose-quote-borders: var(--border-default);
|
||||
--tw-prose-captions: var(--text-muted);
|
||||
--tw-prose-kbd: var(--text-primary);
|
||||
--tw-prose-kbd-shadows: oklch(0.97 0 0 / 10%);
|
||||
--tw-prose-code: var(--accent);
|
||||
--tw-prose-pre-code: var(--text-primary);
|
||||
--tw-prose-pre-bg: var(--surface-elevated);
|
||||
--tw-prose-th-borders: var(--border-default);
|
||||
--tw-prose-td-borders: var(--border-subtle);
|
||||
--tw-prose-body: var(--text-primary);
|
||||
--tw-prose-headings: var(--text-primary);
|
||||
--tw-prose-lead: var(--text-secondary);
|
||||
--tw-prose-links: var(--accent);
|
||||
--tw-prose-bold: var(--text-primary);
|
||||
--tw-prose-counters: var(--text-secondary);
|
||||
--tw-prose-bullets: var(--text-muted);
|
||||
--tw-prose-hr: var(--border-default);
|
||||
--tw-prose-quotes: var(--text-secondary);
|
||||
--tw-prose-quote-borders: var(--border-default);
|
||||
--tw-prose-captions: var(--text-muted);
|
||||
--tw-prose-kbd: var(--text-primary);
|
||||
--tw-prose-kbd-shadows: oklch(0.97 0 0 / 10%);
|
||||
--tw-prose-code: var(--accent);
|
||||
--tw-prose-pre-code: var(--text-primary);
|
||||
--tw-prose-pre-bg: var(--surface-elevated);
|
||||
--tw-prose-th-borders: var(--border-default);
|
||||
--tw-prose-td-borders: var(--border-subtle);
|
||||
}
|
||||
|
||||
.app-scrollbar {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in oklch, var(--text-muted) 34%, transparent) transparent;
|
||||
scrollbar-gutter: stable;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: color-mix(in oklch, var(--text-muted) 34%, transparent)
|
||||
transparent;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.app-scrollbar::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.app-scrollbar::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.app-scrollbar::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in oklch, var(--text-muted) 28%, transparent);
|
||||
border: 3px solid transparent;
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
background: color-mix(in oklch, var(--text-muted) 28%, transparent);
|
||||
border: 3px solid transparent;
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.app-scrollbar:hover::-webkit-scrollbar-thumb {
|
||||
background: color-mix(in oklch, var(--text-secondary) 42%, transparent);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
background: color-mix(in oklch, var(--text-secondary) 42%, transparent);
|
||||
border: 2px solid transparent;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
|
||||
.app-scrollbar[data-scrollbar="room"]::-webkit-scrollbar {
|
||||
width: 12px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
.pre_nobackground {
|
||||
pre {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
pre {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
13
src/lib/color.ts
Normal file
13
src/lib/color.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const AVATAR_COLORS = [
|
||||
"#6366f1", "#8b5cf6", "#d946ef", "#ec4899", "#f43f5e",
|
||||
"#ef4444", "#f97316", "#eab308", "#22c55e", "#14b8a6",
|
||||
"#06b6d4", "#3b82f6", "#2563eb", "#7c3aed", "#c026d3",
|
||||
];
|
||||
|
||||
export function hashColor(str: string): string {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return AVATAR_COLORS[Math.abs(hash) % AVATAR_COLORS.length];
|
||||
}
|
||||
79
src/lib/theme-vars.ts
Normal file
79
src/lib/theme-vars.ts
Normal file
@ -0,0 +1,79 @@
|
||||
export interface ThemeVarMap {
|
||||
[key: string]: string
|
||||
}
|
||||
|
||||
export const CUSTOM_THEME_VARS_STORAGE_KEY = "custom-theme-vars"
|
||||
|
||||
function getRoot() {
|
||||
if (typeof document === "undefined") {
|
||||
return null
|
||||
}
|
||||
|
||||
return document.documentElement
|
||||
}
|
||||
|
||||
function readStoredThemeVars(): ThemeVarMap {
|
||||
if (typeof window === "undefined") {
|
||||
return {}
|
||||
}
|
||||
|
||||
const stored = localStorage.getItem(CUSTOM_THEME_VARS_STORAGE_KEY)
|
||||
if (!stored) {
|
||||
return {}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(stored) as ThemeVarMap
|
||||
} catch {
|
||||
console.error("Failed to parse stored theme vars")
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyThemeVars(vars: ThemeVarMap) {
|
||||
const root = getRoot()
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
Object.entries(vars).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${key}`, value)
|
||||
})
|
||||
}
|
||||
|
||||
export function removeThemeVars(keys: Iterable<string>) {
|
||||
const root = getRoot()
|
||||
if (!root) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
root.style.removeProperty(`--${key}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function loadThemeVars() {
|
||||
const vars = readStoredThemeVars()
|
||||
applyThemeVars(vars)
|
||||
return vars
|
||||
}
|
||||
|
||||
export function resetAllThemeVars(vars?: ThemeVarMap) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.removeItem(CUSTOM_THEME_VARS_STORAGE_KEY)
|
||||
}
|
||||
|
||||
removeThemeVars(Object.keys(vars ?? readStoredThemeVars()))
|
||||
}
|
||||
|
||||
export function saveThemeVars(vars: ThemeVarMap) {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem(CUSTOM_THEME_VARS_STORAGE_KEY, JSON.stringify(vars))
|
||||
}
|
||||
|
||||
applyThemeVars(vars)
|
||||
}
|
||||
|
||||
export function getStoredThemeVars() {
|
||||
return readStoredThemeVars()
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user