feat: theme family system with 12 color palettes + NavRail redesign

Theme system:
- Two-layer architecture: mode (light/dark/system via next-themes) + theme family (color palette via CSS variables)
- 12 theme families: Default, GitHub, Rosé Pine, Tokyo Night, Everforest, Nord, Kanagawa, Night Owl, Poimandres, Vesper, Horizon, SynthWave '84
- Per-family code block highlighting for Prism, HLJS, and Shiki (no more generic oneLight/oneDark fallback)
- Anti-FOUC: synchronous script in <head> reads localStorage and sets data-theme-family before paint
- Theme JSON files in themes/ directory, auto-discovered by server-side loader
- OKLCH color space for all values; CSS specificity html[data-theme-family] beats :root

UI changes:
- NavRail: moved Settings to bottom (replacing theme toggle), removed light/dark toggle button
- Settings: merged General controls into one card with dividers; Appearance section with live Shiki code preview
- Replaced hardcoded blue-500/blue-600 with primary/primary-foreground across 19 components
- Appearance settings: pill mode selector (light/dark/system), theme family dropdown, live code preview

New files:
- src/lib/theme/{types,loader,render-css,context,code-themes}.ts — core theme infrastructure
- src/components/layout/ThemeFamilyProvider.tsx — client context provider
- src/components/settings/AppearanceSection.tsx — appearance settings UI
- src/hooks/useAppTheme.ts — combined theme hook
- themes/*.json — 12 theme family definitions
- docs/handover/theme-system.md — handover documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
7418andClaude Opus 4.6 committed 2026-03-09 11:15:01 +08:00
1 parent 7ad66f82c4
commit 4da188bfe5
53 files changed
+2446 -144

No files matched your search

+45 -23
View File
@@ -18,6 +18,8 @@ import {
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { useThemeFamily } from "@/lib/theme/context";
import { resolveShikiTheme, resolveShikiThemes, SHIKI_DEFAULT_LIGHT, SHIKI_DEFAULT_DARK } from "@/lib/theme/code-themes";
import { CheckIcon, CopyIcon } from "lucide-react";
import {
createContext,
@@ -121,7 +123,7 @@ const CodeBlockContext = createContext<CodeBlockContextType>({
code: "",
});
// Highlighter cache (singleton per language)
// Highlighter cache keyed by "lang:lightTheme:darkTheme"
const highlighterCache = new Map<
string,
Promise<HighlighterGeneric<BundledLanguage, BundledTheme>>
@@ -133,36 +135,45 @@ const tokensCache = new Map<string, TokenizedCode>();
// Subscribers for async token updates
const subscribers = new Map<string, Set<(result: TokenizedCode) => void>>();
const getTokensCacheKey = (code: string, language: BundledLanguage) => {
const getTokensCacheKey = (code: string, language: BundledLanguage, lightTheme: BundledTheme, darkTheme: BundledTheme) => {
const start = code.slice(0, 100);
const end = code.length > 100 ? code.slice(-100) : "";
return `${language}:${code.length}:${start}:${end}`;
return `${language}:${lightTheme}:${darkTheme}:${code.length}:${start}:${end}`;
};
const isBundledLanguage = (lang: string): lang is BundledLanguage =>
lang in bundledLanguages || lang === "text" || lang === "plaintext";
const getHighlighter = (
language: BundledLanguage
language: BundledLanguage,
lightTheme: BundledTheme = SHIKI_DEFAULT_LIGHT,
darkTheme: BundledTheme = SHIKI_DEFAULT_DARK,
): Promise<HighlighterGeneric<BundledLanguage, BundledTheme>> => {
// Normalize unknown languages to "text" before hitting Shiki
const safeLang = isBundledLanguage(language) ? language : ("text" as BundledLanguage);
const cacheKey = `${safeLang}:${lightTheme}:${darkTheme}`;
const cached = highlighterCache.get(safeLang);
const cached = highlighterCache.get(cacheKey);
if (cached) {
return cached;
}
const highlighterPromise = createHighlighter({
langs: [safeLang],
themes: ["github-light", "github-dark"],
themes: [lightTheme, darkTheme],
}).catch(() => {
// Language not supported by Shiki — fall back to plain text
highlighterCache.delete(safeLang);
return getHighlighter("text" as BundledLanguage);
// Language or theme not supported — fall back to plain text + default themes.
// Using default themes avoids infinite retry if the *theme* was the problem.
highlighterCache.delete(cacheKey);
const useFallbackThemes =
lightTheme !== SHIKI_DEFAULT_LIGHT || darkTheme !== SHIKI_DEFAULT_DARK;
if (useFallbackThemes) {
return getHighlighter("text" as BundledLanguage, SHIKI_DEFAULT_LIGHT, SHIKI_DEFAULT_DARK);
}
return getHighlighter("text" as BundledLanguage, lightTheme, darkTheme);
});
highlighterCache.set(safeLang, highlighterPromise);
highlighterCache.set(cacheKey, highlighterPromise);
return highlighterPromise;
};
@@ -187,9 +198,11 @@ export const highlightCode = (
code: string,
language: BundledLanguage,
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-callbacks)
callback?: (result: TokenizedCode) => void
callback?: (result: TokenizedCode) => void,
lightTheme: BundledTheme = SHIKI_DEFAULT_LIGHT,
darkTheme: BundledTheme = SHIKI_DEFAULT_DARK,
): TokenizedCode | null => {
const tokensCacheKey = getTokensCacheKey(code, language);
const tokensCacheKey = getTokensCacheKey(code, language, lightTheme, darkTheme);
// Return cached result if available
const cached = tokensCache.get(tokensCacheKey);
@@ -206,7 +219,7 @@ export const highlightCode = (
}
// Start highlighting in background - fire-and-forget async pattern
getHighlighter(language)
getHighlighter(language, lightTheme, darkTheme)
// oxlint-disable-next-line eslint-plugin-promise(prefer-await-to-then)
.then((highlighter) => {
const availableLangs = highlighter.getLoadedLanguages();
@@ -215,8 +228,8 @@ export const highlightCode = (
const result = highlighter.codeToTokens(code, {
lang: langToUse,
themes: {
dark: "github-dark",
light: "github-light",
dark: darkTheme,
light: lightTheme,
},
});
@@ -387,6 +400,13 @@ export const CodeBlockActions = ({
</div>
);
/** Resolve Shiki theme pair from the current theme family. */
function useShikiThemes(): { light: BundledTheme; dark: BundledTheme } {
const { family, families } = useThemeFamily();
const shikiTheme = resolveShikiTheme(families, family);
return resolveShikiThemes(shikiTheme);
}
export const CodeBlockContent = ({
code,
language,
@@ -396,18 +416,20 @@ export const CodeBlockContent = ({
language: BundledLanguage;
showLineNumbers?: boolean;
}) => {
const { light: lightTheme, dark: darkTheme } = useShikiThemes();
// Memoized raw tokens for immediate display
const rawTokens = useMemo(() => createRawTokens(code), [code]);
// Try to get cached result synchronously, otherwise use raw tokens
const syncTokenized = useMemo(
() => highlightCode(code, language) ?? rawTokens,
[code, language, rawTokens]
() => highlightCode(code, language, undefined, lightTheme, darkTheme) ?? rawTokens,
[code, language, rawTokens, lightTheme, darkTheme]
);
// Track async highlighting results keyed by code+language to avoid stale state
// Track async highlighting results keyed by code+language+themes to avoid stale state
const [asyncResult, setAsyncResult] = useState<{ key: string; tokens: TokenizedCode } | null>(null);
const resultKey = `${code}:${language}`;
const resultKey = `${code}:${language}:${lightTheme}:${darkTheme}`;
useEffect(() => {
let cancelled = false;
@@ -415,16 +437,16 @@ export const CodeBlockContent = ({
// Subscribe to async highlighting result
highlightCode(code, language, (result) => {
if (!cancelled) {
setAsyncResult({ key: `${code}:${language}`, tokens: result });
setAsyncResult({ key: `${code}:${language}:${lightTheme}:${darkTheme}`, tokens: result });
}
});
}, lightTheme, darkTheme);
return () => {
cancelled = true;
};
}, [code, language]);
}, [code, language, lightTheme, darkTheme]);
// Only use async result if it matches the current code+language
// Only use async result if it matches the current code+language+themes
const tokenized = (asyncResult && asyncResult.key === resultKey) ? asyncResult.tokens : syncTokenized;
return (
+3 -3
View File
@@ -257,7 +257,7 @@ export function BridgeSection() {
<div
className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${
isEnabled
? "border-blue-500/50 bg-blue-500/5"
? "border-primary/50 bg-primary/5"
: "border-border/50"
}`}
>
@@ -275,8 +275,8 @@ export function BridgeSection() {
/>
</div>
{isEnabled && (
<div className="mt-3 flex items-center gap-2 rounded-md bg-blue-500/10 px-3 py-2 text-xs text-blue-600 dark:text-blue-400">
<span className="h-2 w-2 shrink-0 rounded-full bg-blue-500" />
<div className="mt-3 flex items-center gap-2 rounded-md bg-primary/10 px-3 py-2 text-xs text-primary">
<span className="h-2 w-2 shrink-0 rounded-full bg-primary" />
{t("bridge.activeHint")}
</div>
)}
+48 -17
View File
@@ -2,7 +2,10 @@
import { useState, useMemo, useRef } from 'react';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark, vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { useThemeFamily } from '@/lib/theme/context';
import { resolveCodeTheme, resolvePrismStyle } from '@/lib/theme/code-themes';
import { useTheme } from 'next-themes';
import { HugeiconsIcon } from "@hugeicons/react";
import type { IconSvgElement } from "@hugeicons/react";
import {
@@ -41,6 +44,14 @@ interface CodeBlockProps {
maxCollapsedLines?: number;
}
/** Resolve the Prism syntax highlighting style for CodeBlock. */
function useCodeBlockTheme(isTerminal: boolean, isDark: boolean) {
const { family, families } = useThemeFamily();
if (isTerminal) return vscDarkPlus;
const codeTheme = resolveCodeTheme(families, family);
return resolvePrismStyle(codeTheme, isDark);
}
export function CodeBlock({
code,
language = 'text',
@@ -53,6 +64,8 @@ export function CodeBlock({
const [expanded, setExpanded] = useState(false);
const codeContainerRef = useRef<HTMLDivElement>(null);
const [animatingHeight, setAnimatingHeight] = useState<string | undefined>(undefined);
const { resolvedTheme } = useTheme();
const isDark = resolvedTheme === 'dark';
const lines = useMemo(() => code.split('\n'), [code]);
const totalLines = lines.length;
@@ -93,17 +106,14 @@ export function CodeBlock({
}
const currentHeight = container.scrollHeight;
if (!expanded) {
// Expanding: set current height, then switch to auto after transition
setAnimatingHeight(`${currentHeight}px`);
setExpanded(true);
requestAnimationFrame(() => {
// Measure the full height after content change
const fullHeight = container.scrollHeight;
setAnimatingHeight(`${fullHeight}px`);
setTimeout(() => setAnimatingHeight(undefined), 300);
});
} else {
// Collapsing: set current height, then reduce
setAnimatingHeight(`${currentHeight}px`);
requestAnimationFrame(() => {
const collapsedH = maxCollapsedLines * 1.5 + 1.5;
@@ -118,35 +128,50 @@ export function CodeBlock({
const languageIconData = getLanguageIcon(language);
const theme = isTerminal ? vscDarkPlus : oneDark;
const theme = useCodeBlockTheme(isTerminal, isDark);
return (
<div className="relative group not-prose my-3 rounded-lg overflow-hidden border border-zinc-700/50">
<div className={cn(
"relative group not-prose my-3 rounded-lg overflow-hidden",
isTerminal
? "border border-zinc-700/50"
: "border border-border"
)}>
{/* Header bar */}
<div className={cn(
"flex items-center justify-between px-4 py-1.5 text-xs",
isTerminal
? "bg-zinc-950 text-zinc-400"
: "bg-zinc-800 dark:bg-zinc-900 text-zinc-400"
: "bg-muted text-muted-foreground"
)}>
<div className="flex items-center gap-2 min-w-0">
<HugeiconsIcon icon={languageIconData} className={cn(
"h-3.5 w-3.5 shrink-0",
isTerminal ? "text-green-400" : "text-zinc-400",
isTerminal ? "text-green-400" : "text-muted-foreground",
)} />
{filename && (
<span className="truncate text-zinc-300 font-medium">{filename}</span>
<span className={cn(
"truncate font-medium",
isTerminal ? "text-zinc-300" : "text-foreground"
)}>{filename}</span>
)}
{filename && <span className="text-zinc-600">|</span>}
{filename && <span className="text-muted-foreground/50">|</span>}
<span className={cn(
"bg-zinc-700/50 rounded px-1.5 py-0.5",
isTerminal && "text-green-400",
"rounded px-1.5 py-0.5",
isTerminal
? "bg-zinc-700/50 text-green-400"
: "bg-accent text-accent-foreground"
)}>{language.toUpperCase()}</span>
</div>
<div className="flex items-center gap-1 ml-2 shrink-0">
<button
onClick={handleCopy}
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors"
className={cn(
"flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors",
isTerminal
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
title="Copy code"
>
{copied ? (
@@ -163,7 +188,12 @@ export function CodeBlock({
</button>
<button
onClick={handleCopyMarkdown}
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50 transition-colors"
className={cn(
"flex items-center gap-1 rounded px-1.5 py-0.5 transition-colors",
isTerminal
? "text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700/50"
: "text-muted-foreground hover:text-foreground hover:bg-accent"
)}
title="Copy as Markdown"
>
{copiedMarkdown ? (
@@ -197,7 +227,8 @@ export function CodeBlock({
lineNumberStyle={{
minWidth: '2.5em',
paddingRight: '1em',
color: '#3a3a48',
color: 'var(--muted-foreground)',
opacity: 0.4,
userSelect: 'none',
}}
customStyle={{
@@ -220,7 +251,7 @@ export function CodeBlock({
"absolute bottom-0 left-0 right-0 h-16 pointer-events-none",
isTerminal
? "bg-gradient-to-t from-[#0a0a0a] to-transparent"
: "bg-gradient-to-t from-[#282c34] to-transparent"
: "bg-gradient-to-t from-muted to-transparent"
)} />
)}
</div>
@@ -233,7 +264,7 @@ export function CodeBlock({
"flex w-full items-center justify-center gap-1.5 py-1.5 text-xs transition-colors",
isTerminal
? "bg-zinc-950 text-zinc-400 hover:text-zinc-200"
: "bg-zinc-800 dark:bg-zinc-900 text-zinc-400 hover:text-zinc-200"
: "bg-muted text-muted-foreground hover:text-foreground"
)}
>
{expanded ? (
+1 -1
View File
@@ -178,7 +178,7 @@ export function FolderPicker({ open, onOpenChange, onSelect, initialPath }: Fold
className="flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-sm hover:bg-accent transition-colors text-left"
onClick={() => handleNavigate(dir.path)}
>
<HugeiconsIcon icon={Folder01Icon} className="h-4 w-4 shrink-0 text-blue-500" />
<HugeiconsIcon icon={Folder01Icon} className="h-4 w-4 shrink-0 text-primary" />
<span className="truncate">{dir.name}</span>
<HugeiconsIcon icon={ArrowRight01Icon} className="ml-auto h-3 w-3 shrink-0 text-muted-foreground" />
</button>
+2 -2
View File
@@ -287,7 +287,7 @@ export function ImageGenConfirmation({
'px-2.5 py-1 text-xs font-medium rounded-md border transition-colors',
'disabled:opacity-60 disabled:cursor-not-allowed',
aspectRatio === ratio
? 'border-blue-500 bg-blue-500/10 text-blue-600 dark:text-blue-400'
? 'border-primary bg-primary/10 text-primary'
: 'border-border/60 text-muted-foreground hover:text-foreground hover:border-foreground/30'
)}
>
@@ -313,7 +313,7 @@ export function ImageGenConfirmation({
'px-3 py-1 text-xs font-medium rounded-md border transition-colors',
'disabled:opacity-60 disabled:cursor-not-allowed',
resolution === res
? 'border-blue-500 bg-blue-500/10 text-blue-600 dark:text-blue-400'
? 'border-primary bg-primary/10 text-primary'
: 'border-border/60 text-muted-foreground hover:text-foreground hover:border-foreground/30'
)}
>
+1 -1
View File
@@ -27,7 +27,7 @@ export function ImageGenToggle() {
className={cn(
'inline-flex items-center justify-center rounded-full px-2.5 h-7 text-xs font-medium border transition-all',
state.enabled
? 'bg-blue-500/15 text-blue-600 dark:text-blue-400 border-blue-500/30'
? 'bg-primary/15 text-primary border-primary/30'
: 'text-muted-foreground border-border/60 hover:text-foreground hover:border-foreground/30 hover:bg-accent/50'
)}
>
+3 -3
View File
@@ -1133,15 +1133,15 @@ export function MessageInput({
{/* Command badge */}
{badge && (
<div className="flex w-full items-center gap-1.5 px-3 pt-2.5 pb-0 order-first">
<span className="inline-flex items-center gap-1.5 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400 pl-2.5 pr-1.5 py-1 text-xs font-medium border border-blue-500/20">
<span className="inline-flex items-center gap-1.5 rounded-full bg-primary/10 text-primary pl-2.5 pr-1.5 py-1 text-xs font-medium border border-primary/20">
<span className="font-mono">{badge.command}</span>
{badge.description && (
<span className="text-blue-500/60 dark:text-blue-400/60 text-[10px]">{badge.description}</span>
<span className="text-primary/60 text-[10px]">{badge.description}</span>
)}
<button
type="button"
onClick={removeBadge}
className="ml-0.5 rounded-full p-0.5 hover:bg-blue-500/20 transition-colors"
className="ml-0.5 rounded-full p-0.5 hover:bg-primary/20 transition-colors"
>
<svg className="h-3 w-3" viewBox="0 0 12 12" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M3 3l6 6M9 3l-6 6" />
+1 -1
View File
@@ -106,7 +106,7 @@ function RewindButton({ sessionId, userMessageId }: { sessionId: string; userMes
</span>
<button
onClick={handleRewind}
className="text-[10px] text-blue-600 dark:text-blue-400 hover:underline"
className="text-[10px] text-primary hover:underline"
>
{t('messageList.rewindConfirm' as TranslationKey)}
</button>
+7 -7
View File
@@ -100,8 +100,8 @@ function StatusIndicator({ status }: { status: ToolStatus }) {
case 'running':
return (
<span className="relative flex h-3.5 w-3.5 items-center justify-center">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-30" />
<HugeiconsIcon icon={Loading02Icon} className="relative h-3.5 w-3.5 animate-spin text-blue-500" />
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-30" />
<HugeiconsIcon icon={Loading02Icon} className="relative h-3.5 w-3.5 animate-spin text-primary" />
</span>
);
case 'success':
@@ -282,13 +282,13 @@ export function ToolCallBlock({
};
const statusBorderColor = {
running: 'border-blue-500/70',
running: 'border-primary/70',
success: 'border-green-500/50',
error: 'border-red-500/60',
}[status];
const statusBgColor = {
running: 'bg-blue-500/[0.03] dark:bg-blue-500/[0.05]',
running: 'bg-primary/[0.03] dark:bg-primary/[0.05]',
success: 'bg-transparent',
error: 'bg-red-500/[0.03] dark:bg-red-500/[0.05]',
}[status];
@@ -309,11 +309,11 @@ export function ToolCallBlock({
)}
<HugeiconsIcon icon={toolIconData} className={cn(
"h-3.5 w-3.5 shrink-0",
category === 'read' && "text-blue-500",
category === 'read' && "text-primary",
category === 'write' && "text-amber-500",
category === 'bash' && "text-green-500",
category === 'search' && "text-indigo-500",
category === 'other' && "text-zinc-500",
category === 'search' && "text-primary",
category === 'other' && "text-muted-foreground",
)} />
<span className="font-mono text-xs truncate flex-1 text-foreground/80">{summary}</span>
<div className="flex items-center gap-2 shrink-0 ml-2">
@@ -13,7 +13,7 @@ export function BatchExecutionItem({ item }: BatchExecutionItemProps) {
const statusColor = {
pending: 'text-muted-foreground',
processing: 'text-blue-500',
processing: 'text-primary',
completed: 'text-green-500',
failed: 'text-red-500',
cancelled: 'text-muted-foreground',
@@ -37,7 +37,7 @@ export function BatchExecutionItem({ item }: BatchExecutionItemProps) {
{/* Status indicator */}
<div className="shrink-0">
{item.status === 'processing' ? (
<div className="w-4 h-4 rounded-full border-2 border-blue-500 border-t-transparent animate-spin" />
<div className="w-4 h-4 rounded-full border-2 border-primary border-t-transparent animate-spin" />
) : item.status === 'completed' ? (
<div className="w-4 h-4 rounded-full bg-green-500/20 flex items-center justify-center">
<svg className="w-2.5 h-2.5 text-green-500" viewBox="0 0 12 12" fill="none">
+13 -4
View File
@@ -6,8 +6,8 @@ import { HugeiconsIcon } from "@hugeicons/react";
import { Cancel01Icon, Copy01Icon, Tick01Icon, Loading02Icon } from "@hugeicons/core-free-icons";
import { Button } from "@/components/ui/button";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useThemeFamily } from "@/lib/theme/context";
import { resolveCodeTheme, resolveHljsStyle } from "@/lib/theme/code-themes";
import { Streamdown } from "streamdown";
import { cjk } from "@streamdown/cjk";
import { code } from "@streamdown/code";
@@ -216,13 +216,21 @@ function ViewModeToggle({
);
}
/** Resolve hljs style from the current theme family + mode. */
function useDocCodeTheme(isDark: boolean) {
const { family, families } = useThemeFamily();
const codeTheme = resolveCodeTheme(families, family);
return resolveHljsStyle(codeTheme, isDark);
}
/** Source code view using react-syntax-highlighter */
function SourceView({ preview, isDark }: { preview: FilePreviewType; isDark: boolean }) {
const hljsStyle = useDocCodeTheme(isDark);
return (
<div className="text-xs">
<SyntaxHighlighter
language={preview.language}
style={isDark ? atomOneDark : atomOneLight}
style={hljsStyle}
showLineNumbers
customStyle={{
margin: 0,
@@ -235,7 +243,8 @@ function SourceView({ preview, isDark }: { preview: FilePreviewType; isDark: boo
lineNumberStyle={{
minWidth: "2.5em",
paddingRight: "8px",
color: isDark ? "#636d83" : "#9ca3af",
color: "var(--muted-foreground)",
opacity: 0.5,
userSelect: "none",
}}
>
+6 -6
View File
@@ -70,7 +70,7 @@ function StepIcon({ status }: { status: string }) {
case "success":
return <HugeiconsIcon icon={Tick01Icon} className="size-4 text-emerald-500" />;
case "running":
return <HugeiconsIcon icon={Loading02Icon} className="size-4 text-blue-500 animate-spin" />;
return <HugeiconsIcon icon={Loading02Icon} className="size-4 text-primary animate-spin" />;
case "failed":
return <HugeiconsIcon icon={Cancel01Icon} className="size-4 text-red-500" />;
case "skipped":
@@ -223,11 +223,11 @@ export function InstallWizard({
useEffect(() => {
if (open) {
setPhase("checking"); // eslint-disable-line react-hooks/set-state-in-effect -- reset state before async check
setLogs([]); // eslint-disable-line react-hooks/set-state-in-effect
setProgress(null); // eslint-disable-line react-hooks/set-state-in-effect
setCopied(false); // eslint-disable-line react-hooks/set-state-in-effect
setCopiedBrew(false); // eslint-disable-line react-hooks/set-state-in-effect
setPrereqs(null); // eslint-disable-line react-hooks/set-state-in-effect
setLogs([]);
setProgress(null);
setCopied(false);
setCopiedBrew(false);
setPrereqs(null);
checkPrereqs();
}
return () => {
+24 -33
View File
@@ -2,8 +2,6 @@
import Link from "next/link";
import { usePathname, useRouter } from "next/navigation";
import { useTheme } from "next-themes";
import { useCallback, useSyncExternalStore } from "react";
import { HugeiconsIcon } from "@hugeicons/react";
import {
Message02Icon,
@@ -12,8 +10,6 @@ import {
Image01Icon,
Settings02Icon,
Wifi01Icon,
Moon02Icon,
Sun02Icon,
} from "@hugeicons/core-free-icons";
import { Button } from "@/components/ui/button";
import {
@@ -40,13 +36,11 @@ const navItems = [
{ href: "/mcp", label: "MCP", icon: Plug01Icon },
{ href: "/gallery", label: "Gallery", icon: Image01Icon },
{ href: "/bridge", label: "Bridge", icon: Wifi01Icon },
{ href: "/settings", label: "Settings", icon: Settings02Icon },
] as const;
export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermissionsActive }: NavRailProps) {
const pathname = usePathname();
const router = useRouter();
const { theme, setTheme } = useTheme();
const { t } = useTranslation();
const navLabelKeys: Record<string, TranslationKey> = {
'Chats': 'nav.chats',
@@ -54,11 +48,9 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
'MCP': 'extensions.mcpServers',
'Gallery': 'gallery.title',
'Bridge': 'nav.bridge',
'Settings': 'nav.settings',
};
const emptySubscribe = useCallback(() => () => {}, []);
const mounted = useSyncExternalStore(emptySubscribe, () => true, () => false);
const isChatRoute = pathname === "/chat" || pathname.startsWith("/chat/");
const isSettingsActive = pathname === "/settings" || pathname.startsWith("/settings/");
return (
<aside className="flex w-14 shrink-0 flex-col items-center bg-sidebar pb-3 pt-10">
@@ -110,12 +102,6 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
<span className="sr-only">{t(navLabelKeys[item.label] ?? item.label as TranslationKey)}</span>
</Link>
</Button>
{item.href === "/settings" && hasUpdate && (
<span className={cn(
"absolute top-0.5 right-0.5 h-2 w-2 rounded-full",
readyToInstall ? "bg-green-500 animate-pulse" : "bg-blue-500"
)} />
)}
</div>
)}
</TooltipTrigger>
@@ -125,7 +111,7 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
})}
</nav>
{/* Bottom: skip-permissions indicator + theme toggle */}
{/* Bottom: skip-permissions indicator + settings */}
<div className="mt-auto flex flex-col items-center gap-2">
{skipPermissionsActive && (
<Tooltip>
@@ -140,28 +126,33 @@ export function NavRail({ onToggleChatList, hasUpdate, readyToInstall, skipPermi
<TooltipContent side="right">{t('nav.autoApproveOn')}</TooltipContent>
</Tooltip>
)}
{mounted && (
<Tooltip>
<TooltipTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<div className="relative">
<Button
asChild
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
className="h-8 w-8"
>
{theme === "dark" ? (
<HugeiconsIcon icon={Sun02Icon} className="h-4 w-4" />
) : (
<HugeiconsIcon icon={Moon02Icon} className="h-4 w-4" />
className={cn(
"h-9 w-9",
isSettingsActive && "bg-sidebar-accent text-sidebar-accent-foreground"
)}
<span className="sr-only">{t('nav.toggleTheme')}</span>
>
<Link href="/settings">
<HugeiconsIcon icon={Settings02Icon} className="h-4 w-4" />
<span className="sr-only">{t('nav.settings')}</span>
</Link>
</Button>
</TooltipTrigger>
<TooltipContent side="right">
{theme === "dark" ? t('nav.lightMode') : t('nav.darkMode')}
</TooltipContent>
</Tooltip>
)}
{hasUpdate && (
<span className={cn(
"absolute top-0.5 right-0.5 h-2 w-2 rounded-full",
readyToInstall ? "bg-green-500 animate-pulse" : "bg-primary"
)} />
)}
</div>
</TooltipTrigger>
<TooltipContent side="right">{t('nav.settings')}</TooltipContent>
</Tooltip>
</div>
</aside>
);
+3 -3
View File
@@ -113,7 +113,7 @@ export function SplitColumn({ sessionId, isActive, onClose, onFocus }: SplitColu
<div
className={cn(
"flex flex-1 min-w-0 flex-col overflow-hidden rounded-md border-2 transition-colors",
isActive ? "border-blue-500" : "border-transparent"
isActive ? "border-primary" : "border-transparent"
)}
onClick={onFocus}
>
@@ -129,7 +129,7 @@ export function SplitColumn({ sessionId, isActive, onClose, onFocus }: SplitColu
<div
className={cn(
"flex flex-1 min-w-0 flex-col overflow-hidden rounded-md border-2 transition-colors",
isActive ? "border-blue-500" : "border-transparent"
isActive ? "border-primary" : "border-transparent"
)}
onClick={onFocus}
>
@@ -144,7 +144,7 @@ export function SplitColumn({ sessionId, isActive, onClose, onFocus }: SplitColu
<div
className={cn(
"flex flex-1 min-w-0 flex-col overflow-hidden rounded-md border-2 transition-colors",
isActive ? "border-blue-500" : "border-transparent"
isActive ? "border-primary" : "border-transparent"
)}
onClick={onFocus}
>
@@ -0,0 +1,60 @@
"use client";
import { useState, useCallback, useEffect, type ReactNode } from 'react';
import { ThemeFamilyContext } from '@/lib/theme/context';
import type { ThemeFamilyMeta } from '@/lib/theme/types';
const STORAGE_KEY = 'codepilot_theme_family';
function getInitialFamily(): string {
// Read from DOM attribute (set by anti-FOUC script before hydration)
if (typeof document !== 'undefined') {
return document.documentElement.dataset.themeFamily || 'default';
}
return 'default';
}
interface ThemeFamilyProviderProps {
families: ThemeFamilyMeta[];
children: ReactNode;
}
export function ThemeFamilyProvider({ families, children }: ThemeFamilyProviderProps) {
const [rawFamily, setFamilyState] = useState(getInitialFamily);
// Derive validated family: if stored ID no longer exists in loaded themes, use 'default'.
const isValid = families.length === 0 || families.some((f) => f.id === rawFamily);
const family = isValid ? rawFamily : 'default';
// When the derived family differs from raw (i.e. fallback occurred),
// sync DOM attribute and localStorage. This effect does NOT call setState,
// avoiding the cascading-render lint error.
useEffect(() => {
if (family !== rawFamily) {
document.documentElement.setAttribute('data-theme-family', family);
try {
localStorage.setItem(STORAGE_KEY, family);
} catch {
// localStorage may be unavailable
}
}
}, [family, rawFamily]);
const setFamily = useCallback((id: string) => {
setFamilyState(id);
// Sync to DOM immediately for instant visual update
document.documentElement.setAttribute('data-theme-family', id);
// Persist to localStorage (sole source of truth for theme family)
try {
localStorage.setItem(STORAGE_KEY, id);
} catch {
// localStorage may be unavailable
}
}, []);
return (
<ThemeFamilyContext.Provider value={{ family, setFamily, families }}>
{children}
</ThemeFamilyContext.Provider>
);
}
+1 -1
View File
@@ -11,7 +11,7 @@ export function UpdateBanner() {
if (!updateInfo?.isNativeUpdate || !updateInfo.readyToInstall) return null;
return (
<div className="flex items-center justify-center gap-3 border-b border-blue-500/20 bg-blue-500/10 px-4 py-1.5 text-sm">
<div className="flex items-center justify-center gap-3 border-b border-primary/20 bg-primary/10 px-4 py-1.5 text-sm">
<span>{t('update.readyToInstall', { version: updateInfo.latestVersion })}</span>
<Button size="sm" variant="outline" className="h-6 text-xs" onClick={quitAndInstall}>
{t('update.restartNow')}
+2 -2
View File
@@ -53,7 +53,7 @@ export function UpdateDialog() {
ol: ({ children }) => <ol className="mb-2 list-decimal pl-4 text-sm">{children}</ol>,
li: ({ children }) => <li className="mb-0.5">{children}</li>,
a: ({ href, children }) => (
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-500 underline">
<a href={href} target="_blank" rel="noopener noreferrer" className="text-primary underline">
{children}
</a>
),
@@ -88,7 +88,7 @@ export function UpdateDialog() {
<div className="space-y-1">
<div className="h-2 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-blue-500 transition-all"
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${Math.min(downloadProgress!, 100)}%` }}
/>
</div>
+1 -1
View File
@@ -277,7 +277,7 @@ export function McpManager() {
<span className={`h-2 w-2 rounded-full shrink-0 ${
s.status === 'connected' ? 'bg-green-500' :
s.status === 'failed' ? 'bg-red-500' :
s.status === 'pending' ? 'bg-blue-500' :
s.status === 'pending' ? 'bg-primary' :
s.status === 'disabled' ? 'bg-gray-400' :
'bg-yellow-500'
}`} />
+2 -2
View File
@@ -28,7 +28,7 @@ function getServerTypeInfo(server: MCPServer) {
const type = server.type || 'stdio';
switch (type) {
case 'sse':
return { label: 'SSE', icon: Wifi01Icon, color: 'text-blue-500' };
return { label: 'SSE', icon: Wifi01Icon, color: 'text-primary' };
case 'http':
return { label: 'HTTP', icon: GlobeIcon, color: 'text-green-500' };
default:
@@ -45,7 +45,7 @@ function getStatusBadge(status: McpRuntimeStatus['status']) {
case 'needs-auth':
return { label: 'Auth Required', className: 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20' };
case 'pending':
return { label: 'Pending', className: 'bg-blue-500/10 text-blue-600 border-blue-500/20' };
return { label: 'Pending', className: 'bg-primary/10 text-primary border-primary/20' };
case 'disabled':
return { label: 'Disabled', className: 'bg-gray-500/10 text-gray-500 border-gray-500/20' };
default:
+15 -3
View File
@@ -6,9 +6,19 @@ import { ArrowLeft01Icon, Copy01Icon, Tick01Icon, Loading02Icon } from "@hugeico
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ScrollArea } from "@/components/ui/scroll-area";
import { useTheme } from "next-themes";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useThemeFamily } from "@/lib/theme/context";
import { resolveCodeTheme, resolveHljsStyle } from "@/lib/theme/code-themes";
import { usePanel } from "@/hooks/usePanel";
function useFilePreviewCodeTheme() {
const { resolvedTheme } = useTheme();
const { family, families } = useThemeFamily();
const isDark = resolvedTheme === "dark";
const codeTheme = resolveCodeTheme(families, family);
return resolveHljsStyle(codeTheme, isDark);
}
import { useTranslation } from "@/hooks/useTranslation";
import type { FilePreview as FilePreviewType } from "@/types";
@@ -20,6 +30,7 @@ interface FilePreviewProps {
export function FilePreview({ filePath, onBack }: FilePreviewProps) {
const { workingDirectory } = usePanel();
const { t } = useTranslation();
const hljsStyle = useFilePreviewCodeTheme();
const [preview, setPreview] = useState<FilePreviewType | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -119,7 +130,7 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
<div className="rounded-md border border-border text-xs">
<SyntaxHighlighter
language={preview.language}
style={atomOneDark}
style={hljsStyle}
showLineNumbers
customStyle={{
margin: 0,
@@ -131,7 +142,8 @@ export function FilePreview({ filePath, onBack }: FilePreviewProps) {
lineNumberStyle={{
minWidth: "2.5em",
paddingRight: "8px",
color: "#636d83",
color: "var(--muted-foreground)",
opacity: 0.5,
userSelect: "none",
}}
>
+1 -1
View File
@@ -15,7 +15,7 @@ interface TaskCardProps {
const statusColors: Record<TaskStatus, string> = {
pending: "bg-yellow-500",
in_progress: "bg-blue-500",
in_progress: "bg-primary",
completed: "bg-green-500",
failed: "bg-red-500",
};
@@ -0,0 +1,226 @@
"use client";
import { useEffect, useState, useSyncExternalStore } from "react";
import { useTheme } from "next-themes";
import { codeToHtml, type BundledTheme } from "shiki";
import { useThemeFamily } from "@/lib/theme/context";
import {
resolveShikiTheme,
resolveShikiThemes,
} from "@/lib/theme/code-themes";
import { useTranslation } from "@/hooks/useTranslation";
import { HugeiconsIcon } from "@hugeicons/react";
import { Sun02Icon, Moon02Icon, ComputerDesk01Icon } from "@hugeicons/core-free-icons";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
// ── Theme Mode Pill Selector ────────────────────────────────────────
const MODE_OPTIONS = [
{ value: "light", icon: Sun02Icon, labelKey: "settings.modeLight" as const },
{ value: "dark", icon: Moon02Icon, labelKey: "settings.modeDark" as const },
{ value: "system", icon: ComputerDesk01Icon, labelKey: "settings.modeSystem" as const },
] as const;
function ThemeModePills({
value,
onChange,
}: {
value: string;
onChange: (v: string) => void;
}) {
const { t } = useTranslation();
return (
<div className="flex items-center rounded-lg border border-border/50 p-1 gap-1" role="radiogroup">
{MODE_OPTIONS.map((opt) => {
const selected = value === opt.value;
return (
<button
key={opt.value}
role="radio"
aria-checked={selected}
onClick={() => onChange(opt.value)}
className={cn(
"flex items-center gap-1.5 rounded-md px-3 py-1.5 text-xs font-medium transition-all",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
selected
? "bg-primary text-primary-foreground shadow-sm"
: "text-muted-foreground hover:bg-accent hover:text-accent-foreground"
)}
>
<HugeiconsIcon icon={opt.icon} className="h-3.5 w-3.5" />
{t(opt.labelKey)}
</button>
);
})}
</div>
);
}
// ── Shiki Code Preview ──────────────────────────────────────────────
const PREVIEW_CODE = `function greet(name: string) {
const time = new Date().getHours();
if (time < 12) return \`Good morning, \${name}\`;
return \`Hello, \${name}\`;
}`;
function ShikiCodePreview({ isDark }: { isDark: boolean }) {
const { family, families } = useThemeFamily();
const shikiMapping = resolveShikiTheme(families, family);
const { light, dark } = resolveShikiThemes(shikiMapping);
const theme: BundledTheme = isDark ? dark : light;
const [html, setHtml] = useState("");
useEffect(() => {
let cancelled = false;
codeToHtml(PREVIEW_CODE, {
lang: "typescript",
theme,
}).then((result) => {
if (!cancelled) setHtml(result);
}).catch(() => {});
return () => { cancelled = true; };
}, [theme]);
return (
<div className="rounded-md border border-border overflow-hidden">
<div className="flex items-center justify-between px-3 py-1.5 text-xs bg-muted text-muted-foreground">
<span className="font-medium">preview.ts</span>
<span className="rounded bg-accent px-1.5 py-0.5 text-accent-foreground">TypeScript</span>
</div>
{html ? (
<div
className="shiki-preview [&_pre]:!m-0 [&_pre]:!rounded-none [&_pre]:!text-xs [&_pre]:!leading-relaxed [&_pre]:!p-2 [&_code]:!text-xs"
dangerouslySetInnerHTML={{ __html: html }}
/>
) : (
<div className="h-24 flex items-center justify-center text-xs text-muted-foreground">
Loading…
</div>
)}
</div>
);
}
// ── UI Token Preview ────────────────────────────────────────────────
function UIPreview() {
return (
<div className="flex flex-wrap gap-2">
<button className="rounded-md bg-primary px-3 py-1 text-xs font-medium text-primary-foreground">
Primary
</button>
<button className="rounded-md bg-secondary px-3 py-1 text-xs font-medium text-secondary-foreground">
Secondary
</button>
<button className="rounded-md bg-destructive px-3 py-1 text-xs font-medium text-primary-foreground">
Destructive
</button>
<span className="inline-flex items-center rounded-full bg-accent px-2.5 py-0.5 text-[10px] font-medium text-accent-foreground">
Badge
</span>
<span className="inline-flex items-center rounded-full border border-border bg-card px-2.5 py-0.5 text-[10px] text-card-foreground">
Card
</span>
<span className="inline-flex items-center rounded-full bg-muted px-2.5 py-0.5 text-[10px] text-muted-foreground">
Muted
</span>
</div>
);
}
// ── Main Appearance Section ─────────────────────────────────────────
export function AppearanceSection() {
const { theme, setTheme, resolvedTheme } = useTheme();
const { family, setFamily, families } = useThemeFamily();
const { t } = useTranslation();
const isDark = resolvedTheme === "dark";
const mounted = useSyncExternalStore(
() => () => {},
() => true,
() => false
);
if (!mounted) return null;
return (
<div className="space-y-4">
{/* Section header — outside card */}
<div>
<h2 className="text-sm font-medium">{t("settings.appearance")}</h2>
<p className="text-xs text-muted-foreground">{t("settings.appearanceDesc")}</p>
</div>
<div className="rounded-lg border border-border/50 p-4 space-y-4 transition-shadow hover:shadow-sm">
{/* Mode */}
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<h3 className="text-xs font-medium">{t("settings.themeMode")}</h3>
<p className="text-[11px] text-muted-foreground">{t("settings.themeModeDesc")}</p>
</div>
<ThemeModePills value={theme || "system"} onChange={setTheme} />
</div>
{theme === "system" && resolvedTheme && (
<p className="text-[11px] text-muted-foreground pl-1">
{resolvedTheme === "dark" ? t("settings.modeDark") : t("settings.modeLight")}
</p>
)}
<div className="border-t border-border/30" />
{/* Family */}
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<h3 className="text-xs font-medium">{t("settings.themeFamily")}</h3>
<p className="text-[11px] text-muted-foreground">{t("settings.themeFamilyDesc")}</p>
</div>
<Select value={family} onValueChange={setFamily}>
<SelectTrigger className="w-[200px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
{families.map((f) => (
<SelectItem key={f.id} value={f.id}>
<span className="flex items-center gap-2">
{f.previewColors && (
<span className="flex gap-0.5">
<span
className="inline-block h-3 w-3 rounded-full border border-border/30"
style={{ background: f.previewColors.primaryLight }}
/>
<span
className="inline-block h-3 w-3 rounded-full border border-border/30"
style={{ background: f.previewColors.primaryDark }}
/>
</span>
)}
<span className="text-xs">{f.label}</span>
</span>
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="border-t border-border/30" />
{/* Preview */}
<div className="space-y-3">
<h3 className="text-xs font-medium text-muted-foreground">Preview</h3>
<UIPreview />
<ShikiCodePreview isDark={isDark} />
</div>
</div>
</div>
);
}
+16 -11
View File
@@ -20,6 +20,7 @@ import { useTranslation } from "@/hooks/useTranslation";
import { SUPPORTED_LOCALES, type Locale } from "@/i18n";
import type { TranslationKey } from "@/i18n";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { AppearanceSection } from "./AppearanceSection";
function UpdateCard() {
const { updateInfo, checking, checkForUpdates, downloadUpdate, quitAndInstall, setShowDialog } = useUpdate();
@@ -75,7 +76,7 @@ function UpdateCard() {
{updateInfo.updateAvailable ? (
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : isDownloading ? 'bg-yellow-500 animate-pulse' : 'bg-blue-500'}`} />
<span className={`h-2 w-2 rounded-full ${updateInfo.readyToInstall ? 'bg-green-500' : isDownloading ? 'bg-yellow-500 animate-pulse' : 'bg-primary'}`} />
<span className="text-sm">
{updateInfo.readyToInstall
? t('update.readyToInstall', { version: updateInfo.latestVersion })
@@ -98,7 +99,7 @@ function UpdateCard() {
{isDownloading && (
<div className="h-1.5 w-full overflow-hidden rounded-full bg-muted">
<div
className="h-full rounded-full bg-blue-500 transition-all"
className="h-full rounded-full bg-primary transition-all"
style={{ width: `${Math.min(updateInfo.downloadProgress!, 100)}%` }}
/>
</div>
@@ -209,8 +210,9 @@ export function GeneralSection() {
<div className="max-w-3xl space-y-6">
<UpdateCard />
{/* Auto-approve toggle */}
<div className={`rounded-lg border p-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
{/* General settings card */}
<div className={`rounded-lg border p-4 space-y-4 transition-shadow hover:shadow-sm ${skipPermissions ? "border-orange-500/50 bg-orange-500/5" : "border-border/50"}`}>
{/* Auto-approve toggle */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.autoApproveTitle')}</h2>
@@ -225,15 +227,15 @@ export function GeneralSection() {
/>
</div>
{skipPermissions && (
<div className="mt-3 flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
<div className="flex items-center gap-2 rounded-md bg-orange-500/10 px-3 py-2 text-xs text-orange-600 dark:text-orange-400">
<span className="h-2 w-2 shrink-0 rounded-full bg-orange-500" />
{t('settings.autoApproveWarning')}
</div>
)}
</div>
{/* Language picker */}
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<div className="border-t border-border/30" />
{/* Language picker */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.language')}</h2>
@@ -250,10 +252,10 @@ export function GeneralSection() {
</SelectContent>
</Select>
</div>
</div>
{/* Thinking mode */}
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
<div className="border-t border-border/30" />
{/* Thinking mode */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-sm font-medium">{t('settings.thinkingMode' as TranslationKey)}</h2>
@@ -272,6 +274,9 @@ export function GeneralSection() {
</div>
</div>
{/* Appearance */}
<AppearanceSection />
{/* Account info */}
{accountInfo && (
<div className="rounded-lg border border-border/50 p-4 transition-shadow hover:shadow-sm">
+1 -1
View File
@@ -134,7 +134,7 @@ export function CreateSkillDialog({
className={cn(
"flex-1 flex items-center gap-2 rounded-md border px-3 py-2 text-sm transition-colors",
scope === "project"
? "border-blue-500/50 bg-blue-500/10 text-blue-600 dark:text-blue-400"
? "border-primary/50 bg-primary/10 text-primary"
: "border-border hover:bg-accent"
)}
>
@@ -140,7 +140,7 @@ export function InstallProgressDialog({
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{phase === "running" && (
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-blue-500" />
<HugeiconsIcon icon={Loading02Icon} className="h-5 w-5 animate-spin text-primary" />
)}
{phase === "success" && (
<HugeiconsIcon icon={CheckmarkCircle02Icon} className="h-5 w-5 text-green-500" />
+2 -2
View File
@@ -126,8 +126,8 @@ export function SkillEditor({ skill, onSave, onDelete }: SkillEditorProps) {
: skill.source === "installed"
? "border-orange-500/40 text-orange-600 dark:text-orange-400"
: skill.source === "plugin"
? "border-indigo-500/40 text-indigo-600 dark:text-indigo-400"
: "border-blue-500/40 text-blue-600 dark:text-blue-400"
? "border-primary/40 text-primary"
: "border-primary/40 text-primary"
)}
>
{skill.source === "global" ? (