From e83414abf361fb313dc92ebb9ba594cdd30ab410 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Sat, 28 Feb 2026 23:08:27 +0800 Subject: [PATCH] feat(theme): implement dynamic theme logo with reactive color updates --- ccw/frontend/src/components/icons/CCWLogo.tsx | 46 ++++++++++++- ccw/frontend/src/components/layout/Header.tsx | 2 +- ccw/frontend/src/index.css | 14 +++- .../theme/components/ProfessionalHome.vue | 19 ++++-- .../.vitepress/theme/components/ThemeLogo.vue | 64 +++++++++++++++++++ docs/.vitepress/theme/layouts/Layout.vue | 18 ++++++ docs/.vitepress/theme/styles/custom.css | 47 ++++++++------ 7 files changed, 179 insertions(+), 31 deletions(-) create mode 100644 docs/.vitepress/theme/components/ThemeLogo.vue diff --git a/ccw/frontend/src/components/icons/CCWLogo.tsx b/ccw/frontend/src/components/icons/CCWLogo.tsx index 618d4596..9127d86d 100644 --- a/ccw/frontend/src/components/icons/CCWLogo.tsx +++ b/ccw/frontend/src/components/icons/CCWLogo.tsx @@ -3,6 +3,7 @@ // ======================================== // Line-style logo for Claude Code Workflow +import { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; interface CCWLogoProps { @@ -14,11 +15,54 @@ interface CCWLogoProps { showDot?: boolean; } +/** + * Hook to get reactive theme accent color + */ +function useThemeAccentColor(): string { + const [accentColor, setAccentColor] = useState(() => { + if (typeof document === 'undefined') return 'hsl(220, 60%, 65%)'; + const root = document.documentElement; + const accentValue = getComputedStyle(root).getPropertyValue('--accent').trim(); + return accentValue ? `hsl(${accentValue})` : 'hsl(220, 60%, 65%)'; + }); + + useEffect(() => { + const updateAccentColor = () => { + const root = document.documentElement; + const accentValue = getComputedStyle(root).getPropertyValue('--accent').trim(); + setAccentColor(accentValue ? `hsl(${accentValue})` : 'hsl(220, 60%, 65%)'); + }; + + // Initial update + updateAccentColor(); + + // Watch for theme changes via MutationObserver + const observer = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.attributeName === 'data-theme') { + updateAccentColor(); + } + }); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['data-theme'], + }); + + return () => observer.disconnect(); + }, []); + + return accentColor; +} + /** * Line-style CCW logo component * Features three horizontal lines with a status dot that follows theme color */ export function CCWLogo({ size = 24, className, showDot = true }: CCWLogoProps) { + const accentColor = useThemeAccentColor(); + return ( {/* Three horizontal lines - line style */} diff --git a/ccw/frontend/src/components/layout/Header.tsx b/ccw/frontend/src/components/layout/Header.tsx index fbc1fada..2462c80d 100644 --- a/ccw/frontend/src/components/layout/Header.tsx +++ b/ccw/frontend/src/components/layout/Header.tsx @@ -74,7 +74,7 @@ export function Header({ to="/" className="flex items-center gap-2 text-lg font-semibold hover:opacity-80 transition-opacity" > - + {formatMessage({ id: 'navigation.header.brand' })} {formatMessage({ id: 'navigation.header.brandShort' })} diff --git a/ccw/frontend/src/index.css b/ccw/frontend/src/index.css index 125cb095..1e2a9238 100644 --- a/ccw/frontend/src/index.css +++ b/ccw/frontend/src/index.css @@ -870,6 +870,16 @@ /* =========================== CCW Logo =========================== */ -.ccw-logo { - color: hsl(var(--accent)); +.ccw-logo, +svg.ccw-logo, +header .ccw-logo { + color: hsl(var(--accent)) !important; } + +/* Ensure dot inherits color */ +.ccw-logo circle, +svg.ccw-logo circle { + fill: currentColor; +} + +/* Theme-specific accent colors are applied automatically via --accent variable */ diff --git a/docs/.vitepress/theme/components/ProfessionalHome.vue b/docs/.vitepress/theme/components/ProfessionalHome.vue index 87a095c1..7016320b 100644 --- a/docs/.vitepress/theme/components/ProfessionalHome.vue +++ b/docs/.vitepress/theme/components/ProfessionalHome.vue @@ -509,9 +509,9 @@ onUnmounted(() => { } .section-container { - max-width: 1152px; + max-width: 1200px; margin: 0 auto; - padding: 0; + padding: 0 2rem; width: 100%; } @@ -529,13 +529,13 @@ onUnmounted(() => { } .hero-container { - max-width: 1152px; + max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; align-items: center; - padding: 0; + padding: 0 2rem; width: 100%; } @@ -568,7 +568,7 @@ onUnmounted(() => { } .hero-title { - font-size: 3.5rem; + font-size: 2.75rem; line-height: 1.15; font-weight: 800; margin-bottom: 1.25rem; @@ -863,8 +863,10 @@ onUnmounted(() => { grid-template-columns: 1fr 1.2fr; gap: 3rem; align-items: center; - padding: 5rem 0; + padding: 5rem 2rem; width: 100%; + max-width: 1200px; + margin: 0 auto; } .json-text h2 { font-size: 2.25rem; font-weight: 700; margin-bottom: 1.25rem; color: var(--vp-c-text-1); line-height: 1.2; } .json-text p { font-size: 1.05rem; color: var(--vp-c-text-2); margin-bottom: 2rem; line-height: 1.7; } @@ -910,6 +912,9 @@ onUnmounted(() => { grid-template-columns: 0.85fr 1.15fr; gap: 3rem; align-items: start; + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; } .quickstart-title { @@ -1037,6 +1042,8 @@ onUnmounted(() => { .cta-card { text-align: center; padding: 3.5rem 2rem; + max-width: 800px; + margin: 0 auto; background: var(--vp-c-bg-soft); border: 1px solid var(--vp-c-divider); border-radius: 24px; diff --git a/docs/.vitepress/theme/components/ThemeLogo.vue b/docs/.vitepress/theme/components/ThemeLogo.vue new file mode 100644 index 00000000..846621ba --- /dev/null +++ b/docs/.vitepress/theme/components/ThemeLogo.vue @@ -0,0 +1,64 @@ + + + + + diff --git a/docs/.vitepress/theme/layouts/Layout.vue b/docs/.vitepress/theme/layouts/Layout.vue index 9f9f5ae3..703dd0d3 100644 --- a/docs/.vitepress/theme/layouts/Layout.vue +++ b/docs/.vitepress/theme/layouts/Layout.vue @@ -2,6 +2,7 @@ import DefaultTheme from 'vitepress/theme' import { onBeforeUnmount, onMounted } from 'vue' import { useDynamicIcon } from '../composables/useDynamicIcon' +import ThemeLogo from '../components/ThemeLogo.vue' let mediaQuery: MediaQueryList | null = null let systemThemeChangeHandler: (() => void) | null = null @@ -50,6 +51,11 @@ onBeforeUnmount(() => {