Skip to content

Commit e83414a

Browse files
author
catlog22
committed
feat(theme): implement dynamic theme logo with reactive color updates
1 parent e42597b commit e83414a

7 files changed

Lines changed: 179 additions & 31 deletions

File tree

ccw/frontend/src/components/icons/CCWLogo.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ========================================
44
// Line-style logo for Claude Code Workflow
55

6+
import { useEffect, useState } from 'react';
67
import { cn } from '@/lib/utils';
78

89
interface CCWLogoProps {
@@ -14,11 +15,54 @@ interface CCWLogoProps {
1415
showDot?: boolean;
1516
}
1617

18+
/**
19+
* Hook to get reactive theme accent color
20+
*/
21+
function useThemeAccentColor(): string {
22+
const [accentColor, setAccentColor] = useState<string>(() => {
23+
if (typeof document === 'undefined') return 'hsl(220, 60%, 65%)';
24+
const root = document.documentElement;
25+
const accentValue = getComputedStyle(root).getPropertyValue('--accent').trim();
26+
return accentValue ? `hsl(${accentValue})` : 'hsl(220, 60%, 65%)';
27+
});
28+
29+
useEffect(() => {
30+
const updateAccentColor = () => {
31+
const root = document.documentElement;
32+
const accentValue = getComputedStyle(root).getPropertyValue('--accent').trim();
33+
setAccentColor(accentValue ? `hsl(${accentValue})` : 'hsl(220, 60%, 65%)');
34+
};
35+
36+
// Initial update
37+
updateAccentColor();
38+
39+
// Watch for theme changes via MutationObserver
40+
const observer = new MutationObserver((mutations) => {
41+
mutations.forEach((mutation) => {
42+
if (mutation.attributeName === 'data-theme') {
43+
updateAccentColor();
44+
}
45+
});
46+
});
47+
48+
observer.observe(document.documentElement, {
49+
attributes: true,
50+
attributeFilter: ['data-theme'],
51+
});
52+
53+
return () => observer.disconnect();
54+
}, []);
55+
56+
return accentColor;
57+
}
58+
1759
/**
1860
* Line-style CCW logo component
1961
* Features three horizontal lines with a status dot that follows theme color
2062
*/
2163
export function CCWLogo({ size = 24, className, showDot = true }: CCWLogoProps) {
64+
const accentColor = useThemeAccentColor();
65+
2266
return (
2367
<svg
2468
width={size}
@@ -27,7 +71,7 @@ export function CCWLogo({ size = 24, className, showDot = true }: CCWLogoProps)
2771
fill="none"
2872
xmlns="http://www.w3.org/2000/svg"
2973
className={cn('ccw-logo', className)}
30-
style={{ color: 'hsl(var(--accent))' }}
74+
style={{ color: accentColor }}
3175
aria-label="Claude Code Workflow"
3276
>
3377
{/* Three horizontal lines - line style */}

ccw/frontend/src/components/layout/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ export function Header({
7474
to="/"
7575
className="flex items-center gap-2 text-lg font-semibold hover:opacity-80 transition-opacity"
7676
>
77-
<CCWLogo size={24} className="text-primary" />
77+
<CCWLogo size={24} />
7878
<span className="hidden sm:inline text-primary">{formatMessage({ id: 'navigation.header.brand' })}</span>
7979
<span className="sm:hidden text-primary">{formatMessage({ id: 'navigation.header.brandShort' })}</span>
8080
</Link>

ccw/frontend/src/index.css

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,16 @@
870870
/* ===========================
871871
CCW Logo
872872
=========================== */
873-
.ccw-logo {
874-
color: hsl(var(--accent));
873+
.ccw-logo,
874+
svg.ccw-logo,
875+
header .ccw-logo {
876+
color: hsl(var(--accent)) !important;
877+
}
878+
879+
/* Ensure dot inherits color */
880+
.ccw-logo circle,
881+
svg.ccw-logo circle {
882+
fill: currentColor;
875883
}
884+
885+
/* Theme-specific accent colors are applied automatically via --accent variable */

docs/.vitepress/theme/components/ProfessionalHome.vue

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -509,9 +509,9 @@ onUnmounted(() => {
509509
}
510510
511511
.section-container {
512-
max-width: 1152px;
512+
max-width: 1200px;
513513
margin: 0 auto;
514-
padding: 0;
514+
padding: 0 2rem;
515515
width: 100%;
516516
}
517517
@@ -529,13 +529,13 @@ onUnmounted(() => {
529529
}
530530
531531
.hero-container {
532-
max-width: 1152px;
532+
max-width: 1200px;
533533
margin: 0 auto;
534534
display: grid;
535535
grid-template-columns: 1fr 1fr;
536536
gap: 2rem;
537537
align-items: center;
538-
padding: 0;
538+
padding: 0 2rem;
539539
width: 100%;
540540
}
541541
@@ -568,7 +568,7 @@ onUnmounted(() => {
568568
}
569569
570570
.hero-title {
571-
font-size: 3.5rem;
571+
font-size: 2.75rem;
572572
line-height: 1.15;
573573
font-weight: 800;
574574
margin-bottom: 1.25rem;
@@ -863,8 +863,10 @@ onUnmounted(() => {
863863
grid-template-columns: 1fr 1.2fr;
864864
gap: 3rem;
865865
align-items: center;
866-
padding: 5rem 0;
866+
padding: 5rem 2rem;
867867
width: 100%;
868+
max-width: 1200px;
869+
margin: 0 auto;
868870
}
869871
.json-text h2 { font-size: 2.25rem; font-weight: 700; margin-bottom: 1.25rem; color: var(--vp-c-text-1); line-height: 1.2; }
870872
.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(() => {
910912
grid-template-columns: 0.85fr 1.15fr;
911913
gap: 3rem;
912914
align-items: start;
915+
max-width: 1200px;
916+
margin: 0 auto;
917+
padding: 0 2rem;
913918
}
914919
915920
.quickstart-title {
@@ -1037,6 +1042,8 @@ onUnmounted(() => {
10371042
.cta-card {
10381043
text-align: center;
10391044
padding: 3.5rem 2rem;
1045+
max-width: 800px;
1046+
margin: 0 auto;
10401047
background: var(--vp-c-bg-soft);
10411048
border: 1px solid var(--vp-c-divider);
10421049
border-radius: 24px;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<script setup lang="ts">
2+
import { ref, onMounted, onUnmounted } from 'vue'
3+
4+
const dotColor = ref('var(--vp-c-primary)')
5+
6+
function updateDotColor() {
7+
if (typeof document === 'undefined') return
8+
9+
const root = document.documentElement
10+
const style = getComputedStyle(root)
11+
const primaryColor = style.getPropertyValue('--vp-c-primary').trim()
12+
dotColor.value = primaryColor || 'currentColor'
13+
}
14+
15+
let observer: MutationObserver | null = null
16+
17+
onMounted(() => {
18+
updateDotColor()
19+
20+
// Watch for theme changes via MutationObserver
21+
observer = new MutationObserver(() => {
22+
updateDotColor()
23+
})
24+
25+
observer.observe(document.documentElement, {
26+
attributes: true,
27+
attributeFilter: ['data-theme', 'class'],
28+
})
29+
})
30+
31+
onUnmounted(() => {
32+
observer?.disconnect()
33+
})
34+
</script>
35+
36+
<template>
37+
<svg
38+
xmlns="http://www.w3.org/2000/svg"
39+
viewBox="0 0 24 24"
40+
fill="none"
41+
class="theme-logo"
42+
aria-label="Claude Code Workflow"
43+
>
44+
<!-- Three horizontal lines - use currentColor to inherit from text -->
45+
<line x1="3" y1="6" x2="18" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
46+
<line x1="3" y1="12" x2="15" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
47+
<line x1="3" y1="18" x2="12" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
48+
<!-- Status dot - follows theme primary color -->
49+
<circle cx="19" cy="17" r="3" :style="{ fill: dotColor }"/>
50+
</svg>
51+
</template>
52+
53+
<style scoped>
54+
.theme-logo {
55+
width: 24px;
56+
height: 24px;
57+
color: var(--vp-c-text-1);
58+
}
59+
60+
.theme-logo circle {
61+
fill: var(--vp-c-primary);
62+
transition: fill 0.3s ease;
63+
}
64+
</style>

docs/.vitepress/theme/layouts/Layout.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import DefaultTheme from 'vitepress/theme'
33
import { onBeforeUnmount, onMounted } from 'vue'
44
import { useDynamicIcon } from '../composables/useDynamicIcon'
5+
import ThemeLogo from '../components/ThemeLogo.vue'
56
67
let mediaQuery: MediaQueryList | null = null
78
let systemThemeChangeHandler: (() => void) | null = null
@@ -50,6 +51,11 @@ onBeforeUnmount(() => {
5051

5152
<template>
5253
<DefaultTheme.Layout>
54+
<!-- Custom logo in navbar that follows theme color -->
55+
<template #nav-bar-title-before>
56+
<ThemeLogo class="nav-logo" />
57+
</template>
58+
5359
<template #home-hero-after>
5460
<div class="hero-extensions">
5561
<div class="hero-stats">
@@ -120,6 +126,18 @@ onBeforeUnmount(() => {
120126
padding-left: 16px;
121127
}
122128
129+
.nav-logo {
130+
width: 24px;
131+
height: 24px;
132+
margin-right: 8px;
133+
flex-shrink: 0;
134+
}
135+
136+
/* Hide the default VitePress logo image since we use our custom component */
137+
:deep(.VPNavBarTitle .logo) {
138+
display: none;
139+
}
140+
123141
.skip-link {
124142
position: absolute;
125143
top: -100px;

docs/.vitepress/theme/styles/custom.css

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,8 @@
6363
}
6464

6565
/* Adjust sidebar and content layout */
66-
.VPDoc,
67-
.VPDoc[data-v-343c73d6] {
68-
padding-left: var(--vp-sidebar-width) !important;
69-
}
66+
/* NOTE: Removed duplicate padding-left - VitePress already handles sidebar layout */
67+
/* .VPDoc, .VPDoc[data-v-343c73d6] { padding-left: var(--vp-sidebar-width) !important; } */
7068

7169
/* Right side outline (TOC) adjustments */
7270
.VPDocOutline {
@@ -82,34 +80,41 @@
8280
/* ============================================
8381
* Home Page Override
8482
* ============================================ */
85-
.VPHome {
86-
padding-bottom: 0;
87-
}
88-
89-
/* Remove horizontal padding for home page only (has .VPHome class) */
90-
.VPHome .VPDoc {
91-
padding-left: 0 !important;
92-
padding-right: 0 !important;
93-
}
9483

95-
.VPHome .VPDoc .content-container {
96-
padding-left: 0 !important;
97-
padding-right: 0 !important;
84+
/* Use :has() to detect home page (contains .pro-home) */
85+
.VPContent:has(.pro-home) {
86+
padding: 0 !important;
87+
margin: 0 !important;
9888
max-width: 100% !important;
9989
}
10090

101-
.VPHome .VPDoc .content {
102-
padding-left: 0 !important;
103-
padding-right: 0 !important;
91+
.Layout:has(.pro-home) {
92+
max-width: 100% !important;
10493
}
10594

106-
.VPHome .VPContent {
95+
/* ProfessionalHome component full width */
96+
.pro-home {
97+
max-width: 100% !important;
98+
padding: 0 !important;
99+
margin: 0 !important;
100+
width: 100% !important;
101+
}
102+
103+
/* Ensure all sections extend to full width */
104+
.pro-home .hero-section,
105+
.pro-home .features-section,
106+
.pro-home .pipeline-section,
107+
.pro-home .json-section,
108+
.pro-home .quickstart-section,
109+
.pro-home .cta-section {
110+
width: 100% !important;
111+
margin: 0 !important;
107112
padding-left: 0 !important;
108113
padding-right: 0 !important;
109114
}
110115

111116
.VPHomeHero {
112-
padding: 80px 24px;
117+
padding: 80px 0;
113118
background: linear-gradient(180deg, var(--vp-c-bg-soft) 0%, var(--vp-c-bg) 100%);
114119
}
115120

0 commit comments

Comments
 (0)